~launchpad-pqm/launchpad/devel

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