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)