2017-01-25 1 views
27

Вопрос

Стандартная библиотека четко документы how to import source files directly (учитывая абсолютный путь к файлу исходного файла), но этот подход не работает, если что исходный файл использует неявные импорт одноуровневые, как описано в примере ниже.Python 3.5+: Как динамически импортировать модуль с учетом полного пути к файлу (при наличии имплицитного импорта собора)?

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

Я уже проверил this и this other StackOverflow вопросы по этой теме, но они не затрагивают импортирует неявное Sibling в файл импортируется вручную.

Установка/Пример

Вот показательный пример

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

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, как задумано, но я хотел бы избежать munging sys.path, если это возможно.

требования Ответ

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

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

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

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


PYTHONPATH

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

-m

-m flag является рекомендуемым/вещий подход, но стандартная библиотека явно документы How to import source files directly. Я хотел бы знать, как я могу адаптировать этот подход, чтобы справиться с неявным относительным импортом. Очевидно, что внутренние компоненты Python должны это делать, и как внутренние элементы отличаются от документации «импортировать исходные файлы напрямую»?

+0

Что касается Python, этот «имплицитный импорт брака» является обычным абсолютным импортом и, безусловно, не является имплицитным относительным импортом. Имплицированный относительный импорт больше не поддерживается в Python 3. – user2357112

+1

Изменение 'sys.path', вероятно, лучший выбор. Что бы вы ни делали, чтобы механизм импорта выглядел в папке этого файла, он должен задерживаться дольше первоначального импорта, поскольку функции из этого файла могут выполнять дальнейшие импортные операции при их вызове. – user2357112

+0

@ user2357112 Действительно [PEP 8] (https://www.python.org/dev/peps/pep-0008/#imports) говорит, что относительный неявный импорт отключен на Python 3. Но мне остается интересно: если выше пример не относительный неявный импорт, то что? У вас есть пример? –

ответ

10

Самым простым решением я мог придумать, чтобы временно изменить 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. Тем не менее, «что бы вы ни делали, чтобы механизм импорта выглядел в папке этого файла, он должен задерживаться после продолжительности первоначального импорта, поскольку функции из этого файла могут выполнять дальнейший импорт, когда вы их называете», как указано в @ пользователь2357112 в вопросе комментарии.

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

Кроме того, этот вопрос, по-видимому, не имеет отношения к importlib, а скорее к общей проблеме с sibling imports.

Edit2: Если вы не хотите, чтобы модуль в конечном итоге в 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 
+0

Я думаю, что sys.modules также пострадает при запуске этого ... но не уверен, как не иметь этого побочного эффекта. – Har

+0

Это кажется правдой. Однако для меня неочевидно, почему это было бы нежелательно/проблематично? - Модуль загружается в конце концов. –

+0

Возможно, они могли бы заменить модуль в текущем пространстве имен, предоставив модуль с тем же именем – Har

6

добавить переменную в PYTHONPATH среде путь приложение находится на

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

на Баш его, как это:

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

или запустить непосредственно:

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

Я обновил свой ответ с более точными требованиями к решению. К сожалению, поскольку я хочу, чтобы решение было чистым python, это не сработает. –

+0

Могу ли я спросить вас, что является первоначальным требованием? И, в частности, какой аспект делает это так, что вы можете решить его только с помощью чистого решения python. – user1712447

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

    из root.folder импорта implicit_sibling_import #called из app.py

+0

Как указано в вопросе, я ищу повторяющееся (без необходимости переконфигурировать каждый раз), чистое решение python, так что это не сработает. –

+0

@Pedro Не повторяет, что конфигурация предпочтительнее предлагаемого вами решения (что явно выглядит очень не-питоновым)? – user1712447

1

Идея 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() 
1

Try:

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

или запустить непосредственно:

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

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

from root.folder import implicit_sibling_import #called from app.py 
Смежные вопросы