import sys import threading if sys.version_info.major < 3: import __builtin__ as builtins else: import builtins # The R callback to be run. Initialized in the 'initialize()' method. _callback = None # A list of Python packages which have been imported. The aforementioned # callback will be run on the main thread after a module has been imported # on the main thread. _imported_packages = [] # A simple counter, tracking the recursion depth. This is used as we only # attempt to run the R callback at the top level; that is, we don't want # to run it while modules are being loaded recursively. _recursion_depth = 0 # The builtin implementation of '__import__'; saved so that we can re-use it # after initialization. __import__ = builtins.__import__ # The implementation of '_find_and_load' captured from 'importlib._bootstrap'. # Since we're trying to poke at Python internals, we try to wrap this code # in try-catch and only use this if it appears safe to do so. _find_and_load = None try: import importlib._bootstrap _find_and_load = importlib._bootstrap._find_and_load except: pass # Run hooks on imported packages, if safe to do so. def _maybe_run_hooks(): # Don't run hooks while loading packages recursively. global _recursion_depth if _recursion_depth != 0: return False # Check whether we're on the main thread. Note that separate threads can # attempt to load Python modules, but the R callback we register can only # be safely run on the main thread. is_main_thread = isinstance(threading.current_thread(), threading._MainThread) if not is_main_thread: return False # Pre-flight checks passed; run the callbacks. global _imported_packages global _callback for package in _imported_packages: _callback(package) # Clear the import list. del _imported_packages[:] # Resolve a module name on import. See Python code here for motivation. # https://github.com/python/cpython/blob/c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd/Lib/importlib/_bootstrap.py#L1246-L1270 def _resolve_module_name(name, globals=None, level=0): if level == 0: return name package = globals.get("__package__") if package is not None: return package spec = globals.get("__spec__") if spec is not None: return spec.parent return name # Helper function for running an import hook with our extra scaffolding. def _run_hook(name, hook): # Check whether this module has already been imported. already_imported = name in sys.modules # Bump the recursion depth. global _recursion_depth _recursion_depth += 1 # Run the hook. try: module = hook() except: raise finally: _recursion_depth -= 1 # Add this package to the import list, if this is the first # time importing that package. global _imported_packages if not already_imported: _imported_packages.append(name) # try and run hooks if possible _maybe_run_hooks() # return loaded module return module # The hook installed to replace 'importlib._bootstrap._find_and_load'. def _find_and_load_hook(name, import_): def _hook(): global _find_and_load return _find_and_load(name, import_) return _run_hook(name, _hook) # Initialize the '_find_and_load' replacement hook. def _initialize_importlib(): import importlib._bootstrap importlib._bootstrap._find_and_load = _find_and_load_hook # The hook installed to replace '__import__'. def _import_hook(name, globals=None, locals=None, fromlist=(), level=0): # resolve module name resolved_module_name = _resolve_module_name(name, globals, level) def _hook(): global __import__ return __import__(name, globals=globals, locals=locals, fromlist=fromlist, level=level) return _run_hook(_hook) # Initialize the '__import__' hook. def _initialize_default(): builtins.__import__ = _import_hook # The main entrypoint for this module. def initialize(callback): # Save the callback. global _callback _callback = callback # Check whether we can initialie with importlib. global _find_and_load if _find_and_load is not None: return _initialize_importlib() # Otherwise, fall back to default implementation. return _initialize_default()