import ihooks
import os
import sys
import imp

"""ImportSpy

Keeps track of imported modules and protects against concurrent imports.

This module helps save the filepath of every module which is imported.
This is used by the `AutoReloadingAppServer` (see doc strings for more
information) to restart the server if any source files change.

Other than keeping track of the filepaths, the behaviour of this module
loader is identical to Python's default behaviour.

It will intercept normal imports, and can be used as a replacement for
the builtin `imp` module, most easily like::

    import ImportSpy as imp

The functions `load_module` and `find_module` act like their equivalents
in the `imp` module, and you may use the `watchFile(filename)` function
to add more files to watch without importing them (configuration files,
for instance).

ianb: This can all be more easily implemented by looking at sys.modules,
along with select files (like config files). It's fast to poll sys.modules,
then we don't need this fanciness.

jdh: I'm not sure.  The original implementation from Tavis used sys.modules
like you say. I implemented this to avoid having any polling at all,
but perhaps it's not worth it. I'm not sure if there were other factors
in the design which pointed to this solution.

"""

try: # backward compatibility for Python < 2.3
	True, False
except NameError:
	True, False = 1, 0

try: # backward compatibility for Python < 2.4
	set
except NameError: # fallback for Python < 2.4
	try:
		from sets import Set as set
	except ImportError: # fallback for Python < 2.3
		from UserDict import UserDict
		class set(UserDict):
			def add(self, item):
				self.data[item] = item
			def remove(self, item):
				del self._dict[item]

class ImportLock:
	"""Lock for multi-threaded imports.

	Provides a lock for protecting against concurrent imports. This is
	necessary because WebKit is multithreaded and uses its own import hook.

	This class abstracts the difference between using the Python interpreter's
	global import lock, and using our own RLock. The global lock is the correct
	solution, but is only available in Python since version 2.2.3. If it's not
	available, we fall back to using an RLock (which is not as good, but better
	than nothing).

	"""

	def __init__(self):
		"""Create the lock.

		Aliases the `acquire` and `release` methods to
		`imp.acquire_lock` and `imp.release_lock` (if available),
		or to acquire and release our own RLock.

		"""
		if hasattr(imp,'acquire_lock'):
			self.acquire = imp.acquire_lock
			self.release = imp.release_lock
		else: # fallback for Python < 2.3
			from threading import RLock
			self._lock = RLock()
			self.acquire = self._lock.acquire
			self.release = self._lock.release


class ModuleLoader(ihooks.ModuleLoader):
	"""The import hook.

	Implements the ihook module loader that tracks imported modules.

	"""

	def __init__(self):
		"""Create import hook."""
		assert modloader is None, \
			"ModuleLoader can only be instantiated once"
		ihooks.ModuleLoader.__init__(self)
		self._fileList = {}
		self._notifyHook = None
		self._installed = False
		self._lock = ImportLock()
		self._modulesSet = set()

	def load_module(self, name, stuff):
		"""Replaces imp.load_module()."""
		try:
			try:
				self._lock.acquire()
				mod = ihooks.ModuleLoader.load_module(self, name, stuff)
			finally:
				self._lock.release()
			self.recordFileName(stuff, mod)
		except:
			self.recordFileName(stuff, None)
			raise
		return mod

	def recordModules(self, moduleNames):
		"""Record a list of modules."""
		for name in moduleNames:
			mod = sys.modules[name]
			if not hasattr(mod, '__file__'):
				# If we can't find it, we can't monitor it
				continue
			file = mod.__file__
			pathname = os.path.dirname(file)
			desc = None
			self.recordFileName((file, pathname, desc),
				sys.modules[name])

	def fileList(self):
		"""Return the list of tracked files."""
		return self._fileList

	def notifyOfNewFiles(self, hook):
		"""Register notification hook.

		Called by someone else to register that they'd like to be know
		when a new file is imported.

		"""
		self._notifyHook = hook

	def watchFile(self, filepath, getmtime=os.path.getmtime):
		"""Add more files to watch without importing them."""
		modtime = getmtime(filepath)
		self._fileList[filepath] = modtime
		# send notification that this file was imported
		if self._notifyHook:
			self._notifyHook(filepath, modtime)

	def recordFileName(self, stuff, mod, isfile=os.path.isfile):
		"""Record a file."""
		file, pathname, desc = stuff
		fileList = self._fileList
		if mod:
			assert sys.modules.has_key(mod.__name__)
			self._modulesSet.add(mod)
			# __orig_file__ is used for cheetah and psp mods; we want to
			# record the source filenames, not the auto-generated modules:
			f2 = getattr(mod, '__orig_file__', 0)
			f = getattr(mod, '__file__', 0)
			if f2 and f2 not in fileList.keys():
				try:
					if isfile(f2):
						self.watchFile(f2)
				except OSError:
					pass
			elif f and f not in fileList.keys():
				# record the .py file corresponding to each '.pyc'
				if f[-4:].lower() in ['.pyc', '.pyo']:
					f = f[:-1]
				try:
					if isfile(f):
						self.watchFile(f)
					else:
						self.watchFile(os.path.join(f, '__init__.py'))
				except OSError:
					pass
		# Also record filepaths which weren't successfully loaded, which
		# may happen due to a syntax error in a servlet, because we also
		# want to know when such a file is modified:
		elif pathname:
			if isfile(pathname):
				self.watchFile(pathname)

	def activate(self):
		"""Activate the ModuleLoader."""
		imp = ihooks.ModuleImporter(loader=modloader)
		ihooks.install(imp)
		self.recordModules(sys.modules.keys())
		self._installed = True

	def delModules(self, includePythonModules=False, excludePrefixes=[]):
		"""Delete imported modules.

		Deletes all the modules that the ImportSpy has ever imported unless
		they are part of WebKit. This in support of DebugAppServer's useful
		(yet imperfect) support for AutoReload.
		"""
		for mod in self._modulesSet:
			name = mod.__name__
			if not includePythonModules and (not hasattr(mod, '__file__')
					or mod.__file__.startswith(sys.prefix)):
				continue
			exclude = False
			for prefix in excludePrefixes:
				if mod.__name__.startswith(prefix):
					exclude = True
					break
			if exclude:
				continue
			del sys.modules[mod.__name__]
		self._modulesSet = set()


## Global ##

# We do this little double-assignment trick to make sure ModuleLoader
# is only instantiated once.
modloader = None
modloader = ModuleLoader()

def reset():
	global modloader
	modloader = None
	modloader = ModuleLoader()


def load_module(name, file, filename, description):
	"""See ModuleLoader.load_module."""
	return modloader.load_module(name,(file,filename,description))

def find_module(name,path=None):
	"""See ModuleLoader.find_module."""
	return modloader.find_module(name,path)

def watchFile(*args):
	"""See ModuleLoader.watchFile."""
	return modloader.watchFile(*args)
