PyTis Import Nanny Tool

This is a command line tool that browses a project and finds and prints a report showing you how to reduce your scope to increase performance by showing unused imports.

Help Menu


    usage: importnanny filename.py 

    copyright
    =========
        This tool was written by Jeremy Lowery and extended by Josh Lee

    Features:
        *  Will not change files, only print a user friendly report


    options:
        none at this time, but optparser will be in the next verison

    examples:
        importnanny filename
         find . -iname *.py | importnanny
    

    

output example

jlee on DEV bin $ find . -iname '*.py' | importnanny
Results for './gimpy/gimpy.py'
Skipping * import on module 'util.MapToPython' (line 144)
  from util.MapToPython import *

Results for './gimpy/test/test_report.py'
Missing reference 'StringIO' for variable 'cStringIO' (line 15)
  from cStringIO import StringIO

Results for './gimpy/test/test_gimpy.py'
Skipping * import on module 'util.MapToPython' (line 7)
  from util.MapToPython import *

Results for './gimpy/__init__.py'
Skipping * import on module 'gimpy' (line 1)
  from gimpy import *

Results for './gimpy/util/__init__.py'
Missing reference 'MapHTMLParser' for variable 'xml_parser' (line 1)
  from xml_parser import MapHTMLParser
Skipping * import on module 'MapToPython' (line 2)
  from MapToPython import *
Missing reference 'Align' for variable 'align' (line 3)
  from align import Align
jlee on DEV bin $

Code:

FILENAME: /home/pytis/bin/importnanny
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
329.
330.
331.
332.
333.
334.
335.
336.
337.
338.
339.
340.
341.
342.
343.
344.
345.
#!/usr/bin/env python
"""importnanny
----------
Ensures that all imported modules are referenced from the Python code
in the module. Use to detect unneeded imports.

Provide a list of file names to the ARGV of the script or pipe in a 
list of file names to STDIN.

CHANGELOG
=========

 Version 3
 ---------
 Incorperated into PyTis tools.

 Version 2
 ---------
 Update by Josh Lee to handle meta classes.

 Version 1
 ---------
 Originaly created by Jeremy Lowery

"""

import compiler
import optparse
import os
import sys
import traceback
import pytis as PyTis

__curdir__ = os.path.abspath(os.path.dirname(__file__))
__author__ = 'Jeremy Lowery'
__created__ = '06:24pm 06 Jun, 2010'
__copyright__ = 'PyTis.com'
__version__ = '3.0'

class modref(object):
  def __init__(self, modname, alias, lineno):
    self.modname = modname
    self.alias = alias
    self.lineno = lineno

  def __repr__(self):
    return '<%s:%s at line %s>' % (self.alias, self.modname, self.lineno)

class ns_stack(list):
  def __init__(self):
    self._master_list = {}

  def add_mod(self, mod):
    self[0][mod.alias] = [mod]
    self._master_list[mod] = False
  
  def mark(self, name):
    """ Mark a name as matched. propigating up the stack until we're done
    or we hit a None. """
    for stack in self:
      for ref in stack.get(name, []):
        if ref is None:
          # We hit a name that was covered up
          return
        self._master_list[ref] = True
        return

  def missing(self):
    return [m for m, f in self._master_list.items() if not f]

  def push_stack(self, stack=None):
    if stack is None:
      stack = {}
    self.insert(0, stack)

  def pop_stack(self):
    return self.pop(0)
  
  def has_name(self, name):
    for lookup in self:
      if name in lookup:
        return True
    return False

  def cover_name(self, name):
    if self.has_name(name):
      if name not in self[0]:
        self[0][name] = []
      self[0][name].insert(0, None)

