10637.3.1
by Guilherme Salgado
Use the default python version instead of a hard-coded version |
1 |
#!/usr/bin/python
|
8452.3.3
by Karl Fogel
* utilities/: Add copyright header block to source files that were |
2 |
#
|
8687.15.2
by Karl Fogel
In files modified by r8688, change "<YEARS>" to "2009", as per |
3 |
# Copyright 2009 Canonical Ltd. This software is licensed under the
|
8687.15.3
by Karl Fogel
Shorten the copyright header block to two lines. |
4 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
8452.3.3
by Karl Fogel
* utilities/: Add copyright header block to source files that were |
5 |
|
1102
by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs |
6 |
"""
|
7 |
FindImports is a script that processes Python module dependencies. Currently
|
|
8 |
it can be used for finding unused imports and graphing module dependencies
|
|
9 |
(with graphviz). FindImports requires Python 2.3.
|
|
10 |
||
11 |
Syntax: findimports.py [options] [filename|dirname ...]
|
|
12 |
||
13 |
Options:
|
|
14 |
-h, --help This help message
|
|
15 |
||
16 |
-i, --imports Print dependency graph (default action).
|
|
17 |
-d, --dot Print dependency graph in dot format.
|
|
18 |
-n, --names Print dependency graph with all imported names.
|
|
19 |
||
20 |
-u, --unused Print unused imports.
|
|
21 |
-a, --all Print all unused imports (use together with -u).
|
|
22 |
||
23 |
Copyright (c) 2003, 2004 Marius Gedminas <marius@pov.lt>
|
|
24 |
||
25 |
This program is free software; you can redistribute it and/or modify it under
|
|
26 |
the terms of the GNU General Public License as published by the Free Software
|
|
27 |
Foundation; either version 2 of the License, or (at your option) any later
|
|
28 |
version.
|
|
29 |
||
30 |
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
31 |
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
32 |
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
33 |
||
34 |
You should have received a copy of the GNU General Public License along with
|
|
35 |
this program; if not, write to the Free Software Foundation, Inc., 675 Mass
|
|
36 |
Ave, Cambridge, MA 02139, USA.
|
|
37 |
"""
|
|
38 |
||
39 |
import os |
|
40 |
import sys |
|
41 |
import getopt |
|
42 |
import compiler |
|
43 |
import linecache |
|
44 |
from compiler import ast |
|
45 |
from compiler.visitor import ASTVisitor |
|
46 |
||
47 |
||
48 |
class ImportFinder(ASTVisitor): |
|
49 |
"""AST visitor that collects all imported names in its imports attribute.
|
|
50 |
||
51 |
For example, the following import statements in the AST tree
|
|
52 |
||
53 |
import a, b.c, d as e
|
|
54 |
from q.w.e import x, y as foo, z
|
|
55 |
from woof import *
|
|
56 |
||
57 |
will cause imports to contain
|
|
58 |
||
59 |
a
|
|
60 |
b.c
|
|
61 |
d
|
|
62 |
q.w.e.x
|
|
63 |
q.w.e.y
|
|
64 |
q.w.e.z
|
|
65 |
woof.*
|
|
66 |
"""
|
|
67 |
||
68 |
def __init__(self): |
|
69 |
self.imports = [] |
|
70 |
||
71 |
def visitImport(self, node): |
|
72 |
for name, imported_as in node.names: |
|
73 |
self.imports.append(name) |
|
74 |
||
75 |
def visitFrom(self, node): |
|
76 |
for name, imported_as in node.names: |
|
77 |
self.imports.append('%s.%s' % (node.modname, name)) |
|
78 |
||
79 |
||
80 |
class UnusedName(object): |
|
81 |
||
82 |
def __init__(self, name, lineno): |
|
83 |
self.name = name |
|
84 |
self.lineno = lineno |
|
85 |
||
86 |
||
87 |
class ImportFinderAndNametracker(ImportFinder): |
|
88 |
"""ImportFinder that also keeps track on used names."""
|
|
89 |
||
90 |
def __init__(self): |
|
91 |
ImportFinder.__init__(self) |
|
92 |
self.unused_names = {} |
|
93 |
||
94 |
def visitImport(self, node): |
|
95 |
ImportFinder.visitImport(self, node) |
|
96 |
for name, imported_as in node.names: |
|
97 |
if not imported_as: |
|
98 |
imported_as = name |
|
99 |
if imported_as != "*": |
|
100 |
self.unused_names[imported_as] = UnusedName(imported_as, |
|
101 |
node.lineno) |
|
102 |
||
103 |
def visitFrom(self, node): |
|
104 |
ImportFinder.visitFrom(self, node) |
|
105 |
for name, imported_as in node.names: |
|
106 |
if not imported_as: |
|
107 |
imported_as = name |
|
108 |
if imported_as != "*": |
|
109 |
self.unused_names[imported_as] = UnusedName(imported_as, |
|
110 |
node.lineno) |
|
111 |
||
112 |
def visitName(self, node): |
|
113 |
if node.name in self.unused_names: |
|
114 |
del self.unused_names[node.name] |
|
115 |
||
116 |
def visitGetattr(self, node): |
|
117 |
full_name = [node.attrname] |
|
118 |
parent = node.expr |
|
119 |
while isinstance(parent, compiler.ast.Getattr): |
|
120 |
full_name.append(parent.attrname) |
|
121 |
parent = parent.expr |
|
122 |
if isinstance(parent, compiler.ast.Name): |
|
123 |
full_name.append(parent.name) |
|
124 |
full_name.reverse() |
|
125 |
name = "" |
|
126 |
for part in full_name: |
|
127 |
if name: name = '%s.%s' % (name, part) |
|
128 |
else: name += part |
|
129 |
if name in self.unused_names: |
|
130 |
del self.unused_names[name] |
|
131 |
for c in node.getChildNodes(): |
|
132 |
self.visit(c) |
|
133 |
||
134 |
||
135 |
def find_imports(filename): |
|
136 |
"""Find all imported names in a given file."""
|
|
137 |
ast = compiler.parseFile(filename) |
|
138 |
visitor = ImportFinder() |
|
139 |
compiler.walk(ast, visitor) |
|
140 |
return visitor.imports |
|
141 |
||
142 |
def find_imports_and_track_names(filename): |
|
143 |
"""Find all imported names in a given file."""
|
|
144 |
ast = compiler.parseFile(filename) |
|
145 |
visitor = ImportFinderAndNametracker() |
|
146 |
compiler.walk(ast, visitor) |
|
147 |
return visitor.imports, visitor.unused_names |
|
148 |
||
149 |
||
150 |
class Module(object): |
|
151 |
||
152 |
def __init__(self, modname, filename): |
|
153 |
self.modname = modname |
|
154 |
self.filename = filename |
|
155 |
||
156 |
||
157 |
class ModuleGraph(object): |
|
158 |
||
159 |
trackUnusedNames = False |
|
160 |
all_unused = False |
|
161 |
||
162 |
def __init__(self): |
|
163 |
self.modules = {} |
|
164 |
self.path = sys.path |
|
165 |
self._module_cache = {} |
|
10293.3.1
by Max Bowsher
Remove use of the deprecated sets module. |
166 |
self._warned_about = set() |
1102
by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs |
167 |
|
168 |
def parsePathname(self, pathname): |
|
169 |
if os.path.isdir(pathname): |
|
170 |
for root, dirs, files in os.walk(pathname): |
|
171 |
for fn in files: |
|
172 |
# ignore emacsish junk
|
|
173 |
if fn.endswith('.py') and not fn.startswith('.#'): |
|
174 |
self.parseFile(os.path.join(root, fn)) |
|
175 |
else: |
|
176 |
self.parseFile(pathname) |
|
177 |
||
178 |
def parseFile(self, filename): |
|
179 |
modname = self.filenameToModname(filename) |
|
180 |
module = Module(modname, filename) |
|
181 |
self.modules[modname] = module |
|
182 |
if self.trackUnusedNames: |
|
183 |
module.imported_names, module.unused_names = \ |
|
184 |
find_imports_and_track_names(filename) |
|
185 |
else: |
|
186 |
module.imported_names = find_imports(filename) |
|
187 |
module.unused_names = None |
|
188 |
dir = os.path.dirname(filename) |
|
10293.3.1
by Max Bowsher
Remove use of the deprecated sets module. |
189 |
module.imports = set([self.findModuleOfName(name, filename, dir) |
1102
by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs |
190 |
for name in module.imported_names]) |
191 |
||
192 |
def filenameToModname(self, filename): |
|
193 |
for ext in ('.py', '.so', '.dll'): |
|
194 |
if filename.endswith(ext): |
|
195 |
break
|
|
196 |
else: |
|
197 |
print >> sys.stderr, "%s: unknown file name extension" % filename |
|
198 |
longest_prefix_len = 0 |
|
199 |
filename = os.path.abspath(filename) |
|
200 |
for prefix in self.path: |
|
201 |
prefix = os.path.abspath(prefix) |
|
202 |
if (filename.startswith(prefix) |
|
203 |
and len(prefix) > longest_prefix_len): |
|
204 |
longest_prefix_len = len(prefix) |
|
205 |
filename = filename[longest_prefix_len:-len('.py')] |
|
206 |
if filename.startswith(os.path.sep): |
|
207 |
filename = filename[len(os.path.sep):] |
|
208 |
modname = ".".join(filename.split(os.path.sep)) |
|
209 |
return modname |
|
210 |
||
211 |
def findModuleOfName(self, dotted_name, filename, extrapath=None): |
|
212 |
if dotted_name.endswith('.*'): |
|
213 |
return dotted_name[:-2] |
|
214 |
name = dotted_name |
|
215 |
while name: |
|
216 |
candidate = self.isModule(name, extrapath) |
|
217 |
if candidate: |
|
218 |
return candidate |
|
219 |
candidate = self.isPackage(name, extrapath) |
|
220 |
if candidate: |
|
221 |
return candidate |
|
222 |
name = name[:name.rfind('.')] |
|
223 |
if dotted_name not in self._warned_about: |
|
224 |
print >> sys.stderr, ("%s: could not find %s" |
|
225 |
% (filename, dotted_name)) |
|
226 |
self._warned_about.add(dotted_name) |
|
227 |
return dotted_name |
|
228 |
||
229 |
def isModule(self, dotted_name, extrapath=None): |
|
230 |
try: |
|
231 |
return self._module_cache[(dotted_name, extrapath)] |
|
232 |
except KeyError: |
|
233 |
pass
|
|
234 |
if dotted_name in sys.modules: |
|
235 |
return dotted_name |
|
236 |
filename = dotted_name.replace('.', os.path.sep) |
|
237 |
if extrapath: |
|
238 |
for ext in ('.py', '.so', '.dll'): |
|
239 |
candidate = os.path.join(extrapath, filename) + ext |
|
240 |
if os.path.exists(candidate): |
|
241 |
modname = self.filenameToModname(candidate) |
|
242 |
self._module_cache[(dotted_name, extrapath)] = modname |
|
243 |
return modname |
|
244 |
try: |
|
245 |
return self._module_cache[(dotted_name, None)] |
|
246 |
except KeyError: |
|
247 |
pass
|
|
248 |
for dir in self.path: |
|
249 |
for ext in ('.py', '.so', '.dll'): |
|
250 |
candidate = os.path.join(dir, filename) + ext |
|
251 |
if os.path.exists(candidate): |
|
252 |
modname = self.filenameToModname(candidate) |
|
253 |
self._module_cache[(dotted_name, extrapath)] = modname |
|
254 |
self._module_cache[(dotted_name, None)] = modname |
|
255 |
return modname |
|
256 |
return None |
|
257 |
||
258 |
def isPackage(self, dotted_name, extrapath=None): |
|
259 |
candidate = self.isModule(dotted_name + '.__init__', extrapath) |
|
260 |
if candidate: |
|
261 |
candidate = candidate[:-len(".__init__")] |
|
262 |
return candidate |
|
263 |
||
264 |
def listModules(self): |
|
265 |
modules = list(self.modules.items()) |
|
266 |
modules.sort() |
|
267 |
return [module for name, module in modules] |
|
268 |
||
269 |
def printImportedNames(self): |
|
270 |
for module in self.listModules(): |
|
271 |
print "%s:" % module.modname |
|
272 |
print " %s" % "\n ".join(module.imported_names) |
|
273 |
||
274 |
def printImports(self): |
|
275 |
for module in self.listModules(): |
|
276 |
print "%s:" % module.modname |
|
277 |
imports = list(module.imports) |
|
278 |
imports.sort() |
|
279 |
print " %s" % "\n ".join(imports) |
|
280 |
||
281 |
def printUnusedImports(self): |
|
282 |
for module in self.listModules(): |
|
283 |
names = [(unused.lineno, unused.name) |
|
284 |
for unused in module.unused_names.itervalues()] |
|
285 |
names.sort() |
|
286 |
for lineno, name in names: |
|
287 |
if not self.all_unused: |
|
288 |
line = linecache.getline(module.filename, lineno) |
|
289 |
if '#' in line: |
|
290 |
continue # assume there's a comment explaining why it |
|
291 |
# is not used
|
|
292 |
print "%s:%s: %s not used" % (module.filename, lineno, name) |
|
293 |
||
294 |
def printDot(self): |
|
295 |
print "digraph ModuleDependencies {" |
|
296 |
print " node[shape=box];" |
|
10293.3.1
by Max Bowsher
Remove use of the deprecated sets module. |
297 |
allNames = set() |
1102
by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs |
298 |
nameDict = {} |
299 |
for n, module in enumerate(self.listModules()): |
|
300 |
module._dot_name = "mod%d" % n |
|
301 |
nameDict[module.modname] = module._dot_name |
|
302 |
print " %s[label=\"%s\"];" % (module._dot_name, module.modname) |
|
303 |
for name in module.imports: |
|
304 |
if name not in self.modules: |
|
305 |
allNames.add(name) |
|
306 |
print " node[style=dotted];" |
|
307 |
names = list(allNames) |
|
308 |
names.sort() |
|
309 |
for n, name in enumerate(names): |
|
310 |
nameDict[name] = id = "extmod%d" % n |
|
311 |
print " %s[label=\"%s\"];" % (id, name) |
|
312 |
for module in self.modules.values(): |
|
313 |
for other in module.imports: |
|
314 |
print " %s -> %s;" % (nameDict[module.modname], |
|
315 |
nameDict[other]); |
|
316 |
print "}" |
|
317 |
||
318 |
||
319 |
def main(argv=sys.argv): |
|
320 |
progname = os.path.basename(argv[0]) |
|
321 |
helptext = __doc__.strip().replace('findimports.py', progname) |
|
322 |
g = ModuleGraph() |
|
323 |
action = g.printImports |
|
324 |
try: |
|
325 |
opts, args = getopt.getopt(argv[1:], 'duniah', |
|
326 |
['dot', 'unused', 'all', 'names', 'imports', |
|
327 |
'help']) |
|
328 |
except getopt.error, e: |
|
329 |
print >> sys.stderr, "%s: %s" % (progname, e) |
|
330 |
print >> sys.stderr, "Try %s --help." % progname |
|
331 |
return 1 |
|
332 |
for k, v in opts: |
|
333 |
if k in ('-d', '--dot'): |
|
334 |
action = g.printDot |
|
335 |
elif k in ('-u', '--unused'): |
|
336 |
action = g.printUnusedImports |
|
337 |
elif k in ('-a', '--all'): |
|
338 |
g.all_unused = True |
|
339 |
elif k in ('-n', '--names'): |
|
340 |
action = g.printImportedNames |
|
341 |
elif k in ('-i', '--imports'): |
|
342 |
action = g.printImports |
|
343 |
elif k in ('-h', '--help'): |
|
344 |
print helptext |
|
345 |
return 0 |
|
346 |
g.trackUnusedNames = (action == g.printUnusedImports) |
|
347 |
if not args: |
|
348 |
args = ['.'] |
|
349 |
for fn in args: |
|
350 |
g.parsePathname(fn) |
|
351 |
action() |
|
352 |
return 0 |
|
353 |
||
354 |
if __name__ == '__main__': |
|
355 |
sys.exit(main()) |
|
356 |