Source code for featuremonkey.importhooks

import sys
import importlib

class ImportHookBase(object):
    """
    base class for import hooks

    provides utility methods to install and
    remove the hook
    """
    _hook=None

    @classmethod
    def _insert_hook(cls):
        '''
        appends the hook to sys.meta_path

        subclasses may override this method
        to prepend the hook instead of appending it or
        to install the hook in sys.path_hooks instead
        '''
        sys.meta_path.insert(0, cls._hook)

    @classmethod
    def _install(cls):
        """
        install the hook if not already done
        """
        if not cls._hook:
            cls._hook = cls()
            cls._insert_hook()

    @classmethod
    def _uninstall(cls):
        """
        uninstall the hook if installed
        """
        if cls._hook:
            sys.meta_path.remove(cls._hook)
            cls._hook = None


def load_fsts(fst_list):
    from featuremonkey import CompositionError
    result = []
    for elem in fst_list:
        if isinstance(elem, str):
            try:
                elem = importlib.import_module(elem)
            except ImportError:
                raise CompositionError(
                    'FST "%s" cannot be imported!' % elem
                )
        result.append(elem)
    return result


class LazyComposerHook(ImportHookBase):
    """
    Import Hook required for compose_later to work.

    if fsts are queued for composition
    they are superimposed on the target module right
    after it is imported
    """
    _to_compose = dict()

    @classmethod
    def add(cls, module_name, fsts, composer):
        '''
        add a couple of fsts to be superimposed on the module given
        by module_name as soon as it is imported.

        internal - use featuremonkey.compose_later
        '''
        cls._to_compose.setdefault(module_name, [])
        cls._to_compose[module_name].append(
            (list(fsts), composer)
        )
        cls._install()


    def find_module(self, fullname, path=None):
        if fullname in self._to_compose:
            return self


    def load_module(self, module_name):
        layers = self._to_compose.pop(module_name)
        module = importlib.import_module(module_name)
        for fsts, composer in layers:
            fsts = load_fsts(fsts)
            fsts.append(module)
            composer.compose(*fsts)
        if not self._to_compose:
            self._uninstall()
        return module


class ImportGuard(ImportError): pass

[docs]class ImportGuardHook(ImportHookBase): """ Import Hook to implement import guards. In Python imports can have side-effects and import order may be relevant. When using featuremonkey, it is important to compose the product before making references to it. Otherwise, you could end up with a reference to a module/class/object that has only been composed partially. This may introduce subtle bugs that are hard to debug. Using an import guard, you can enforce that a module cannot be imported until the import guard on that module is dropped again. Importing a guarded module results in an ImportGuard exception being thrown. Usually, you don`t want to catch these: better fail during the composition phase than continuing to run a miscomposed program. The existance of the import hook is considered an implementation detail. The public API to import guards are ``featuremonkey.add_import_guard`` and ``featuremonkey.remove_import_guard``. """ _guards = dict() _num_entries = 0 @classmethod def _insert_hook(cls): #the guard hook needs to be first sys.meta_path.insert(0, cls._hook) @classmethod def add(cls, module_name, msg=''): ''' Until the guard is dropped again, disallow imports of the module given by ``module_name``. If the module is imported while the guard is in place an ``ImportGuard`` is raised. An additional message on why the module cannot be imported can optionally be specified using the parameter ``msg``. If multiple guards are placed on the same module, all these guards have to be dropped before the module can be imported again. ''' if module_name in sys.modules: raise ImportGuard( 'Module to guard has already been imported: ' + module_name ) cls._guards.setdefault(module_name, []) cls._guards[module_name].append(msg) cls._num_entries += 1 cls._install() @classmethod def remove(cls, module_name): """ drop a previously created guard on ``module_name`` if the module is not guarded, then this is a no-op. """ module_guards = cls._guards.get(module_name, False) if module_guards: module_guards.pop() cls._num_entries -= 1 if cls._num_entries < 1: if cls._num_entries < 0: raise Exception( 'Bug: ImportGuardHook._num_entries became negative!' ) cls._uninstall() def find_module(self, fullname, path=None): if self._guards.get(fullname, False): return self def load_module(self, module_name): raise ImportGuard( 'Import while import guard in place: ' #msg of latest guard that has been placed on module + (self._guards[module_name][-1] or module_name) )