Python 3.5+: How to dynamically import a module given the full file path (in the presence of implicit sibling imports)?

纵饮孤独 提交于 2019-11-29 23:55:54
Jonathan von Schroeder

The easiest solution I could come up with is to temporarily modify sys.path in the function doing the import:

from contextlib import contextmanager

@contextmanager
def add_to_path(p):
    import sys
    old_path = sys.path
    sys.path = sys.path[:]
    sys.path.insert(0, p)
    try:
        yield
    finally:
        sys.path = old_path

def path_import(absolute_path):
   '''implementation taken from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly'''
   with add_to_path(os.path.dirname(absolute_path)):
       spec = importlib.util.spec_from_file_location(absolute_path, absolute_path)
       module = importlib.util.module_from_spec(spec)
       spec.loader.exec_module(module)
       return module

This should not cause any problems unless you do imports in another thread concurrently. Otherwise, since sys.path is restored to its previous state, there should be no unwanted side effects.

Edit:

I realize that my answer is somewhat unsatisfactory but, digging into the code reveals that, the line spec.loader.exec_module(module) basically results in exec(spec.loader.get_code(module.__name__),module.__dict__) getting called. Here spec.loader.get_code(module.__name__) is simply the code contained in lib.py.

Thus a better answer to the question would have to find a way to make the import statement behave differently by simply injecting one or more global variables through the second argument of the exec-statement. However, "whatever you do to make the import machinery look in that file's folder, it'll have to linger beyond the duration of the initial import, since functions from that file might perform further imports when you call them", as stated by @user2357112 in the question comments.

Unfortunately the only way to change the behavior of the import statement seems to be to change sys.path or in a package __path__. module.__dict__ already contains __path__ so that doesn't seem to work which leaves sys.path (Or trying to figure out why exec does not treat the code as a package even though it has __path__ and __package__ ... - But I don't know where to start - Maybe it has something to do with having no __init__.py file).

Furthermore this issue does not seem to be specific to importlib but rather a general problem with sibling imports.

Edit2: If you don't want the module to end up in sys.modules the following should work (Note that any modules added to sys.modules during the import are removed):

from contextlib import contextmanager

@contextmanager
def add_to_path(p):
    import sys
    old_path = sys.path
    old_modules = sys.modules
    sys.modules = old_modules.copy()
    sys.path = sys.path[:]
    sys.path.insert(0, p)
    try:
        yield
    finally:
        sys.path = old_path
        sys.modules = old_modules

add to the PYTHONPATH environment variable the path your application is on

Augment the default search path for module files. The format is the same as the shell’s PATH: one or more directory pathnames separated by os.pathsep (e.g. colons on Unix or semicolons on Windows). Non-existent directories are silently ignored.

on bash its like this:

export PYTHONPATH="./folder/:${PYTHONPATH}"

or run directly:

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py
  1. Make sure your root is in a folder that is explicitly searched in the PYTHONPATH
  2. Use an absolute import:

    from root.folder import implicit_sibling_import #called from app.py

The OP's idea is great, this work only for this example by adding sibling modules with proper name to the sys.modules, I would say it is the SAME as adding PYTHONPATH. tested and working with version 3.5.1.

import os
import sys
import importlib.util


class PathImport(object):

    def get_module_name(self, absolute_path):
        module_name = os.path.basename(absolute_path)
        module_name = module_name.replace('.py', '')
        return module_name

    def add_sibling_modules(self, sibling_dirname):
        for current, subdir, files in os.walk(sibling_dirname):
            for file_py in files:
                if not file_py.endswith('.py'):
                    continue
                if file_py == '__init__.py':
                    continue
                python_file = os.path.join(current, file_py)
                (module, spec) = self.path_import(python_file)
                sys.modules[spec.name] = module

    def path_import(self, absolute_path):
        module_name = self.get_module_name(absolute_path)
        spec = importlib.util.spec_from_file_location(module_name, absolute_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return (module, spec)

def main():
    pathImport = PathImport()
    root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
    isi_path = os.path.join(root, 'folder', 'implicit_sibling_import.py')
    sibling_dirname = os.path.dirname(isi_path)
    pathImport.add_sibling_modules(sibling_dirname)
    (lib, spec) = pathImport.path_import(isi_path)
    print (lib.hello())

if __name__ == '__main__':
    main()
Md Jahangir Alam

Try:

export PYTHONPATH="./folder/:${PYTHONPATH}"

or run directly:

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py

Make sure your root is in a folder that is explicitly searched in the PYTHONPATH. Use an absolute import:

from root.folder import implicit_sibling_import #called from app.py
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!