Приветствую, земляне, сегодня я спешу к вам с очередным подарком. Несколько месяцев я разрабатывал программный комплекс, который позволяет в некоторой степени упростить разработку узкоспециализированных программ и использовать их в одном приложении. Ранее проект был анонсирован в моем телеграм канале, подписывайтесь, чтобы быть в курсе и иметь возможность принять участие в разработке. Эта статья дает старт рубрике ШБР (школа борцов с рутиной), в которой будут обсуждаться вопросы автоматизации некоторых задач.

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

Итак, что же я в очередной раз сотворил и зачем? Дело в том, что я в свое время вдохновила книга «Автоматизация рутинных задач с помощью Python. Практическое руководство для начинающих», и начал писать разные скрипты для решения всяких специфических задач. Скрипты копились, я забывал о них и их предназначении, также у них не было графического интерфейса, что я считаю, не очень круто. Поэтому я принял решение написать программу, которая бы позволила навести порядок во всем этом зоопарке, и в некоторых аспектах ускорить разработку, к тому же мне давно хотелось сделать программу, которая бы состояла из подключаемых модулей(плагинов). И да, я не соврал, когда говорил, что это швейцарский нож, но с одним нюансом – пока есть только рукоятка и напильник.

Так на свет появился комплекс PUSSY (Python Universal Script System for You) – набор средств для создания утилит с графическим интерфейсом на базе Python и PySide6 и программы управления ими. Он предоставляет простые инструменты создания структур для хранения пользовательских настроек, достаточно определить один класс с перечнем свойств, для определения одного параметра достаточно одной строчки кода, а программа сама организует средства для ввода/вывода и хранения этих данных.

Далее я буду называть программу управления как Менеджер, а утилиту плагином. По сути, каждый плагин — это независимое Qt-приложение, программа управления никак не вмешивается в их работу, только оповещает их о некоторых пользовательских действиях. Разработчику только нужно определить класс интерфейса, который Менеджер встроит в свой интерфейс. В данном случае интерфейс здесь является отправной точкой, а за ним может скрываться совершенно любая реализация.

Обзор Менеджера

Перед тем как приступить к разработке плагина, проведу обзор возможностей Менеджера, именно через него пользователь будет взаимодействовать с плагинами. Качаем код проекта  и запускаем скрипт PUSSY.pyw, для запуска потребуется Python 3 и PySide6.

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

Главное окно программы и демонстрационный плагин
Главное окно программы и демонстрационный плагин

Быстренько пробежимся по настройкам Менеджера, чтобы перейти на страницу с ними нажмите Ctrl+Alt+S или выберите соответствующий пункт в верхнем меню. Тут все очень минималистично, настройки чтобы немного настроить внешний вид интерфейса и прочее. Только остановлюсь на «Display error info in Logs», если отмечена, то на странице логов в случае ошибки будет выводиться дополнительная информация о перехваченном исключении; «External plugin directories»- здесь перечислены дополнительные директории, в которых будет производится поиск плагинов. Чтобы сохранить настройки нужно прожать «Apply Settings», a кнопка «Delete Invalid Data» нужна чтобы удалить данные конфигурации для плагинов, которые были удалены.

