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
Ответ 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