Python 3.5+: как динамически импортировать модуль с учетом полного пути к файлу (при наличии имплицитного импорта соборов)?

Вопрос

Стандартная библиотека явно документирует как напрямую импортировать исходные файлы (учитывая абсолютный путь к исходному файлу), но этот подход делает не работают, если этот исходный файл использует неявный импорт брата, как описано в примере ниже.

Как этот пример можно адаптировать для работы при наличии имплицитных импортных товаров?

Я уже проверил этот и этот другой Вопрос о Stackoverflow по этой теме, но они не адресуют неявный импорт брака в файл, импортируемый вручную.

Настройка/Пример

Здесь иллюстративный пример

Структура каталогов:

root/
  - directory/
    - app.py
  - folder/
    - implicit_sibling_import.py
    - lib.py

app.py:

import os
import importlib.util

# construct absolute paths
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')

def path_import(absolute_path):
   '''implementation taken from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly'''
   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

isi = path_import(isi_path)
print(isi.hello_wrapper())

lib.py:

def hello():
    return 'world'

implicit_sibling_import.py:

import lib # this is the implicit sibling import. grabs root/folder/lib.py

def hello_wrapper():
    return "ISI says: " + lib.hello()

#if __name__ == '__main__':
#    print(hello_wrapper())

Запуск python folder/implicit_sibling_import.py с блоком if __name__ == '__main__': закомментировал выходы ISI says: world в Python 3.6.

Но запуск python directory/app.py дает:

Traceback (most recent call last):
  File "directory/app.py", line 10, in <module>
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 205, in _call_with_frames_removed
  File "/Users/pedro/test/folder/implicit_sibling_import.py", line 1, in <module>
    import lib
ModuleNotFoundError: No module named 'lib'

Обход

Если я добавлю import sys; sys.path.insert(0, os.path.dirname(isi_path)) в app.py, python app.py дает world, как и предполагалось, но я хотел бы избежать по возможности перетащить sys.path.

Требования к ответам

Мне бы хотелось python app.py распечатать ISI says: world, и я хотел бы сделать это, изменив функцию path_import.

Я не уверен в последствиях mangling sys.path. Например. если был directory/requests.py, и я добавил путь к directory в sys.path, я бы не хотел, чтобы import requests начал импортировать directory/requests.py вместо импорта запрашивает библиотеку, которую я установил с помощью pip install requests.

Решение ДОЛЖНО реализовано как функция python, которая принимает абсолютный путь к нужному модулю и возвращает объект модуля .

В идеале решение не должно вводить побочные эффекты (например, если он изменяет sys.path, он должен вернуть sys.path в исходное состояние). Если решение действительно вводит побочные эффекты, оно должно объяснить, почему решение не может быть достигнуто без введения побочных эффектов.


PYTHONPATH

Если у меня есть несколько проектов, которые делают это, я не хочу забывать устанавливать PYTHONPATH каждый раз, когда я переключаюсь между ними. Пользователь должен просто иметь возможность pip install моего проекта и запускать его без каких-либо дополнительных настроек.

-m

-m флаг является рекомендуемым/питоновым подходом, но стандартная библиотека также явно документирует Как напрямую импортировать исходные файлы. Я хотел бы знать, как я могу адаптировать этот подход, чтобы справиться с неявным относительным импортом. Очевидно, что внутренние компоненты Python должны это делать, поэтому как внутренние компоненты отличаются от документации "импортировать исходные файлы напрямую"?

Ответ 1

Самое простое решение, которое я мог бы придумать, - временно изменить sys.path в функции, выполняющей импорт:

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

Это не должно вызывать никаких проблем, если вы одновременно импортируете другой поток. В противном случае, поскольку sys.path восстанавливается в прежнее состояние, не должно быть нежелательных побочных эффектов.

Edit:

Я понимаю, что мой ответ несколько неудовлетворительный, но, копаясь в коде, видно, что строка spec.loader.exec_module(module) в основном приводит к вызову exec(spec.loader.get_code(module.__name__),module.__dict__). Здесь spec.loader.get_code(module.__name__) - это просто код, содержащийся в lib.py.

Таким образом, лучший ответ на вопрос должен был бы найти способ заставить оператор import вести себя по-другому, просто введя одну или несколько глобальных переменных через второй аргумент exec-statement. Тем не менее, "что бы вы ни делали, чтобы механизм импорта выглядел в этой папке с файлами, он должен задерживаться после продолжительности первоначального импорта, поскольку функции из этого файла могут выполнять дальнейший импорт, когда вы их называете", как указано в @user2357112 в вопросе комментариев.

К сожалению, единственный способ изменить поведение оператора import, по-видимому, заключается в изменении sys.path или в пакете __path__. module.__dict__ уже содержит __path__, поэтому он не работает, и он оставляет sys.path (или пытается выяснить, почему exec не обрабатывает код как пакет, хотя он имеет __path__ и __package__.. - Но я не знаю, с чего начать. Возможно, это связано с отсутствием файла __init__.py).

Кроме того, эта проблема, по-видимому, не является специфичной для importlib, а скорее общей проблемой с импортом сестры.

Редактировать 2: Если вы не хотите, чтобы модуль заканчивался в sys.modules, следует работать следующее (обратите внимание, что все модули, добавленные в sys.modules во время импорта, удаляются):

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

Ответ 2

добавьте в переменную среды PYTHONPATH путь, по которому ваше приложение включено

Дополнить путь поиска по умолчанию для файлов модулей. Формат такой же, как и оболочки PATH: один или несколько путей к каталогу разделенные os.pathsep(например, двоеточия в Unix или точки с запятой на Windows). Необязательные каталоги молча игнорируются.

на bash выглядит следующим образом:

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

или запустить напрямую:

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

Ответ 3

  • Убедитесь, что ваш корень находится в папке, которая явно просматривается в PYTHONPATH
  • Используйте абсолютный импорт:

    из файла root.folder import implicit_sibling_import # called из app.py

Ответ 4

Идея OP велика, это работает только для этого примера, добавляя модули sibling с собственным именем в sys.modules, я бы сказал, что это ТОЛЬКО как добавление PYTHONPATH. протестирован и работает с версией 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()

Ответ 5

Try:

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

или запустить напрямую:

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

Убедитесь, что ваш корень находится в папке, которая явно просматривается в PYTHONPATH. Используйте абсолютный импорт:

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