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

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

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, так и модифицированный.

5 2 голоса
Article Rating
Подписаться
Уведомить о
guest

22 Комментарий
Новые
Старые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Андрей
1 год назад

И снова здрасьте 🙂
Еще раз нырнул в этот код с новыми мозгами

Вопрос по этим строкам:

У нас создается переменная в globals(), только при первом запуске кода.
При запуске кода второй раз, когда модуль уже импортирован, в globals() уже ничего нет т.к. мы явно не указываем на это и в текущем контексте уже нет созданных переменных.

Можно явно указать создание переменной в globals() вот так:

Получается создание чего либо в globals() не имеет смысла.

Можно сделать обычную переменную для того чтобы использовать её для setattr:

Например вот так:

Далее заметка вот какая:
Если бы действительно в globals() создавалась переменная “modulesNames”, то нам бы не пришлось добавлять в каждый модуль параметр “modulesNames”, для дальнейшего вызова индивидуально для каждого модуля с одним и тем же словарем продублированным в каждом модуле.

Мы бы тогда могли обращаться к переменной modulesNames в globals() и всегда доставать ссылки на загруженные модули их полные и обычные имена и что угодно еще.

Мне стало интересно, что именно было необходимо достичь?

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

Андрей
1 год назад
Ответить на  Nikita

В данном случае globals() ведь просто используется как нестандартный способ создания переменной.

globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName)
И не отличается от создания обычной переменной

module = importlib.import_module(currentModuleFullName)
Мы ведь просто создаем переменную, только для того чтобы использовать её в setattr. И эта переменная существует только при первом запуске скрипта в Blender. При втором запуске её уже не будет.

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

Последний раз редактировалось 1 год назад Андрей ем
Андрей
1 год назад
Ответить на  Nikita

Если не сложно, посмотри/оцени мою версию.
Может тебе тоже станет интересно и появится новая статья 🙂
—–

modulesNames = [‘addCube’, ‘addCubePanel’]

import sys
import importlib

modulesFullNames = {}
for currentModuleName in modulesNames:
    if ‘DEBUG_MODE’ in sys.argv:
        modulesFullNames[currentModuleName] = (‘{}’.format(currentModuleName))
    else:
        modulesFullNames[currentModuleName] = (‘{}.{}’.format(__name__, currentModuleName))
del currentModuleName
del modulesNames

for currentModuleFullName in modulesFullNames.values():
    if currentModuleFullName in sys.modules:
        globals()[currentModuleFullName] = importlib.reload(sys.modules[currentModuleFullName])
    else:
        globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName)
del currentModuleFullName

# Импорты делать не обязательно,
# Это чтобы код не подчеркивал строки красным
import addCube
import addCubePanel

print(‘test addCube’, addCube.aaa())
print(‘test addCubePanel’, addCubePanel.vvv())
В файле addCube тестовая функция

def aaa():
    return 5
В файле addCubePanel

import addCube

def vvv():
    return 15

print(‘in vvv: ‘, addCube.aaa())
Blender выводит

in vvv:  5
test addCube 5
test addCubePanel 15
——
Мы получается создаем переменную с именем модуля сразу с ссылкой на модуль и при любом раскладе храним в глобальных переменных, просто в одном случае мы релоадим, в дргуом импортим.
И сделал удаление на ненужные переменные.

И получается сразу можем обращаться к модулю как к переменной.
Я в коде сделал импорты только потому что, что IDE ругается, так даже без них работает все.

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

Последний раз редактировалось 1 год назад Андрей ем
Андрей
1 год назад
Ответить на  Nikita

Оо ничёси!
А чё так можно было? 🙂

Андрей
1 год назад
Ответить на  Nikita

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

Sviatoslav Petrov
1 год назад

Несмотря на то, что у меня не получилось с помощью урока сделать init файл для сборки, тем не менее, скрипт для тестирования оказался очень полезным.
Спасибо автору за урок!

Sviatoslav Petrov
1 год назад

Структура тестового проекта стандартная
D:/testProject
/__init__.py
/add_cube.py (с функцией и панелью)

Sviatoslav Petrov
1 год назад

Я только начинаю разбираться как создавать скрипты, поэтому разработчики могут меня поправить.
Скрипт для дебага в Blender3.x у меня получилось использовать:

1. использовать как __init__ последний файл в статье:

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

2. запускать его скриптом описанном тут:

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

Для запаковки в zip, код описанный в статье – не сработал. Покопавшись в штатных дополнениях Blender, я определил, что работающая структура __init__.py файла для запаковки, такая:

Sviatoslav Petrov
1 год назад

Какие изменения произошли, в Blender 3+? Потому как если запускать дебаг скрипт, с указанием пути к __init__.py то дополнение работает корректно (я работаю со своим примером). Но когда пробую собрать в zip и установить, то вроде как дополнение ставится, но ничего не происходит. Нет ошибок, и панель не появляется в UI

Kulagin
Kulagin
3 лет назад

Не работает на 2.92 “out of the box”. Выбрасывает ошибку про “has category” в модуле addCubePanel:https://i.imgur.com/PSJFgL9.png

Чинится при помощи комментирования строки bl_category = “Add Cube” в addCubePanel.py.

NikitaD
Администратор
3 лет назад
Ответить на  Kulagin

Нет, для того, чтобы панель нормально отображалась в 2.92, нужно заменить строчку

bl_region_type = “TOOLS”

на

bl_region_type = “UI”

в этом файле

Алексей Маслов
Алексей Маслов
7 лет назад

Немного расшарил инит файл!
А что если добавить вот такую часть?
что бы автоматом искать новые модули в директории, ислкючая “__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()

Nikita
7 лет назад
Ответить на  Алексей Маслов

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

Алексей Маслов
Алексей Маслов
7 лет назад

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

Nikita
7 лет назад
Ответить на  Алексей Маслов

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