# Determine the names and filenames of the modules imported by a
# script, recursively.  This is done by scanning for lines containing
# import statements.  (The scanning has only superficial knowledge of
# Python syntax and no knowledge of semantics, so in theory the result
# may be incorrect -- however this is quite unlikely if you don't
# intentionally obscure your Python code.)

import os
import regex
import string
import sys


# Top-level interface.
# First argument is the main program (script).
# Second optional argument is list of modules to be searched as well.

def findmodules(scriptfile, modules = [], path = sys.path):
	todo = {}
	todo['__main__'] = scriptfile
	for name in modules:
		mod = os.path.basename(name)
		if mod[-3:] == '.py': mod = mod[:-3]
		elif mod[-4:] == '.pyc': mod = mod[:-4]
		todo[mod] = name
	done = closure(todo)
	return done


# Compute the closure of scanfile() and findmodule().
# Return a dictionary mapping module names to filenames.
# Writes to stderr if a file can't be or read.

def closure(todo):
	done = {}
	while todo:
		newtodo = {}
		for modname in todo.keys():
			if not done.has_key(modname):
				filename = todo[modname]
				if filename is None:
					filename = findmodule(modname)
				done[modname] = filename
				if filename in ('<builtin>', '<unknown>'):
					continue
				try:
					modules = scanfile(filename)
				except IOError, msg:
					sys.stderr.write("%s: %s\n" %
							 (filename, str(msg)))
					continue
				for m in modules:
					if not done.has_key(m):
						newtodo[m] = None
		todo = newtodo
	return done


# Scan a file looking for import statements.
# Return list of module names.
# Can raise IOError.

importstr = '\(^\|:\)[ \t]*import[ \t]+\([a-zA-Z0-9_, \t]+\)'
fromstr   = '\(^\|:\)[ \t]*from[ \t]+\([a-zA-Z0-9_]+\)[ \t]+import[ \t]+'
isimport = regex.compile(importstr)
isfrom = regex.compile(fromstr)

def scanfile(filename):
	allmodules = {}
	f = open(filename, 'r')
	try:
		while 1:
			line = f.readline()
			if not line: break # EOF
			while line[-2:] == '\\\n': # Continuation line
				line = line[:-2] + ' '
				line = line + f.readline()
			if isimport.search(line) >= 0:
				rawmodules = isimport.group(2)
				modules = string.splitfields(rawmodules, ',')
				for i in range(len(modules)):
					modules[i] = string.strip(modules[i])
			elif isfrom.search(line) >= 0:
				modules = [isfrom.group(2)]
			else:
				continue
			for mod in modules:
				allmodules[mod] = None
	finally:
		f.close()
	return allmodules.keys()


# Find the file containing a module, given its name.
# Return filename, or '<builtin>', or '<unknown>'.

builtins = sys.builtin_module_names
tails = ['.py', '.pyc']

def findmodule(modname, path = sys.path):
	if modname in builtins: return '<builtin>'
	for dirname in path:
		for tail in tails:
			fullname = os.path.join(dirname, modname + tail)
			try:
				f = open(fullname, 'r')
			except IOError:
				continue
			f.close()
			return fullname
	return '<unknown>'


# Test the above functions.

def test():
	if not sys.argv[1:]:
		print 'usage: python findmodules.py scriptfile [morefiles ...]'
		sys.exit(2)
	done = findmodules(sys.argv[1], sys.argv[2:])
	items = done.items()
	items.sort()
	for mod, file in [('Module', 'File')] + items:
		print "%-15s %s" % (mod, file)

if __name__ == '__main__':
	test()
