Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(252)

Side by Side Diff: third_party/pylint/checkers/imports.py

Issue 10447014: Add pylint to depot_tools. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fix unittests. Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/pylint/checkers/format.py ('k') | third_party/pylint/checkers/logging.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
18 from logilab.common.graph import get_cycles, DotBackend
19 from logilab.common.modutils import is_standard_module
20 from logilab.common.ureports import VerbatimText, Paragraph
21
22 from logilab import astng
23 from logilab.astng import are_exclusive
24
25 from pylint.interfaces import IASTNGChecker
26 from pylint.checkers import BaseChecker, EmptyReport
27
28
29 def get_first_import(node, context, name, base, level):
30 """return the node where [base.]<name> is imported or None if not found
31 """
32 first = None
33 found = False
34 for first in context.values():
35 if isinstance(first, astng.Import):
36 if name in [iname[0] for iname in first.names]:
37 found = True
38 break
39 elif isinstance(first, astng.From):
40 if base == first.modname and level == first.level and \
41 name in [iname[0] for iname in first.names]:
42 found = True
43 break
44 if found and first is not node and not are_exclusive(first, node):
45 return first
46
47 # utilities to represents import dependencies as tree and dot graph ###########
48
49 def filter_dependencies_info(dep_info, package_dir, mode='external'):
50 """filter external or internal dependencies from dep_info (return a
51 new dictionary containing the filtered modules only)
52 """
53 if mode == 'external':
54 filter_func = lambda x: not is_standard_module(x, (package_dir,))
55 else:
56 assert mode == 'internal'
57 filter_func = lambda x: is_standard_module(x, (package_dir,))
58 result = {}
59 for importee, importers in dep_info.items():
60 if filter_func(importee):
61 result[importee] = importers
62 return result
63
64 def make_tree_defs(mod_files_list):
65 """get a list of 2-uple (module, list_of_files_which_import_this_module),
66 it will return a dictionary to represent this as a tree
67 """
68 tree_defs = {}
69 for mod, files in mod_files_list:
70 node = (tree_defs, ())
71 for prefix in mod.split('.'):
72 node = node[0].setdefault(prefix, [{}, []])
73 node[1] += files
74 return tree_defs
75
76 def repr_tree_defs(data, indent_str=None):
77 """return a string which represents imports as a tree"""
78 lines = []
79 nodes = data.items()
80 for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])):
81 if not files:
82 files = ''
83 else:
84 files = '(%s)' % ','.join(files)
85 if indent_str is None:
86 lines.append('%s %s' % (mod, files))
87 sub_indent_str = ' '
88 else:
89 lines.append('%s\-%s %s' % (indent_str, mod, files))
90 if i == len(nodes)-1:
91 sub_indent_str = '%s ' % indent_str
92 else:
93 sub_indent_str = '%s| ' % indent_str
94 if sub:
95 lines.append(repr_tree_defs(sub, sub_indent_str))
96 return '\n'.join(lines)
97
98
99 def dependencies_graph(filename, dep_info):
100 """write dependencies as a dot (graphviz) file
101 """
102 done = {}
103 printer = DotBackend(filename[:-4], rankdir = "LR")
104 printer.emit('URL="." node[shape="box"]')
105 for modname, dependencies in dep_info.items():
106 done[modname] = 1
107 printer.emit_node(modname)
108 for modname in dependencies:
109 if modname not in done:
110 done[modname] = 1
111 printer.emit_node(modname)
112 for depmodname, dependencies in dep_info.items():
113 for modname in dependencies:
114 printer.emit_edge(modname, depmodname)
115 printer.generate(filename)
116
117
118 def make_graph(filename, dep_info, sect, gtype):
119 """generate a dependencies graph and add some information about it in the
120 report's section
121 """
122 dependencies_graph(filename, dep_info)
123 sect.append(Paragraph('%simports graph has been written to %s'
124 % (gtype, filename)))
125
126
127 # the import checker itself ###################################################
128
129 MSGS = {
130 'F0401': ('Unable to import %s',
131 'Used when pylint has been unable to import a module.'),
132 'R0401': ('Cyclic import (%s)',
133 'Used when a cyclic import between two or more modules is \
134 detected.'),
135
136 'W0401': ('Wildcard import %s',
137 'Used when `from module import *` is detected.'),
138 'W0402': ('Uses of a deprecated module %r',
139 'Used a module marked as deprecated is imported.'),
140 'W0403': ('Relative import %r, should be %r',
141 'Used when an import relative to the package directory is \
142 detected.'),
143 'W0404': ('Reimport %r (imported line %s)',
144 'Used when a module is reimported multiple times.'),
145 'W0406': ('Module import itself',
146 'Used when a module is importing itself.'),
147
148 'W0410': ('__future__ import is not the first non docstring statement',
149 'Python 2.5 and greater require __future__ import to be the \
150 first non docstring statement in the module.'),
151 }
152
153 class ImportsChecker(BaseChecker):
154 """checks for
155 * external modules dependencies
156 * relative / wildcard imports
157 * cyclic imports
158 * uses of deprecated modules
159 """
160
161 __implements__ = IASTNGChecker
162
163 name = 'imports'
164 msgs = MSGS
165 priority = -2
166
167 options = (('deprecated-modules',
168 {'default' : ('regsub', 'string', 'TERMIOS',
169 'Bastion', 'rexec'),
170 'type' : 'csv',
171 'metavar' : '<modules>',
172 'help' : 'Deprecated modules which should not be used, \
173 separated by a comma'}
174 ),
175 ('import-graph',
176 {'default' : '',
177 'type' : 'string',
178 'metavar' : '<file.dot>',
179 'help' : 'Create a graph of every (i.e. internal and \
180 external) dependencies in the given file (report RP0402 must not be disabled)'}
181 ),
182 ('ext-import-graph',
183 {'default' : '',
184 'type' : 'string',
185 'metavar' : '<file.dot>',
186 'help' : 'Create a graph of external dependencies in the \
187 given file (report RP0402 must not be disabled)'}
188 ),
189 ('int-import-graph',
190 {'default' : '',
191 'type' : 'string',
192 'metavar' : '<file.dot>',
193 'help' : 'Create a graph of internal dependencies in the \
194 given file (report RP0402 must not be disabled)'}
195 ),
196
197 )
198
199 def __init__(self, linter=None):
200 BaseChecker.__init__(self, linter)
201 self.stats = None
202 self.import_graph = None
203 self.__int_dep_info = self.__ext_dep_info = None
204 self.reports = (('RP0401', 'External dependencies',
205 self.report_external_dependencies),
206 ('RP0402', 'Modules dependencies graph',
207 self.report_dependencies_graph),
208 )
209
210 def open(self):
211 """called before visiting project (i.e set of modules)"""
212 self.linter.add_stats(dependencies={})
213 self.linter.add_stats(cycles=[])
214 self.stats = self.linter.stats
215 self.import_graph = {}
216
217 def close(self):
218 """called before visiting project (i.e set of modules)"""
219 # don't try to compute cycles if the associated message is disabled
220 if self.linter.is_message_enabled('R0401'):
221 for cycle in get_cycles(self.import_graph):
222 self.add_message('R0401', args=' -> '.join(cycle))
223
224 def visit_import(self, node):
225 """triggered when an import statement is seen"""
226 modnode = node.root()
227 for name, _ in node.names:
228 importedmodnode = self.get_imported_module(modnode, node, name)
229 if importedmodnode is None:
230 continue
231 self._check_relative_import(modnode, node, importedmodnode, name)
232 self._add_imported_module(node, importedmodnode.name)
233 self._check_deprecated_module(node, name)
234 self._check_reimport(node, name)
235
236
237 def visit_from(self, node):
238 """triggered when a from statement is seen"""
239 basename = node.modname
240 if basename == '__future__':
241 # check if this is the first non-docstring statement in the module
242 prev = node.previous_sibling()
243 if prev:
244 # consecutive future statements are possible
245 if not (isinstance(prev, astng.From)
246 and prev.modname == '__future__'):
247 self.add_message('W0410', node=node)
248 return
249 modnode = node.root()
250 importedmodnode = self.get_imported_module(modnode, node, basename)
251 if importedmodnode is None:
252 return
253 self._check_relative_import(modnode, node, importedmodnode, basename)
254 self._check_deprecated_module(node, basename)
255 for name, _ in node.names:
256 if name == '*':
257 self.add_message('W0401', args=basename, node=node)
258 continue
259 self._add_imported_module(node, '%s.%s' % (importedmodnode.name, nam e))
260 self._check_reimport(node, name, basename, node.level)
261
262 def get_imported_module(self, modnode, importnode, modname):
263 try:
264 return importnode.do_import_module(modname)
265 except astng.InferenceError, ex:
266 if str(ex) != modname:
267 args = '%r (%s)' % (modname, ex)
268 else:
269 args = repr(modname)
270 self.add_message("F0401", args=args, node=importnode)
271
272 def _check_relative_import(self, modnode, importnode, importedmodnode,
273 importedasname):
274 """check relative import. node is either an Import or From node, modname
275 the imported module name.
276 """
277 if 'W0403' not in self.active_msgs:
278 return
279 if importedmodnode.file is None:
280 return False # built-in module
281 if modnode is importedmodnode:
282 return False # module importing itself
283 if modnode.absolute_import_activated() or getattr(importnode, 'level', N one):
284 return False
285 if importedmodnode.name != importedasname:
286 # this must be a relative import...
287 self.add_message('W0403', args=(importedasname, importedmodnode.name ),
288 node=importnode)
289
290 def _add_imported_module(self, node, importedmodname):
291 """notify an imported module, used to analyze dependencies"""
292 context_name = node.root().name
293 if context_name == importedmodname:
294 # module importing itself !
295 self.add_message('W0406', node=node)
296 elif not is_standard_module(importedmodname):
297 # handle dependencies
298 importedmodnames = self.stats['dependencies'].setdefault(
299 importedmodname, set())
300 if not context_name in importedmodnames:
301 importedmodnames.add(context_name)
302 if is_standard_module( importedmodname, (self.package_dir(),) ):
303 # update import graph
304 mgraph = self.import_graph.setdefault(context_name, set())
305 if not importedmodname in mgraph:
306 mgraph.add(importedmodname)
307
308 def _check_deprecated_module(self, node, mod_path):
309 """check if the module is deprecated"""
310 for mod_name in self.config.deprecated_modules:
311 if mod_path == mod_name or mod_path.startswith(mod_name + '.'):
312 self.add_message('W0402', node=node, args=mod_path)
313
314 def _check_reimport(self, node, name, basename=None, level=0):
315 """check if the import is necessary (i.e. not already done)"""
316 if 'W0404' not in self.active_msgs:
317 return
318 frame = node.frame()
319 root = node.root()
320 contexts = [(frame, level)]
321 if root is not frame:
322 contexts.append((root, 0))
323 for context, level in contexts:
324 first = get_first_import(node, context, name, basename, level)
325 if first is not None:
326 self.add_message('W0404', node=node,
327 args=(name, first.fromlineno))
328
329
330 def report_external_dependencies(self, sect, _, dummy):
331 """return a verbatim layout for displaying dependencies"""
332 dep_info = make_tree_defs(self._external_dependencies_info().items())
333 if not dep_info:
334 raise EmptyReport()
335 tree_str = repr_tree_defs(dep_info)
336 sect.append(VerbatimText(tree_str))
337
338 def report_dependencies_graph(self, sect, _, dummy):
339 """write dependencies as a dot (graphviz) file"""
340 dep_info = self.stats['dependencies']
341 if not dep_info or not (self.config.import_graph
342 or self.config.ext_import_graph
343 or self.config.int_import_graph):
344 raise EmptyReport()
345 filename = self.config.import_graph
346 if filename:
347 make_graph(filename, dep_info, sect, '')
348 filename = self.config.ext_import_graph
349 if filename:
350 make_graph(filename, self._external_dependencies_info(),
351 sect, 'external ')
352 filename = self.config.int_import_graph
353 if filename:
354 make_graph(filename, self._internal_dependencies_info(),
355 sect, 'internal ')
356
357 def _external_dependencies_info(self):
358 """return cached external dependencies information or build and
359 cache them
360 """
361 if self.__ext_dep_info is None:
362 self.__ext_dep_info = filter_dependencies_info(
363 self.stats['dependencies'], self.package_dir(), 'external')
364 return self.__ext_dep_info
365
366 def _internal_dependencies_info(self):
367 """return cached internal dependencies information or build and
368 cache them
369 """
370 if self.__int_dep_info is None:
371 self.__int_dep_info = filter_dependencies_info(
372 self.stats['dependencies'], self.package_dir(), 'internal')
373 return self.__int_dep_info
374
375
376 def register(linter):
377 """required method to auto register this checker """
378 linter.register_checker(ImportsChecker(linter))
OLDNEW
« no previous file with comments | « third_party/pylint/checkers/format.py ('k') | third_party/pylint/checkers/logging.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698