Source code for enaml.core.import_hooks

#------------------------------------------------------------------------------
#  Copyright (c) 2011, Enthought, Inc.
#  All rights reserved.
#------------------------------------------------------------------------------
from abc import ABCMeta, abstractmethod
from collections import defaultdict, namedtuple
import imp
import marshal
import os
import struct
import sys
import types

from .enaml_compiler import EnamlCompiler, COMPILER_VERSION
from .parser import parse

from ..utils import abstractclassmethod


# The magic number as symbols for the current Python interpreter. These
# define the naming scheme used when create cached files and directories.
MAGIC = imp.get_magic()
try:
    MAGIC_TAG = 'enaml-py%s%s-cv%s' % (
        sys.version_info.major, sys.version_info.minor, COMPILER_VERSION,
    )
except AttributeError: 
    # Python 2.6 compatibility
    MAGIC_TAG = 'enaml-py%s%s-cv%s' % (
        sys.version_info[0], sys.version_info[1], COMPILER_VERSION,
    )
CACHEDIR = '__enamlcache__'


#------------------------------------------------------------------------------
# Import Helpers
#------------------------------------------------------------------------------
EnamlFileInfo = namedtuple('EnamlFileInfo', 'src_path, cache_path, cache_dir')


[docs]def make_file_info(src_path): """ Create an EnamlFileInfo object for the given src_path. Parameters ---------- src_path : string The full path to the .enaml file. Returns ------- result : FileInfo A properly populated EnamlFileInfo object. """ root, tail = os.path.split(src_path) fnroot, _ = os.path.splitext(tail) cache_dir = os.path.join(root, CACHEDIR) fn = ''.join((fnroot, '.', MAGIC_TAG, os.path.extsep, 'enamlc')) cache_path = os.path.join(cache_dir, fn) return EnamlFileInfo(src_path, cache_path, cache_dir) #------------------------------------------------------------------------------ # Abstract Enaml Importer #------------------------------------------------------------------------------
[docs]class AbstractEnamlImporter(object): """ An abstract base class which defines the api required to implement an Enaml importer. """ __metaclass__ = ABCMeta # Count the number of times an importer has been installed. # Only uninstall it when the count hits 0 again. This permits # proper nesting of import contexts. _install_count = defaultdict(int) @classmethod
[docs] def install(cls): """ Appends this importer into sys.meta_path. """ cls._install_count[cls] += 1 if cls not in sys.meta_path: sys.meta_path.append(cls)
@classmethod
[docs] def uninstall(cls): """ Removes this importer from sys.meta_path. """ cls._install_count[cls] -= 1 if cls._install_count[cls] <= 0 and cls in sys.meta_path: sys.meta_path.remove(cls) #-------------------------------------------------------------------------- # Python Import API #--------------------------------------------------------------------------
@classmethod
[docs] def find_module(cls, fullname, path=None): """ Finds the given Enaml module and returns an importer, or None if the module is not found. """ loader = cls.locate_module(fullname, path) if loader is not None: if not isinstance(loader, AbstractEnamlImporter): msg = 'Enaml imports received invalid loader object %s' raise ImportError(msg % loader) return loader
[docs] def load_module(self, fullname): """ Loads and returns the Python module for the given enaml path. If a module already exisist in sys.path, the existing module is reused, otherwise a new one is created. """ code, path = self.get_code() if fullname in sys.modules: mod = sys.modules[fullname] else: mod = sys.modules[fullname] = types.ModuleType(fullname) mod.__loader__ = self mod.__file__ = path # Even though the import hook is already installed, this is a # safety net to avoid potentially hard to find bugs if code has # manually installed and removed a hook. The contract here is # that the import hooks are always installed when executing the # module code of an Enaml file. with imports(): exec code in mod.__dict__ return mod #-------------------------------------------------------------------------- # Abstract API #--------------------------------------------------------------------------
@abstractclassmethod
[docs] def locate_module(cls, fullname, path=None): """ Searches for the given Enaml module and returns an instance of AbstractEnamlImporter on success. Paramters --------- fullname : string The fully qualified name of the module. path : string or None The subpackage __path__ for submodules and subpackages or None if a top-level module. Returns ------- result : Instance(AbstractEnamlImporter) or None If the Enaml module is located an instance of the importer that will perform the rest of the operations is returned. Otherwise, returns None. """ raise NotImplementedError
@abstractmethod
[docs] def get_code(self): """ Loads and returns the code object for the Enaml module and the full path to the module for use as the __file__ attribute of the module. Returns ------- result : (code, path) The Python code object for the .enaml module, and the full path to the module as a string. """ raise NotImplementedError #------------------------------------------------------------------------------ # Default Enaml Importer #------------------------------------------------------------------------------
[docs]class EnamlImporter(AbstractEnamlImporter): """ The standard Enaml importer which can import Enaml modules from standard locations on the python path and compile them appropriately to .enamlc files. This importer adopts the Python 3 conventions and scheme for creating the cached files and setting the __file__ attribute on the module. See this discussion thread for more info: http://www.mail-archive.com/python-dev@python.org/msg45203.html """ @classmethod
[docs] def locate_module(cls, fullname, path=None): """ Searches for the given Enaml module and returns an instance of this class on success. Paramters --------- fullname : string The fully qualified name of the module. path : list or None The subpackage __path__ for submodules and subpackages or None if a top-level module. Returns ------- results : Instance(AbstractEnamlImporter) or None If the Enaml module is located an instance of the importer that will perform the rest of the operations is returned. Otherwise, returns None. """ # We're looking inside a package and 'path' the package path if path is not None: modname = fullname.rsplit('.', 1)[-1] leaf = ''.join((modname, os.path.extsep, 'enaml')) for stem in path: enaml_path = os.path.join(stem, leaf) file_info = make_file_info(enaml_path) if (os.path.exists(file_info.src_path) or os.path.exists(file_info.cache_path)): return cls(file_info) # We're trying a load a package elif '.' in fullname: return # We're doing a direct import else: leaf = fullname + os.path.extsep + 'enaml' for stem in sys.path: enaml_path = os.path.join(stem, leaf) file_info = make_file_info(enaml_path) if (os.path.exists(file_info.src_path) or os.path.exists(file_info.cache_path)): return cls(file_info)
[docs] def __init__(self, file_info): """ Initialize an importer object. Parameters ---------- file_info : EnamlFileInfo An instance of EnamlFileInfo. """ self.file_info = file_info
def _load_cache(self, file_info): """ Loads and returns the code object for the given file info. Parameters ---------- file_info : EnamlFileInfo The file info object for the file. Returns ------- result : types.CodeType The code object for the file. """ with open(file_info.cache_path, 'rb') as cache_file: cache_file.read(8) code = marshal.load(cache_file) return code def _write_cache(self, code, ts, file_info): """ Write the cached file for then given info, creating the cache directory if needed. This call will suppress any IOError or OSError exceptions. Parameters ---------- code : types.CodeType The code object to write to the cache. ts : int The integer timestamp for the file. file_info : EnamlFileInfo The file info object for the file. """ try: if not os.path.exists(file_info.cache_dir): os.mkdir(file_info.cache_dir) with open(file_info.cache_path, 'w+b') as cache_file: cache_file.write(MAGIC) cache_file.write(struct.pack('i', ts)) marshal.dump(code, cache_file) except (OSError, IOError): pass def _get_magic_info(self, file_info): """ Loads and returns the magic info for the given path. Parameters ---------- file_info : EnamlFileInfo The file info object for the file. Returns ------- result : (magic, timestamp) The magic string and integer timestamp for the file. """ with open(file_info.cache_path, 'rb') as cache_file: magic = cache_file.read(4) timestamp = struct.unpack('i', cache_file.read(4))[0] return (magic, timestamp)
[docs] def get_code(self): """ Loads and returns the code object for the Enaml module and the full path to the module for use as the __file__ attribute of the module. Returns ------- result : (code, path) The Python code object for the .enaml module, and the full path to the module as a string. """ # If the .enaml file does not exists, just use the .enamlc file. # We can presume that the latter exists because it was already # checked by the loader. Should the situation ever arise that # it was deleted between then and now, an IOError is more # informative than an ImportError. file_info = self.file_info if not os.path.exists(file_info.src_path): code = self._load_cache(file_info) return (code, file_info.src_path) # Use the cached file if it exists and is current src_mod_time = int(os.path.getmtime(file_info.src_path)) if os.path.exists(file_info.cache_path): magic, ts = self._get_magic_info(file_info) if magic == MAGIC and src_mod_time <= ts: code = self._load_cache(file_info) return (code, file_info.src_path) # Otherwise, compile from source and attempt to cache with open(file_info.src_path) as src_file: src = src_file.read() ast = parse(src) code = EnamlCompiler.compile(ast, file_info.src_path) self._write_cache(code, src_mod_time, file_info) return (code, file_info.src_path) #------------------------------------------------------------------------------ # Enaml Imports Context #------------------------------------------------------------------------------
[docs]class imports(object): """ A context manager that hooks/unhooks the enaml meta path importer for the duration of the block. The helps user avoid unintended consequences of a having a meta path importer slow down all of their other imports. """ #: The framework-wide importers in use. We always have the default #: importer available, unless it is explicitly removed. __importers = [EnamlImporter] @classmethod
[docs] def get_importers(cls): """ Returns a tuple of currently active importers in use for the framework. """ return tuple(cls.__importers)
@classmethod
[docs] def add_importer(cls, importer): """ Add an importer to the list of importers for use with the framework. It must be a subclass of AbstractEnamlImporter. The most recently appended importer is used first. If the importer has already been added, this is a no-op. To move an importer up in precedence, remove it and add it again. """ if not issubclass(importer, AbstractEnamlImporter): msg = ('An Enaml importer must be a subclass of ' 'AbstractEnamlImporter. Got %s instead.') raise TypeError(msg % importer) importers = cls.__importers if importer not in importers: importers.append(importer)
@classmethod
[docs] def remove_importer(cls, importer): """ Removes the importer from the list of active importers. If the importer is not in the list, this is a no-op. """ importers = cls.__importers if importer in importers: importers.remove(importer)
[docs] def __init__(self): """ Initializes an Enaml import context. """ self.importers = self.get_importers()
[docs] def __enter__(self): """ Installs the current importer upon entering the context. """ # Install the importers reversed so that the newest ones # get first crack at the import on sys.meta_path. for importer in reversed(self.importers): importer.install()
[docs] def __exit__(self, *args, **kwargs): """ Uninstalls the current importer when leaving the context. """ # We removed in standard order since thats a more efficient # operation on sys.meta_path. for importer in self.importers: importer.uninstall()