importer.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. """This module implements a post import hook mechanism styled after what is
  2. described in PEP-369. Note that it doesn't cope with modules being reloaded.
  3. """
  4. import sys
  5. import threading
  6. PY2 = sys.version_info[0] == 2
  7. if PY2:
  8. string_types = basestring,
  9. find_spec = None
  10. else:
  11. string_types = str,
  12. from importlib.util import find_spec
  13. from .decorators import synchronized
  14. # The dictionary registering any post import hooks to be triggered once
  15. # the target module has been imported. Once a module has been imported
  16. # and the hooks fired, the list of hooks recorded against the target
  17. # module will be truncated but the list left in the dictionary. This
  18. # acts as a flag to indicate that the module had already been imported.
  19. _post_import_hooks = {}
  20. _post_import_hooks_init = False
  21. _post_import_hooks_lock = threading.RLock()
  22. # Register a new post import hook for the target module name. This
  23. # differs from the PEP-369 implementation in that it also allows the
  24. # hook function to be specified as a string consisting of the name of
  25. # the callback in the form 'module:function'. This will result in a
  26. # proxy callback being registered which will defer loading of the
  27. # specified module containing the callback function until required.
  28. def _create_import_hook_from_string(name):
  29. def import_hook(module):
  30. module_name, function = name.split(':')
  31. attrs = function.split('.')
  32. __import__(module_name)
  33. callback = sys.modules[module_name]
  34. for attr in attrs:
  35. callback = getattr(callback, attr)
  36. return callback(module)
  37. return import_hook
  38. @synchronized(_post_import_hooks_lock)
  39. def register_post_import_hook(hook, name):
  40. # Create a deferred import hook if hook is a string name rather than
  41. # a callable function.
  42. if isinstance(hook, string_types):
  43. hook = _create_import_hook_from_string(hook)
  44. # Automatically install the import hook finder if it has not already
  45. # been installed.
  46. global _post_import_hooks_init
  47. if not _post_import_hooks_init:
  48. _post_import_hooks_init = True
  49. sys.meta_path.insert(0, ImportHookFinder())
  50. # Determine if any prior registration of a post import hook for
  51. # the target modules has occurred and act appropriately.
  52. hooks = _post_import_hooks.get(name, None)
  53. if hooks is None:
  54. # No prior registration of post import hooks for the target
  55. # module. We need to check whether the module has already been
  56. # imported. If it has we fire the hook immediately and add an
  57. # empty list to the registry to indicate that the module has
  58. # already been imported and hooks have fired. Otherwise add
  59. # the post import hook to the registry.
  60. module = sys.modules.get(name, None)
  61. if module is not None:
  62. _post_import_hooks[name] = []
  63. hook(module)
  64. else:
  65. _post_import_hooks[name] = [hook]
  66. elif hooks == []:
  67. # A prior registration of port import hooks for the target
  68. # module was done and the hooks already fired. Fire the hook
  69. # immediately.
  70. module = sys.modules[name]
  71. hook(module)
  72. else:
  73. # A prior registration of port import hooks for the target
  74. # module was done but the module has not yet been imported.
  75. _post_import_hooks[name].append(hook)
  76. # Register post import hooks defined as package entry points.
  77. def _create_import_hook_from_entrypoint(entrypoint):
  78. def import_hook(module):
  79. __import__(entrypoint.module_name)
  80. callback = sys.modules[entrypoint.module_name]
  81. for attr in entrypoint.attrs:
  82. callback = getattr(callback, attr)
  83. return callback(module)
  84. return import_hook
  85. def discover_post_import_hooks(group):
  86. try:
  87. import pkg_resources
  88. except ImportError:
  89. return
  90. for entrypoint in pkg_resources.iter_entry_points(group=group):
  91. callback = _create_import_hook_from_entrypoint(entrypoint)
  92. register_post_import_hook(callback, entrypoint.name)
  93. # Indicate that a module has been loaded. Any post import hooks which
  94. # were registered against the target module will be invoked. If an
  95. # exception is raised in any of the post import hooks, that will cause
  96. # the import of the target module to fail.
  97. @synchronized(_post_import_hooks_lock)
  98. def notify_module_loaded(module):
  99. name = getattr(module, '__name__', None)
  100. hooks = _post_import_hooks.get(name, None)
  101. if hooks:
  102. _post_import_hooks[name] = []
  103. for hook in hooks:
  104. hook(module)
  105. # A custom module import finder. This intercepts attempts to import
  106. # modules and watches out for attempts to import target modules of
  107. # interest. When a module of interest is imported, then any post import
  108. # hooks which are registered will be invoked.
  109. class _ImportHookLoader:
  110. def load_module(self, fullname):
  111. module = sys.modules[fullname]
  112. notify_module_loaded(module)
  113. return module
  114. class _ImportHookChainedLoader:
  115. def __init__(self, loader):
  116. self.loader = loader
  117. if hasattr(loader, "load_module"):
  118. self.load_module = self._load_module
  119. if hasattr(loader, "create_module"):
  120. self.create_module = self._create_module
  121. if hasattr(loader, "exec_module"):
  122. self.exec_module = self._exec_module
  123. def _load_module(self, fullname):
  124. module = self.loader.load_module(fullname)
  125. notify_module_loaded(module)
  126. return module
  127. # Python 3.4 introduced create_module() and exec_module() instead of
  128. # load_module() alone. Splitting the two steps.
  129. def _create_module(self, spec):
  130. return self.loader.create_module(spec)
  131. def _exec_module(self, module):
  132. self.loader.exec_module(module)
  133. notify_module_loaded(module)
  134. class ImportHookFinder:
  135. def __init__(self):
  136. self.in_progress = {}
  137. @synchronized(_post_import_hooks_lock)
  138. def find_module(self, fullname, path=None):
  139. # If the module being imported is not one we have registered
  140. # post import hooks for, we can return immediately. We will
  141. # take no further part in the importing of this module.
  142. if not fullname in _post_import_hooks:
  143. return None
  144. # When we are interested in a specific module, we will call back
  145. # into the import system a second time to defer to the import
  146. # finder that is supposed to handle the importing of the module.
  147. # We set an in progress flag for the target module so that on
  148. # the second time through we don't trigger another call back
  149. # into the import system and cause a infinite loop.
  150. if fullname in self.in_progress:
  151. return None
  152. self.in_progress[fullname] = True
  153. # Now call back into the import system again.
  154. try:
  155. if not find_spec:
  156. # For Python 2 we don't have much choice but to
  157. # call back in to __import__(). This will
  158. # actually cause the module to be imported. If no
  159. # module could be found then ImportError will be
  160. # raised. Otherwise we return a loader which
  161. # returns the already loaded module and invokes
  162. # the post import hooks.
  163. __import__(fullname)
  164. return _ImportHookLoader()
  165. else:
  166. # For Python 3 we need to use find_spec().loader
  167. # from the importlib.util module. It doesn't actually
  168. # import the target module and only finds the
  169. # loader. If a loader is found, we need to return
  170. # our own loader which will then in turn call the
  171. # real loader to import the module and invoke the
  172. # post import hooks.
  173. loader = getattr(find_spec(fullname), "loader", None)
  174. if loader and not isinstance(loader, _ImportHookChainedLoader):
  175. return _ImportHookChainedLoader(loader)
  176. finally:
  177. del self.in_progress[fullname]
  178. def find_spec(self, fullname, path=None, target=None):
  179. # Since Python 3.4, you are meant to implement find_spec() method
  180. # instead of find_module() and since Python 3.10 you get deprecation
  181. # warnings if you don't define find_spec().
  182. # If the module being imported is not one we have registered
  183. # post import hooks for, we can return immediately. We will
  184. # take no further part in the importing of this module.
  185. if not fullname in _post_import_hooks:
  186. return None
  187. # When we are interested in a specific module, we will call back
  188. # into the import system a second time to defer to the import
  189. # finder that is supposed to handle the importing of the module.
  190. # We set an in progress flag for the target module so that on
  191. # the second time through we don't trigger another call back
  192. # into the import system and cause a infinite loop.
  193. if fullname in self.in_progress:
  194. return None
  195. self.in_progress[fullname] = True
  196. # Now call back into the import system again.
  197. try:
  198. # This should only be Python 3 so find_spec() should always
  199. # exist so don't need to check.
  200. spec = find_spec(fullname)
  201. loader = getattr(spec, "loader", None)
  202. if loader and not isinstance(loader, _ImportHookChainedLoader):
  203. spec.loader = _ImportHookChainedLoader(loader)
  204. return spec
  205. finally:
  206. del self.in_progress[fullname]
  207. # Decorator for marking that a function should be called as a post
  208. # import hook when the target module is imported.
  209. def when_imported(name):
  210. def register(hook):
  211. register_post_import_hook(hook, name)
  212. return hook
  213. return register