Создание многофайлового аддона

При разработке сложных аддонов с большим количеством кода, хранить весь код в одном файле нецелесообразно. Когда в одном файле присутствуют несколько классов, наборов функций и данных, часто логически не связанных между собой, это затрудняет чтение, отладку, поиск нужных фрагментов кода, его повторное использование. Подобная компоновка кода считается очень плохим тоном программирования.

Blender на уровне Python поддерживает модульную систему компоновки кода, что позволяет разносить логические части аддона по отдельным файлам, после чего подключать их для использования там, где необходимо. Даже если вы ни разу не задумывались об использовании модулей, создавая скрипты или аддоны, вы с ними уже сталкивались — любой код, сохраненный в файл *.py, представляет из себя отдельный самостоятельный модуль. Просто ваш аддон состоял всего лишь из одного модуля. Сложные же аддоны могут состоять из нескольких десятков подключенных модулей.

Мультифайловый аддон
Мультифайловый аддон

Итак, модуль — это отдельный файл с расширением *.py, содержащий исполняемый код Python.

Для того, чтобы сформировать аддон из нескольких модулей, их объединяют в пакет. Пакет — это просто директория (папка), в которой собраны необходимые для работы аддона модули. Отличительным признаком пакета модулей является наличие в нем файла с именем __init__.py. Для того, чтобы такой аддон был правильно установлен в Blender, необходимо заархивировать в архив *.zip весь пакет (всю директорию целиком, а не только содержащиеся в ней файлы) и при установке аддона — Install from file — указать созданный архив.

 Рассмотрим пример создания простейшего многофайлового аддона:

  1. Создадим пакет:
    1. В любом удобном менеджере файлов создать директорию с нужным именем. Для примера — d:/Python/TestMultifile/
  2. Для написания кода удобнее использовать внешнюю IDE, но можно писать код и во встроенном редакторе Blender.
    1. Создать новый проект в PyCharm
    2. В качестве рабочей директории указать созданный пакет.
  3. Создадим первый модуль:
    1. Создать новый файл.
    2. Назвать его addCube.py

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

Не заостряясь на содержании, возьмем простейший код оператора для добавления в сцену дефолтного куба, куда же без него ).

Здесь создается пользовательский оператор, путем оборачивания в класс системного оператор bpy.ops.mesh.primitive_cube_add(), добавляющий куб в сцену.

Еще одно важное отличие от однофайлового аддона — указывать описание аддона в словаре bl_info в каждом отдельном модуле не требуется, только исполняемый код.

  1.  Создадим второй модуль:
    1. Создать новый файл.
    2. Назвать его addCubePanel.py

В этом модуле создадим интерфейс для вызова оператора из первого модуля. Здесь мы следуем концепции MVC (Model-View-Controller), которая предписывает разделять исполняемую часть кода (модель) и код, ответственный за создание интерфейса (представление).

Для пример создадим простейшую вкладку Add Cube в Т-панели с одной кнопкой для вызова созданного ранее оператора.

  1. Создадим индексный файл для связи модулей в пакете:
    1. Создать новый файл.
    2. Назвать его __init__.py

 В момент активации аддона, из всего пакета выполняется именно этот файл. Поэтому именно в нем следует разместить словарь bl_info с описанием аддона:

А так же именно здесь нужно сделать импорт всех остальных модулей.

Индексный файл __init__.py служит только для подключения и регистрации модулей и поэтому в нем не должно быть (хотя и допускается) никакого кода, относящегося к конкретному аддону. Этот файл должен быть как можно более универсален.

Занесем все имена модулей, которые должны подключаться в аддоне в список:

Имя модуля — это название файла, без расширения .py.

Это единственная изменяемая строка файла. Для того, чтобы создать индексный файл для любого другого аддона, можно использовать приведенный код, лишь изменив в списке имена модулей на нужные.

Если модули подключаются из пакета, в переменных среды Python они будут доступны по идентификатору: Имя_пакета.Имя_модуля. Дополним формат указанных имен модулей до полного вида:

Теперь созданный словарь modulesFullNames содержит полные имена подключаемых модулей.

Импортируем все модули из полученного словаря, использовав для этого утилиту importlib:

Все импортированные модули хранятся в sys.modules. Перезагрузка уже импортированного (importlib.reload) модуля требуется, если в окне подключения аддонов (User Preferences — Add-ons) пользователь нажимает клавишу обновления f8.

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

Так как importlib.import_module не создает глобальной переменной с именем подключаемого модуля, следует занести в globals такую переменную, указывающую на соответствующий модуль, чтобы в дальнейшем обращаться к модулю по имени.

