"""Create a controllable import. One should be able to do several things to control imports: * Whitelisting of modules based on name and type + built-ins + frozen + extensions * Block all modules based on type + .pyc * Allow all modules of a certain type + .py """ import importlib import _importlib import functools import imp import sys # Need to inject 'open' into importlib as it will most likely be removed from # the built-in namespace. _importlib.open = open class Whitelister(object): """Whitelist the finding and/or loading of modules.""" def __init__(self, whitelist, *args, **kwargs): """Store whitelist and continue on with instantiation.""" self._whitelist = frozenset(whitelist) super(Whitelister, self).__init__(*args, **kwargs) def check_name(self, name): """Check a module name against the whitelist, returning True if it passes or False otherwise.""" if name in self._whitelist: return True return False def find_module(self, name, path=None): """If the module name passes the whitelist, allow the finding of the modules to continue, otherwise return None.""" if self.check_name(name): return super(Whitelister, self).find_module(name, path) return None def load_module(self, name): """If the module names passes the whitelist, allow the loading of the module to continue, otherwise raise ImportError.""" if self.check_name(name): return super(Whitelister, self).load_module(name) raise ImportError("cannot import module") class WhitelistBuiltin(Whitelister, importlib.BuiltinImporter): """Whitelisting importer/loader for built-in modules.""" pass class WhitelistFrozen(Whitelister, importlib.FrozenImporter): """Whitelisting importer/loader for frozen modules.""" pass class WhitelistExtensionImporter(Whitelister, importlib.ExtensionFileImporter): """Whitelisting importer for extension modules.""" pass class PyOnlyFileLoader(importlib._PyFileLoader): """Only load source files; no bytecode.""" def __init__(self, *args, **kwargs): """Remove the bytecode path.""" super(PyOnlyFileLoader, self).__init__(*args, **kwargs) self._bytecode_path = lambda: None def write_bytecode(self, *args, **kwargs): """Do not write out any bytecode.""" return False class PyOnlyFileImporter(importlib.PyFileImporter): """Only find source files; no bytecode.""" _loader = PyOnlyFileLoader def __init__(self, path_entry): super(PyOnlyFileImporter, self).__init__(path_entry) self._suffixes = importlib.suffix_list(imp.PY_SOURCE) class ControlledImport(importlib.Import): """Represent a controllable version of import that allows for more fine-grained control over what can and cannot be imported.""" def __init__(self, safe_builtins=(), safe_frozen=(), safe_extensions=()): """Set up importation where built-in, frozen, and extension modules must be whitelisted and .pyc files are completely blocked while all. py files are allowed. Import data structures in sys are directly mutated when an instance is created! """ importlib.Import.__init__(self, None, ()) # Clear out any traces of the previous import machinery. sys.path_importer_cache.clear() sys.meta_path = [] sys.path_hooks = [] # Whitelist built-in and frozen modules on sys.meta_path. sys.meta_path.append(WhitelistBuiltin(safe_builtins)) sys.meta_path.append(WhitelistFrozen(safe_frozen)) # Whitelist extension modules on sys.path. ext_importer = functools.partial(WhitelistExtensionImporter, safe_extensions) # Allow all .py files but not .pyc files on sys.path. # XXX fs_factory = importlib.chaining_fs_path_hook(ext_importer, PyOnlyFileImporter) sys.path_hooks.append(fs_factory) def module_from_cache(self, name): """Override so that any module name starting with a dot raises ImportError.""" if name.startswith('.'): raise ImportError("module names starting with '.' cannot be " "imported") return importlib.Import.module_from_cache(self, name) def post_import(self, module): """Strip off the __loader__ attribute so that the object's global namespace is not exposed.""" try: del module.__loader__ except AttributeError: pass return module