Нажатие комбинации Ctrl+Alt+P приведет нас на страницу со списком плагинов. Что мы тут видим (слева-направо): имя плагина, его активность, кнопка для перехода к настройкам, дополнительные опции (Plugin info – вывод информации о плагине; Reset settings – сброс до базовых настроек; Initialize on startup – отметка о том, что нужно инициализировать плагин при запуске Менеджера.

Здесь список всех плагинов и средства управления ими
Здесь список всех плагинов и средства управления ими

При на нажатии на кнопку (3) покажется окно редактирования настроек, если они определены, если нет, то кнопка будет неактивной. Меню настроек по умолчанию выглядит как на нижнем рисунке, если форма вывода не определена кодером иным образом. По умолчанию они выводятся в два столбца: слева - имена свойств; справа – виджеты для ввода значений.

Так выводятся настройки по умолчанию
Так выводятся настройки по умолчанию

И в завершении прожмите Ctrl+Alt+L чтобы перейти на страницу логов, тут ничего мудреного нет, некоторые события будут отмечаться здесь, зеленым будут выделены те, с которыми все окей, в противном случае – красным. Если «Display error info in Logs» установлена, то в случае ошибки будет выведена информация трассировки. В некоторых ситуациях это поможет выявить ошибки в коде. Если ошибка возникнет в процессе отрисовки главного интерфейса или интерфейса редактирования настроек, то вы увидите примерно вот это:

При инициализации интерфейса вылезло исключение, вызывайте фиксиков
При инициализации интерфейса вылезло исключение, вызывайте фиксиков

Думаю, всем понятно как устроен Менеджер, тут все предельно просто и интуитивно, разработка тоже предельно проста, будет достаточно минут 30 максимум чтобы во все разобраться.

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

Как написать свой плагин? Набираем обороты.

Для работы нам потребуется:

  • Python3;

  • PySide6;

  • Инструмент для написания кода (я предпочитаю IDE PyCharm).

Если PySide6 у вас по какому-то недоразумению не установлен, то выполняем команду в терминале:

pip install PySide6

Далее качаем исходный код проекта, если ранее не сделали. Можете первым делом запустить файл PUSSY.pyw, чтобы все пощупать непосредственно. Кстати, на данный момент эта статья единственное существующее руководство, читайте внимательнее.

Комплекс состоит из двух частей: 1-Менеджер (папка manager), 2-Фреймворк (папка PyUB). Если желаете знать почему основной пакет носит имя PyUB, то все просто, первоначальное рабочее название Utilities Box. Итак, что есть главном пакете:

  • Пакет App – его трогать не нужно, он обеспечивает работу Менеджера;

  • Пакет Types – содержит все необходимые компоненты для разработки:

    o Пакет InputWidgets – здесь классы с виджетами для ввода информации;

    o  Пакет Properties – здесь классы свойств, которые служат для ввода пользовательских настроек;

    UBWidget – класс-родитель для морды интерфейса вашего плагина, он и будет являться отправной точкой, является модифицированным потомком QWidget;

    o PropertyContainer – класс куда вы будете складывать свои свойства, это по моей задумке, но при желании это можно обыграть иначе, главное чтобы интерфейсы остались совместимыми;

    UBHelper – класс со вспомогательными функциями;

  • utils – модуль с необходимым функциями.

Как видите, арсенал вполне минималистичный, все только самое необходимое.

Пишем код плагина

Сделаем пробный плагин как на первой картинке, кодовый замок, конечно, это просто игрушка для демонстрации. Код от «замка» устанавливается через настройки, если код введен верно, то поле загорается зелеными, иначе – красным.

Посмотрим на плагин в общих чертах, по сути это пакет Python - папка, содержащая файл __init__.py, размещается она в папке manager/Plugins, либо во внешних директориях, указанных в настройках. В папке Plugins есть шаблон (папка Template) скопируйте его, когда будете писать свой плагин.

Внимание! Пакеты с именем Template игнорируются при загрузке.

 В нашем случае плагин будет разделен на 4 файла (в исходных файла он находится в Plugins/Code lock): __init__.py – здесь будет производится регистрация класса с интерфейсом и размещается дополнительная информация, ничего более тут выполнять не нужно; View.py – здесь определен главный класс интерфейса; Settings.py – класс с настройками; ui_form.py – код виджета, сгенерированный в Qt Designer.

Так выглядит код View.py:

from PyUB.Types import UBWidget
from PySide6.QtWidgets import QPushButton
from .ui_form import Ui_Form
from .Settings import Settings

class CodeLock(UBWidget):

    ub_settings = Settings

    def __init__(self):
        super().__init__()
        self.ui = Ui_Form()
        self.ui.setupUi(self)
        self._init_gui()
        self.code = self.ub_settings.get_property_value("code")

    def _init_gui(self):
        for name in dir(self.ui):
            attr = getattr(self.ui, name)
            if type(attr) is QPushButton:
                attr.clicked.connect(self.on_btn)
        self.ui.lineEdit.setStyleSheet("")
        self.ui.lineEdit.textChanged.connect(self.text_changed)
                          self.ui.lineEdit.setInputMask(self.ub_settings.get_property("code").p_input_mask)

    def on_btn(self):
        if not self.ui.lineEdit.isEnabled(): return
        s = self.sender()
        self.ui.lineEdit.setText(self.ui.lineEdit.text() + s.text())

    def text_changed(self, text):
        if len(text) == len(self.code):
            if text == self.code:
                self.ui.lineEdit.setStyleSheet(u"background-color: rgb(85, 255, 127);")
            else:
                self.ui.lineEdit.setStyleSheet(u"background-color: rgb(240, 54, 8);")

            self.ui.lineEdit.setEnabled(False)
            self.id = self.startTimer(5000)

    def timerEvent(self, event) -> None:
        self.ui.lineEdit.setStyleSheet("")
        self.ui.lineEdit.clear()
        self.ui.lineEdit.setEnabled(1)

        self.killTimer(self.id)

    def settings_edit_finished(self, changed:bool) -> None:
        if not changed: return
        self.code = self.ub_settings.get_property_value("code")
        self.ui.lineEdit.clear()

В данном коде определен класса интерфейса, также в нем описана вся логика программы. Этот класс и будет встраиваться в окно просмотра в качестве вкладки. Код в методе __init__() выполняется, когда пользователь перешел во вкладку, в которой размещается экземпляр данного класса, или при запуске Менеджера, если пользователь ранее активировал параметр «Инициализировать при запуске» (Init on startup).

Класс UBWidget дополняет QWidget следующими атрибутами:

Поля класса:

  • ub_settings  - (опционально) ссылка на класс с пользовательскими настройками (потомок UBPropertyContainer);

  • ub_name:str– (опционально) имя плагина, которое будет отображаться в менеджера, если не определено, то будет использовано имя папки.    

Методы:

  • __init__(self)  - инициализация виджета;

  •   retranslate(self) –перевод интерфейса виджета на другой язык (на момент написания статьи функция не реализована);

  • app_closing(self) – выполняется Менеджером перед его закрытием, когда пользователь запустил процедуру закрытия главного окна, нужно чтобы выполнить требуемые действия перед закрытием, например, остановить исполняемые процессы, сохранить данные;

  • settings_edit_started(self) – выполняется Менеджером перед началом редактирования настроек;

  • settings_edit_finished (self, changed:bool) – выполняется после завершения редактирования настроек, changed принимает значения: True – если хотя бы одно из свойств было изменено, иначе – False;

  • deactivated(self) – выполняется, если плагин был деактивирован пользователем, перед тем как экземпляр данного класса будет удален из окна просмотра.

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

Перейдем к файлу с настройками (Settings.py). Его код:

from PyUB.Types import PropertyContainer
from PyUB.Types.Properties import StringProperty


class Settings(PropertyContainer):
    code: StringProperty(default_value="12345", input_mask="00000000", name="Код")

Здесь мы определяем класс-контейнер со свойствами. Все классы свойств находятся в пакете Properties. Свойства записываются в теле класса как аннотации. Формат объявления свойств следующий:

<имя свойства>: <Свойство>(<значения параметров>)

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

На данный момент выбор следующий:

1)      BoolProperty – свойство для ввода булевых значений;

