В этой статье речь пойдет о плагинах — программных модулях, которые можно легко установить в основное приложение для расширения его функционала. Точнее, не о самих плагинах, а о том, как реализовать в своей программе систему взаимодействия "Приложение — Плагин".
В сети можно легко найти достаточно сложные и, порой, запутанные алгоритмы интеграции в ваш программный код подобной системы, но, поскольку мы будем использовать язык программирования Python, для нас все будет просто и предельно ясно.
Однако перед тем как показать читателю все инструменты своей плагинной системы — своего рода перочинный ножик, который по моему мнению, должен быть в каждом уважающем себя кармане, пардон, программе — немного предыстории...
Однажды для своего проекта мне потребовалась одна единственная, ну очень важная функция, которую на этапе разработки я, к сожалению, не предусмотрел. Проект был большой и довольно старый, поэтому мне пришлось с неделю повозиться, разыскивая в его коде нужные классы и процедуры, чтобы повесить в меню программы заветную кнопочку, реализующую недостающий функционал. Да и, признаться, мало радости в переписывании пусть и не всего, но какой-то части приложения.
После этого я твердо решил пересмотреть архитектуру своих проектов и, дабы не наступать на одни и те же грабли, поставил для себя задачу: расширение возможностей моих программ должно производиться в один клик! Сказанно — сделанно.
Итак, достаем из шкафа новенький скелет будущего приложения, который выглядит вот так:
И пересчитаем его косточки:
Папка Libs предназначена для библиотек и модулей будущего проекта. На данный момент в ней находятся модули для инициализации и подключения плагинов:
loadplugin.py — загружает плагины программы из папки Plugins (будет создана автоматически) корневой директории проекта;
manifest.py — класс, описывающий манифест загружаемого плагина;
В корневой директории проекта:
program.py — основной рограммный код приложения, находящийся в классе Program;
main.py — запускает программный код program.py и подключает плагины, если таковые имееются;
Далее я опишу только ключевые моменты реализации плагинной системы, опуская создание простейшего интерфейса приложения для демонстации работы плагина. Небольшой архив с примером вы сможете скачать по ссылке в конце статьи.
main.py:
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
import traceback
__version__ = "0.0.1"
def main():
try:
from Libs.loadplugin import loadplugin # функция загрузки плагинов
from program import Program # импортируем основной класс программы
app = Program()
loadplugin(app) # загружаем плагины
app.run() # запуск приложения
except Exception as exc:
print traceback.format_exc()
traceback.print_exc(file=open("error.log", "w"))
# Вывод окна с текстом ошибки.
if __name__ "__main__":
main()
Алгоритм модуля main.py:
- Создаем экземпляр app класса Program и передаем его в функцию загрузки плагинов (данный экземпляр будет доступен всем подключаемым плагинам, предоставляя им доступ к переменным и функциям, реализованным в основном программном коде класса Program).
- Загружаем плагины.
- Запускаем приложение.
- Выводим окно с текстом ошибки, если таковая возникла при запуске программы.
loadplugin.py:
# -*- coding: utf-8 -*-
import os
import traceback
def loadplugin(app):
"""Загружает плагины.
:type app: <class 'program.Program'>;
:param app: экземпляр класса Program;
"""
# Директория плагинов.
plugins_path = "{}/Plugins".format(os.path.split(os.path.abspath(sys.argv[0]))[0])
# Список разрешенных к подключению плагинов.
plugin_list = eval(open("{}/plugins_list.list".format(plugins_path)).read())
for name in os.listdir(plugins_path):
if name.startswith("__init__."):
continue
path = os.path.join(plugins_path, name)
if not os.path.isdir(path):
continue
try:
if name in plugin_list:
execfile(os.path.join(path, '__init__.py'),
{"app": app, "path": path})
except Exception:
raise Exception(traceback.format_exc())
Алгоритм модуля loadplugin.py:
- Сканируем директорию (пакет) Plugins на присутствие пакетов плагинов (любой python-пакет в папке Plugins будет считаться плагином).
- Инициализируем список плагинов из файла plugins_list.list, который находится в директории Plugins.
- Если найденный плагин упомянут в данном списке, подключаем его, в противном случае ищем следующий плагин.
Для большей наглядности я убрал из модуля loadplugin.py верефикацию плагинов и различные проверки на отсутствие директории Plugins, файла plugins_list.list и пр.
Как вы заметили, плагин исполняется функцией execfile:
execfile(os.path.join(path, '__init__.py'),
{"app": app, "path": path})
которая выполняет код файла init.py пакета плагина и передает в глобальное пространство имен init.py экземпляр app главного класса приложения.
Далее, через app, плагин получает доступ ко всем функциям и переменным программного кода приложения. Довольно просто.
Теперь давайте перейдем от слов к делу и запустим тестовый пример (main.py) из папки TestPlugin.
Здесь стоит сказать, что пишу я, используя в основном фреймворк Kivy, так что для запуска тестовых примеров предполагается, что все необходимые библиотеки установлены на вашей рабочей станции.
Итак, запустив пример, увидим простенький интерфейс с actionbar внизу экрана. Выберем кнопку в нижнем правом углу, открыв выпадающий список из двух пунктов, нажимаем пункт "Плагины" и читаем сообщение "Нет установленных плагинов".
Действительно, заглянув в папку проекта, обнаружим новую директорию Plugins.
Кроме уже известных нам файлов, директория для плагинов пуста. Ну что ж! Самое время их установить. Давайте расширим инструментал нашего перочинного ножика с помощью плагина и добавим в actionbar новую кнопку с логотипом Хабра.
В архиве с тестовым примером, помимо проекта TestPlugins, имеется папка Plugins. Откройте ее и скопируйте плагин HabraButton в проект в директорию Plugins.
Теперь снова запускаем наше тестовое приложение и выбираем пункт "Плагины".
Ура! Наш перочинный ножик только что обзавелся новеньким инструментом, как и было обещанно, в один клик.
Пункт с именем свежего плагина горит желтым цветом, это значит, что приложение его опознало, но не подключило к использованию. Исправляем это, нажимая на кнопку с найденным плагином.
Подключаем плагин, читаем сообщение, что "Плагин подключен и будет импортирован в проект после следующего запуска."
Если снова выбрать в выпадающем списке пункт "Плагины", увидим, что, действительно, плагин успешно падключен, о чем свидетельствует кнопка, горящая уже голубым цветом.
Собственно, что произошло, когда мы выбрали пункт "Подключить"? Имя плагина было добавлено в список разрешенных к использованию в файл plugins_list.list в директории Plugins. Теперь модуль загрузки плагинов loadplugin.py сможет импортировать данный плагин.
Давайте это проверим и запустим тестовый пример еще раз.
Появилась новая кнопка с логотипом Хабра в actionbar. Нажимаем ее и наслаждаемся сообщением "Привет, плагин"!
Вот, собственно, и все.
Я не рассматриваю в данной статье код самого плагина (он довольно прост). Вы можете открыть файл init.py пакета HabraButton и посмотреть, как через экземпляр app плагин обращается к объектам программного кода класса Program. Также за бортом я оставил верефикацию плагинов. Мне она нужна, чтобы отслеживать совместимость и визуализировать информацию о плагинах. Все это есть в тестовом примере.
Надеюсь, смог быть полезным!
Тестовый пример — 40.65 Kb.
Комментарии (23)
danSamara
11.04.2016 19:57Подобный подход к подгрузке плагинов используют, когда само приложение и плагины разнесены. Например есть основное приложение, а есть доп. функционал, устанавливаемый пользователем. В этом случае оправданы и жёсткое местонахождение плагинов и не очень красивая их загрузка. Я не совсем понял — это ваш случай или нет?
Если же приложение собирает сам автор — можно было реализовать загрузку через сами плагины, типа MyPlugin.register(app). В этом случае получаем больше свободы — нет глобальных объектов, местонахождение непринципиально, структура плагина какая угодно.
HeaTTheatR
11.04.2016 20:36Да, в статье описан именно тот случай. Плагины создаются и добавляются пользователями. Я отталкивался от простоты конструкции, а не от ее красоты :)
gigimon
11.04.2016 21:47А как быть с установкой дополнительных плагинов, написаных не вами? Кидать их ручками в вашу папку Plugins, но тогда пропадает возможность использования пакетных менеджеров. Неплохо плагин система сделана в py.test, через встроенные средства setuptools.
HeaTTheatR
11.04.2016 21:56-2Ну, я считаю, что файлы, принадлежащие приложению (например, плагины), должны быть в каталоге приложения, а поскольку я люблю организацию, то, да, я считаю, что плагинам место в папке Plugins каталога проекта. Как они туда попадут, по-моему, дело второе.
bromzh
HeaTTheatR
А при чем здесь eval? Почему все так боятся eval, забывая, что любой импрот python-модуля — это тоже выполнение кода, суть которого вы не знаете.
bromzh
Лично я помню, что импорт eval'ит файл. Просто нужно использовать соответствующие инструменты, вроде import_module. А в третьем питоне так вообще всё получше стало с динамическим импортом всякого.
У вас даже нет обработки ошибок при открытии файла. И кэш тоже не создастся, а это ускорит последующую загрузку.
Ну и да, почему питон-то второй? Киви и третью версию поддерживает.
HeaTTheatR
Священный страх перед eval навеян сомнительными личностями из Интерната, мол, не дай бог в строку, которая будет выполнена в eval передадут какой-то ужасный код, форматирующий систему. Берем любой модуль из проекта, пишем код форматирования системы, кидаем обратно в проект… и при чем здесь eval? Вот этого я не понимаю.
Насчет третьего Python.
Не вижу плюсов перед второй веткой. В упор.
nikolay_karelin
asyncio?
Насколько я знаю, для многих разработчиков — это именно та killer-feature чтобы перейти на 3-ю ветку.
HeaTTheatR
И только? А сколько причин НЕ переходить на ртерью ветку? В десяткип раз больше!
Как и обсуждения eval в комментариях к этой статье, никто не может назвать хотя бы три адекватных причины, почему я должен переходить на третью ветку Python, кроме тех, что, мол, в Интернете все так делают!
bromzh
1) Юникод
2) Type hints
3) Nonlocal, yield from, итераторы в коллекциях по-умолчанию. Много модулей, которые в 2,7 только в виде бэкпортов
Ну и 2-ю ветку всё меньше и меньше развивают. Новые фичи не бэкпортнутся.
Ждём десятки причин не переходить на третью ветку.
HeaTTheatR
Например, в Python Package Index с Python 3… Сколько и какие пакеты вы можете поставить? Никто из крупных компаний не использует Python3 и не планирует переход не эту мертвую и убогую ветку. Пара тройка пакетов (даже не написанных, а ИНТЕГРИРОВАННЫХ под Python3) явно не устраивают плачущих горе-программистов, которые перешли не эту ветку. Жалобы "портируйте-портируйте-портируйте… " Этого достаточно или еще десяток превести?!
Никаких координальных прорывов в третьей ветки нет! Интерес к ней у разработчиков — постольку-поскольку имидж в сети...
Pr0Ger
Жду остальные 10 причин, потому что все пакеты которые хотя бы более-менее популярны уже давно портированы.
А мертвая ветка как раз таки вторая, чей срок поддержки истекает в 2020 году (и изначальный план был полностью закрыть поддержку в 15 году, но Гвидо слишком добрый)
bromzh
http://py3readiness.org/
330 из 360 самых популярных пакетов. В реальности, для тройки нет только уж совсем специфичных пакетов, которые и так не особо развиваются. Ещё причины есть?
HeaTTheatR
Вы из этих программистов?
HeaTTheatR
Если Type hints — причина для вас, чтобы перейти на третью ветку, — то вы, пардон, сумашедший! Лучше бы уже подтвердили мои предположения относительно "потому что в Интернете все так делают!"
HeaTTheatR
Насчет ошибок...