This is a rough analog to the import engine described in PEP 406. Here I've called it ImportState.The focus here is on using it as a context manager to limit changes to the import state to a block of code (in a with statement). Differences from PEP 406 are described below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | """import_state.py
A rough implementation of PEP 405. This module centers on manipulating
the normal Python import machinery through its defined state. Any other
approach, such as replacing builtins.__import__ is certainly legal, but
not supported here.
"""
__all__ = ['ImportState', 'default_import_state', 'globalstate']
import sys
import builtins
import site
import importlib
import _imp
from collections import namedtuple
class GlobalImportLock:
# no need for a generic ImportLock type, since all import states
# use the same lock
@property
def acquire(self):
_imp.acquire_lock()
@property
def release(self):
_imp.release_lock()
@property
def lock_held(self):
_imp.lock_held()
_ImportState = namedtuple('_ImportState', (
'modules',
'meta_path',
'path',
'path_hooks',
'path_importer_cache',
))
class ImportState(_ImportState):
"""A container for the import state (a la PEP 406).
The dictionary in sys.modules is a special case, since it is part
of the CPython interpreter state. Binding a different dict there
is problematic, since the import machinery may use the internal
reference to the original dict, rather than looking up sys.modules.
The consequence is that the _contents_ of sys.modules must be
swapped in and out, rather than simply binding something else there.
ImportState objects may be used as context managers, to activate the
state temporarily. During a with statement the dict in self.modules
may not reflect the actual state. However, it _will_ be correct
before and after the with statement.
"""
# all import states use the same lock
lock = GlobalImportLock()
def __init__(self, *args, **kwargs):
self._saved = None
def __enter__(self):
self.lock.acquire()
self.activate()
def __exit__(self, *args, **kwargs):
self.deactivate()
self.lock.release()
def copy(self):
"""Return a shallow copy of the import state."""
return type(self)(self.modules.copy(), self.meta_path[:],
self.path[:], self.path_hooks[:],
self.path_importer_cache.copy())
def activate(self, force=False):
"""Have the interpreter use this import state, saving the old."""
if self._saved is not None and not force:
raise TypeError("Already activated; try using a copy")
self._saved = _ImportState(
sys.modules.copy(), # saving away the contents
sys.meta_path,
sys.path,
sys.path_hooks,
sys.path_importer_cache,
)
#sys.modules = self.modules
sys.meta_path = self.meta_path
sys.path = self.path
sys.path_hooks = self.meta_path
sys.path_importer_cache = self.path_importer_cache
# accommodate sys.module's quirkiness
sys.modules.clear()
sys.modules.update(self.modules)
def deactivate(self):
"""Restore the import state saved when this one activated."""
if not self._saved:
raise TypeError("Not activated yet")
# sys.modules = self.modules
sys.meta_path = self._saved.meta_path
sys.path = self._saved.path
sys.path_hooks = self._saved.path_hooks
sys.path_importer_cache = self._saved.path_importer_cache
# accommodate sys.module's quirkiness
self.modules.clear()
self.modules.update(sys.modules)
sys.modules.clear()
sys.modules.update(self._saved.modules)
self._saved = None
def default_import_state(**overrides):
"""Return an ImportState with defaults to the initial import state."""
state = {
'modules': {},
'meta_path': [],
'path': site.getsitepackages(),
'path_hooks': [],
'path_importer_cache': {},
}
state.update(overrides)
return ImportState(**state)
class GlobalImportState(ImportState):
"""An ImportState that wraps the current state"""
# The underlying ImportState values will be ignored.
def __new__(cls):
return super(GlobalImportState, cls).__new__(cls, *([None]*5))
@property
def modules(self):
"""The cache of modules that have already been imported."""
return sys.modules
@property
def meta_path(self):
"""The PEP 302 finders queried before 'path' is traversed."""
return sys.meta_path
@property
def path(self):
"""The directories in which top-level packages are located."""
return sys.path
@property
def path_hooks(self):
"""The PEP 302 path importers that are queried for a path."""
return sys.path_hooks
@property
def path_importer_cache(self):
"""The cache of finders previously found through path_hooks."""
return sys.path_importer_cache
globalstate = GlobalImportState()
|
The import state defined here is not entirely true to PEP 406. One main thing the class is lacking is an import_module() method, which would be used any time to import a module relative to that import state. Also slightly different from PEP 406, ImportState.modules (sys.modules) is only a shallow copy, so the module objects themselves are shared between import states. In practice this should not be a big deal. We'll see how wrong I am. :)