2)      ComboBoxProperty – выпадающий список, возвращает числовой индекс элемента, чтобы получить текст элемента у свойства нужно вызвать метод get_current_item_text();

3)      FilePathListProperty-список путей к файлам или папкам;

4)      FilePathProperty – путь к файлу или папке;

5)      FloatProperty – ввод вещественных чисел;

6)      FontSelectProperty- выпадающий список для выбора шрифта;

7)      IntProperty– ввод целых чисел;

8)      NamedFilePathListProperty - именованный список путей к файлам или папкам;

9)      PasswordStringProperty – однострочное поле ввода паролей;

10)   StringListProperty– список строк;

11)   StringProperty– однострочное поле ввода строк.

Не буду сейчас подробно расписывать каждый из них, можно догадаться по сигнатуре вызова конструктора и именам аргументов. Есть три параметра, которые присутствуют у всех свойств: name-имя, оно выводится окне редактирования настроек в качестве подписи, default_value – значение по умолчанию, tool_tip – всплывающая подсказка.

Чтобы контейнер был полезен, нам надо получать данные, которые он хранит. Для этого есть два способа:

1. Использовать метод get_property_value()

<контейнер>. get_property_value(<имя свойства>)

2. Создать экземпляр класса контейнера и обратиться по имени свойства:

<экземпляр контейнера>. <имя свойства>

При необходимости можно получить доступ к самому свойству:

<контейнер>. get_property (<имя свойства>)

Не исключено, что возникнет потребность получить доступ к параметрам, чтобы прочитать или изменить их. Параметры, которые передаются конструктору можно извлечь, только имейте в виду, что согласно соглашению имен внутри экземпляра класса их имена начинаются с префикса «p_», таким образом, поле name получит имя p_name, но для извлечения имени есть метод get_name(). Данному соглашению нужно следовать, когда будете писать собственные классы свойств.

