Importing a file as module
I want to import a file (maybe generated at run-time, maybe even the
name only known at run-time) that does not exist in sys.path
. In a
couple of previous jobs that has happened with Python bindings
generated from protocol descriptions. In the past I've done that by
temporarily adding the parent to sys.path
then running
importlib.import_module
, while being annoyed there is no function
import_file(name: str, file_path: Path) -> ModuleType
. It turns out
that importlib.util has all the bits necessary, it's just a matter of
putting them together:
import importlib.util import os import sys from types import ModuleType def import_file(module_name: str, file_path: os.PathLike) -> ModuleType: spec = importlib.util.spec_from_file_location(module_name, file_path) # spec: ModuleSpec(name='bar', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f30b1d10310>, origin='/tmp/tmp.WpcbUEkm32/foo.py') # if spec_from_file_location fails, it returns None rather than raise an exception if spec is None: raise ModuleNotFoundError(f"Can't import {module_name} from {file_path}") if spec.loader is None: raise ModuleNotFoundError(f"Can't import {module_name} from {file_path}") module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) return module
I suppose it is optional to add the module to sys.modules
. If we do,
that will allow future import module_name
to find the same module
(which might even be useful for the purpose of injecting different
code into a 3rd party library…). If we don't, then we get a new copy
of the module every time we import it again. Eg. if
/tmp/tmp.WpcbUEkm32/foo.py
defined a global:
MODULE_GOBAL = 1 def myname(): return __name__
And I try to imort it twice under the same name, same path, the module globals are disconnected:
foo1 = import_file("foo", "/tmp/tmp.WpcbUEkm32/foo.py") foo2 = import_file("foo", "/tmp/tmp.WpcbUEkm32/foo.py") foo1.myname() # 'foo' foo2.myname() # 'foo' foo1.MODULE_GOBAL += 1 print(foo1.MODULE_GOBAL) # 2, as expected print(foo2.MODULE_GOBAL) # 1, unmodified