class ImportVisitor(object):
  def __init__(self):
    self._ns_stack = ns_stack()

  def visit(self, node):
    self._ns_stack.push_stack()
    self._visit(node)

  def missing(self):
    return self._ns_stack.missing()

  def _visit(self, node):
    global log
    if not node:
      return
    type = node.__class__.__name__
    if type == 'Function':
      log.debug('starting stack on function %s' % node.name)
      # The function name covers up
      self._ns_stack.cover_name(node.name)
      self._ns_stack.push_stack()

      # In the inner frame, the arguments cover up
      for arg in node.argnames:
        self._ns_stack.cover_name(arg)
      for sub in node.getChildren():
        self._visit(sub)
      self._ns_stack.pop_stack()
      log.debug('ending stack on function %s' % node.name)
    elif type == 'Class':
      # classes are tricky because what happens in the class scope never
      # leaves the class. The methods in the class don't have access to
      #this scope
      self._ns_stack.push_stack()
      for e in node.bases:
        self._dispatch_type(e)
      self.walk_class(node.code, class_name=node.name)
      self._ns_stack.pop_stack()
      self._ns_stack.cover_name(node.name)
    elif self._dispatch_type(node):
      pass
    elif hasattr(node, 'getChildren'):
      for sub in node.getChildren():
        self._visit(sub)

  def _dispatch_type(self, node):
    global log
    type = node.__class__.__name__
    if type == 'Import':
      for modname, alias in node.getChildren()[0]:
        if alias is None:
          alias = modname
        self._ns_stack.add_mod(modref(modname, alias, node.lineno))
    elif type == 'From':
      implist = node.getChildren()
      modname, implist = implist[:2]
      for var, alias in implist:
        if alias is None:
          alias = var
        self._ns_stack.add_mod(modref(modname, alias, node.lineno))
    elif type == 'AssName':
      var_name = node.getChildren()[0]
      self._ns_stack.cover_name(var_name)
    elif type == 'Name':
      log.debug('checking Name: %s stack level: %s' % (node.getChildren()[0], len(self._ns_stack)))
      self._ns_stack.mark(node.getChildren()[0])
    elif type == 'Getattr':
      # We have to handle the "DOT" notation with GetAttr.
      #print 'GETATTR', node.attrname, node
      #print '\t', node.getChildren()
      resolved = self.resolve_attribute(node)
      #print 'Our result for', node.attrname, resolved
      for r in resolved:
        self._ns_stack.mark(r)
    elif type == 'Lambda':
      self._ns_stack.push_stack()
      for arg_name in node.argnames:
        self._ns_stack.cover_name(arg_name)
      self._visit(node.code) 
      self._ns_stack.pop_stack()
    else:
      return False
    return True

  def resolve_attribute(self, node):
    """ Resolving an attribute builds up a dotted list of names. """
    head, tail = node.getChildren()
    head_type = head.__class__.__name__
    if head_type == 'Getattr':
      hv = self.resolve_attribute(head)
      if hv:
        return hv + ["%s.%s" % (hv[-1], tail)]
      else:
        return []
    elif head_type == 'Name':
      return [head.name, "%s.%s" % (head.name, tail)]
    else:
      #print 'Deferring type %s: %s' % (head_type, head)
      # It's something strange like b().c. We defer off to visit
      # However, none of the names in the tail will
      # count towards a resolution
      self._visit(head)
      return []

  def walk_class(self, node, class_name=None):
    global log
    # Whenever we dive into a lexical close, we have to track
    # ourselves and add ourselves back because of the funny scope class
    # semantics.
    if not hasattr(node, 'getChildren'):
      return
    for child in node.getChildren():
      type = child.__class__.__name__
      if type in ('Function', 'Lambda', 'Class'):
        log.debug('going to function from class on %s' % child)
        cur = self._ns_stack.pop_stack()

        # The class name is available in functions under the class, but
        # not the class scope itself
        if class_name:
          self._ns_stack.push_stack()
          self._ns_stack.cover_name(class_name)

        self._visit(child)
        if class_name:
          self._ns_stack.pop_stack()
        self._ns_stack.push_stack(cur)
      elif self._dispatch_type(child):
        pass
      else:
        self.walk_class(child, class_name)

def run(opts,files):
  global log
  for path in files:
    if path.strip():
      log.info("CHECKING: %s" % os.path.abspath(path))
      try:
        mod = compiler.parseFile(path)
      except SyntaxError:
        print 'Syntax Error in %r' % path
        traceback.print_exc(0, file=sys.stdout)
        continue
      v = ImportVisitor()
      v.visit(mod.node)
      file_buf = [x for x in open(path)]
      mods = v.missing()
      mods.sort(lambda x, y: cmp(x.lineno, y.lineno))

      if opts.verbose:
        show_filename = True 
      else:
        show_filename = False

      for mod in mods:
        if not show_filename:
          print 'Results for %r' % path
          show_filename = True
        if mod.alias == '*':
          print 'Skipping * import on module %r (line %s)'\
            % (mod.modname, mod.lineno)
          print ' %s' % file_buf[mod.lineno-1].strip()
        else:
          if mod.alias == mod.modname:
            print 'Missing reference %r (line %s)'\
              % (mod.modname, mod.lineno)
          else:
            print 'Missing reference %r for variable %r (line %s)'\
              % (mod.alias, mod.modname, mod.lineno)
          print ' %s' % file_buf[mod.lineno-1].strip()

      if show_filename:
        print 

def main():
  """usage: importnanny """
  global log

  hlp = __doc__ % dict(version=__version__,
                       author=__author__,
                       copyright=__copyright__)

  parser = PyTis.MyParser()

  if '?' in sys.argv[1:] or '-h' in sys.argv[1:] or '--help' in sys.argv[1:]:
    hlp = "%s\n%s" % (hlp,
"""
example:    

  jlee on  bin $ importnanny -rV pg_*
  CHECKING: /home/jlee/bin/pg_diff
  CHECKING: /home/jlee/bin/pg_diff.rb
  Syntax Error in '/home/jlee/bin/pg_diff.rb'
  Traceback (most recent call last):
    File "<string>", line 18
       require 'postgres'
                        ^
   SyntaxError: invalid syntax

  CHECKING: /home/jlee/bin/pg_func_diff
  CHECKING: /home/jlee/bin/pg_import
  Results for '/home/jlee/bin/pg_import'
  Missing reference 'cStringIO' (line 74)
   import cStringIO
  Missing reference 'math' (line 75)
   import math
  Missing reference 'pydoc' (line 78)
   import pydoc

  CHECKING: /home/jlee/bin/pg_strip

""" )

  parser.set_description(hlp)
  parser.set_usage(main.__doc__)
  parser.formatter.format_description = lambda s:s

  parser.add_option("-D", "--debug", action="store_true",
           default=False, 
           help="Enable debugging")

  parser.add_option("-v", "--version", action="store_true",
           default=False, 
           help="Display Version")

  parser.add_option("-V", "--verbose", action="store_true",
          default=False, 
          help="Be more Verbose")

  parser.add_option("-r", "--recursive", action="store_true", 
          default=False,
          help="Recursively apply copyright to all files.")

  (opts, args) = parser.parse_args()

  log = PyTis.set_logging(opts, 'importnanny')
  log.debug("OPTS debug: %s" % opts.debug)
  log.debug("OPTS version: %s" % opts.version)

  if opts.version:
    return PyTis.version(__version__)

  if sys.stdin.isatty():
    files = PyTis.filesFromArgs(opts,args)
  else:
    files = [x.strip() for x in sys.stdin]

  if not files:
   return parser.print_help()
  else:
   return run(opts,files)

if __name__ == '__main__':
  main()