Для справки: параметры свойств и их значения загружаются при запуске Менеджера в самом начале; значения свойств сохраняются после того, как пользователь их изменил; если параметры изменяются в процессе выполнения, то необходимо их сохранить на диске вызовом метода save_settings_parameters() класса UBHelper.

Итак, все важные классы мы уже определили, теперь нужно сообщить Менеджеру какой класс нужно устанавливать. Для этого в модуле utils есть функция register_ubwidget(), в нее надо передать ссылку на класс-потомок UBWidget, в плагине можно зарегистрировать только один класс, это действие нужно выполнить в файле __init__.py. Также в этом файле по желанию можно определить дополнительную информацию о плагине, создав переменную ub_info ={…}, это типичный словарь; допускаются ключи и значения только строкового типа.

 

Ключ

Пример

Описание

1          

description

“Этот плагина выводит спутник на орбиту”

Описание плагина

2          

author

“Иван Иванов”

Имя автора

3          

author_webpage

“ivan_ivanov.com”

веб-страница автора

4          

author_email

“ivan_ivanov@mail.com”

электронная почта автора

5          

version

“1.3.4”

версия плагина

6          

wiki_url

“ivan_ivanov.wiki.com”

ссылка на страницу вики

Диалоговое окно с информацией будет выглядеть следующим образом.

Диалоговое окно с информацией о плагине
Диалоговое окно с информацией о плагине

Гиперссылки активные и при нажатии будут открыты в браузере или почтовом клиенте. Если какое-то из ключей не определено, то будет выведена советующая информация.

В комплекте еще один класс, который остался без внимания. Чтобы его использовать надо создать экземпляр, передав в конструктор ссылку на класс-потомок UBWidget, это нужно для того, чтобы система могла идентифицировать плагин. Вот перечень методов:

  • __init__(self, <класс-потомок UBWidget>)  - принимает ссылку на класс UBWidget, которая в дальнейшем используется как ключ для идентификации плагина при вызове методов;

  • save_settings_parameters (self)- сохраняет параметры свойств на жестком диске, которые находятся в классе PropertyContainer, присвоенный атрибуту ub_settings (метод является потокобезопасным), это нужно в том случае, если плагин изменяет параметры свойств в процессе работы;

  • get_plugin_dir(self)->str-выводит абсолютный путь к папке, в которой находится плагин;

  • open_localstorage(self, flag='c', protocol=None, writeback=False)-возвращает объект базы данных, является оберткой для функции open() из модуля shelve, узнать подробнее можно по ссылке. Файлы базы данных создаются в папке localdata в корневой папке плагина. 

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

Вот и подошла к концу вводная часть, в следующих сериях мы поговорим о Свойствах и как они работают в составе контейнера, разработаем какой-нибудь класс свойства для расширения коллекции, и может быть разработаем какой-нибудь полезный плагин, жду от вас идей. Если есть какие-то вопросы и идеи, то добро пожаловать на мой Discord сервер. Также же я хочу сделать сайт вики, но не могу определиться с движком, хочется, чтобы интернационализация работала из коробки, работал на PHP, MySQL, кто имеет опыт напишите мне.

Ссылки

Комментарии (6)


  1. kAIST
    23.09.2023 20:46
    +1

    Что то идея проекта я не совсем понял. Вернее понял, но не совсем.

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

    Потом вы скорее всего поймёте что эти все плагины должны какое то взаимодействовать друг с другом, передавать какие то данные.

    У меня было ещё несколько итераций, пока я не пришел к нодовой структуре работы плагинов.


    1. IronMesh Автор
      23.09.2023 20:46

      Идея то проста: сделать основу и агрегатор для скриптов. Кликнул по ярлыку и вот все твои скрипты-помогаторы в одном месте.

      "Потом вы скорее всего поймёте что эти все плагины должны какое то взаимодействовать друг с другом, передавать какие то данные."

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


      1. FreeNickname
        23.09.2023 20:46

        Зачем программам, созданным для решения определенной задачи связываться с другими

        Где-то удивлённо поднял брови UNIX :) Можете ещё посмотреть яблочную Shortcuts для вдохновения, кстати.


        1. IronMesh Автор
          23.09.2023 20:46

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

          И что в принципе непонятного в концепции швейцарского ножа? Ножницы или отвертка могут быть действовать совершенно независимо друг от друга


  1. Gardalus
    23.09.2023 20:46
    +1

    Лайк за нейминг, в голос засмеялся)


    1. qark
      23.09.2023 20:46

      PUSSY lovers