Creating your own importer in Python
ayrton
has always been able to use any Python module, package or extension as
long as it is in a directory in sys.path
, but trying to solve a bigger bug, I
realized that there was no way to use ayrton
modules or packages. Having only
laterally heard about the new importlib
module and the new mechanism, I sat down
and read more about it.
The best source (or at least the easiest to find) is possibly what Python's reference says about the import system, but I have to be honest: it was not an easy read. Next week I'll sit down and see if I can improve it a little. So, for those out there who, like me, might be having some troubles understanding the mechanism, here's how I understand the system works (ignoring deprecated APIs and corner cases or even relative imports; I haven't used or tried those yet):
def import_single(full_path, parent=None, module=None): # try this cache first if full_path in sys.modules: return sys.modules[full_path] # if not, try all the finders for finder in sys.meta_path: if parent is not None: spec = finder.find_spec(full_path, parent.__path__, target) else: spec = finder.find_spec(full_path, None, target) # if the finder 'finds' ('knows how to handle') the full_path # it will return a loader if spec is not None: loader = spec.loader if module is None and hasattr(loader, 'create_module'): module = loader.create_module(spec) if module is None: module = ModuleType(spec.name) # let's assume this creates an empty module object module.__spec__ = spec # add it to the cache before loading so it can referenced from it sys.modules[spec.name] = module try: # if the module was passed as parameter, # this repopulates the module's namespace # by executing the module's (possibly new) code loader.exec_module(module) except: # clean up del sys.modules[spec.name] raise return module raise ImportError def import (full_path, target=None): parent= None # this code iterates over ['foo', 'foo.bar', 'foo.bar.baz'] elems = full_path.split('.') for partial_path in [ '.'.join (elems[:i]) for i in range (len (elems)+1) ][1:] parent = import_single(partial_path, parent, target) # the module is loaded in parent return parent
A more complete version of the if spec is not None
branch can be found in
the Loading section
of the reference. Notice that the algorithm uses all the finders in sys.meta_path
.
So which are the default finders?
In [^9]: sys.meta_path Out[^9]: [_frozen_importlib.BuiltinImporter, _frozen_importlib.FrozenImporter, _frozen_importlib_external.PathFinder]
Of those finders, the latter one is the one that traverses sys.path
, and also has
a hook mechanism. I didn't use those, so for the moment I didn't untangle how they
work.
Finally, this is how I implemented importing ayrton
modules and packages:
from importlib.abc import MetaPathFinder, Loader from importlib.machinery import ModuleSpec import sys import os import os.path from ayrton.file_test import _a, _d from ayrton import Ayrton import ayrton.utils class AyrtonLoader (Loader): @classmethod def exec_module (klass, module): # «the loader should execute the module’s code # in the module’s global name space (module.__dict__).» load_path= module.__spec__.origin loader= Ayrton (g=module.__dict__) loader.run_file (load_path) # set the __path__ # TODO: read PEP 420 init_file_name= '__init__.ay' if load_path.endswith (init_file_name): # also remove the '/' module.__path__= [ load_path[:-len (init_file_name)-1] ] loader= AyrtonLoader () class AyrtonFinder (MetaPathFinder): @classmethod def find_spec (klass, full_name, paths=None, target=None): # TODO: read PEP 420 :) last_mile= full_name.split ('.')[-1] if paths is not None: python_path= paths # search only in the paths provided by the machinery else: python_path= sys.path for path in python_path: full_path= os.path.join (path, last_mile) init_full_path= os.path.join (full_path, '__init__.ay') module_full_path= full_path+'.ay' if _d (full_path) and _a (init_full_path): return ModuleSpec (full_name, loader, origin=init_full_path) else: if _a (module_full_path): return ModuleSpec (full_name, loader, origin=module_full_path) return None finder= AyrtonFinder () # I must insert it at the beginning so it goes before FileFinder sys.meta_path.insert (0, finder)
Notice all the references to PEP 420. I'm pretty sure I must be breaking something, but for the moment this works.