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

Auteur : Frédéric Perrin

Date : vendredi 27 septembre 2024

Sauf mention contraire, les textes de ce site sont sous licence Creative Common BY-SA.

Ce site est produit avec des logiciels libres 100% élevés au grain et en plein air.