Для того, чтобы в отдельных модулях можно было обращаться к другим модулям по имени, в каждом из подключаемых модулей создается переменная moduleNames, ссылающаяся на словарь modulesFullNames. Теперь из любого подключенного модуля можно получить доступ к другому подключенному модулю по его имени. Например для доступа из модуля addCubePanel к модулю addCube нужно выполнить следующую команду:

После того, как все необходимые модули подключены, необходимо зарегистрировать содержащиеся в них классы. Так как функция register при активации аддона автоматически выполняется только в индексном файле, внутри нее необходимо выполнить регистрацию для всех модулей аддона.

То же самое необходимо проделать и для разрегистрации классов.

 Полный код файла __init__.py имеет следующий вид:

Это чистовой (релизный) вид индексного файла __init__.py, который может быть использован для любого многофайлового аддона. Необходимо лишь указывать нужные имена модулей в списке modulesNames.

  1. Директория TestMultifile теперь содержит три файла:
    1. __init__.py
    2. addCube.py
    3. addCubePanel.py

Осталось запаковать ее (директорию целиком, а не только файлы) в архив *.zip и можно выполнить установку тестового мультифайлового аддона в Blender.

Тестовый многофайловый аддон
Тестовый многофайловый аддон

Бонус: отладочный запуск мультифайлового аддона из Blender

Если для разработки многофайлового аддона используется внешняя IDE, выполнить отладочный запуск скриптом из Blender не получится. При использовании стандартного скрипта, Blender не поймет, что вызывается индексный файл пакета, а не отдельный исполняемый модуль. Для того, чтобы отлаживать многофайловый аддон в вызывающий скрипт необходимо внести изменения.

В первую очередь нужно указать место расположения подключаемых модулей — путь к директории d:/Python/TestMultifile/. Поиск импортируемых модулей Python осуществляет в перечне директорий, указанных в sys.path. Для того, чтобы модули были найдены интерпретатором в момент их подключения, нужно указать директорию с их расположением и добавить ее в sys.path. Имя запускаемого файла для удобства сохраним в отдельную переменную.

Отладочный запуск аддона отличается от запуска аддона, уже установленного в Blender. Для того, чтобы внутри модуля __init__.py понимать, какой именно тип запуска требуется, при отладочном запуске добавим в перечень входных параметров sys.argv дополнительный параметр «DEBUG_MODE».

Удалим этот параметр после выполнения скрипта.

Полный код скрипта, запускающего многофайловый аддон из окна Text Editor в Blender теперь имеет следующий вид:

Код скрипта универсален, для разработки других многофайловых аддонов нужно только изменять путь filesDir к месторасположению модулей аддона.

При запуске из окна Text Editor по кнопке Run Script исполняемый файл __init__.py считается отдельным модулем, а не индексным файлом пакета. Поэтому и все подключаемые внутри него модули должны импортироваться не как части пакета, а как такие же отдельные модули. Следовательно полные имена подключаемых модулей будут состоять лишь из имен самих модулей, без приставки с именем пакета.

Используя добавленный в вызывающем скрипте параметр «DEBUG_MODE», внесем в файл __init__.py условие для отладочного запуска аддона, позволяющее правильно импортировать подключаемые модули. Для этого нужно всего лишь правильно формировать полные имена в словаре modulesFullNames:

Полный код файла __init__.py, работающего и в отладочном и в релизном режимах:

Для публикации релиза аддона можно использовать как приведенный выше чистовой файл __init__.py, так и модифицированный.

  • Алексей Маслов

    Немного расшарил инит файл!
    А что если добавить вот такую часть?
    что бы автоматом искать новые модули в директории, ислкючая «__init__.py», и добавлять их в список «modulesNames»
    modulesNames = []
    dir_path = os.path.dirname(os.path.realpath(__file__)) # адрес модуля «__init__.py»
    def serchPy():
    ____import os
    ____for root, dirs, files in os.walk(dir_path):
    ________for name in files:
    ____________if name != ‘__init__.py’:
    ________________name=name.replace(‘.py’, ») # удаляем расширение ‘.py’
    ________________global modulesNames
    ________________modulesNames.append(name) # добавляем найденный модуль в список
    ____________else:
    ________________continue
    serchPy()

    • Хорошая идея!
      Я подобное не делю т.к. у меня бывает в директории еще дополнительные файлы, которые не надо включать в текущий проект.
      Но для общего случая — очень удобно, не надо прописывать подключаемые модули руками.

  • Алексей Маслов

    Спасибо!
    Как всегда очень актуально, доступно и полезно!

    • Рад, что оказалось полезно.