О разработке одного desktop-приложения на Python

Всем привет. В этой статье я хочу рассказать о разработке программы с открытым исходным кодом для оффлайнового хранения заметок — OutWiker. Этим проектом я занимаюсь в свободное время, первая версия программы вышла в далеком 2010 году, и OutWiker до сих пор продолжает развиваться. Так уж исторически сложилось, что кодом я занимаюсь практически в одиночку (хотя изредка получаю полезные отдельные патчи), но зато пользователи активно участвуют в переводе программы на свой родной язык и иногда присылают стили оформления для страниц, которые затем я включаю в сборку. А уж о том, сколько интересных идей они присылают, и говорить не стоит.


Что такое OutWiker


Когда-то я писал об этой программе на Хабре, но это было так давно, что прежде чем говорить об особенностях внутреннего устройства и процесса разработки, нужно сказать, что представляет собой программа с точки зрения пользователя. Все ссылки, связанные с проектом даны в конце статьи. Итак, OutWiker — это программа для хранения заметок в виде дерева, в англоязычном интернете такой софт обычно называют outliner (поэтому у программы такое название). Среди более известных «коллег» OutWiker с подобным функционалом можно назвать Zim, WikidPad, CherryTree, и множество других (ну и, разумеется, org mode для Emacs). Логичный вопрос с точки зрения пользователя — чем OutWiker отличается от других представителей древовидных записных книжек. На данный момент, по прошествии такого количества времени с момента начала разработки, я уже не готов развернуто сравнить весь этот софт. В стародавние времена я перепробовал десятки outliner-ов, штук пять использовал достаточно долгое время, но везде чего-то не хватало, хотелось одну возможность взять из WikidPad, другую — из викидвижка, который может работать оффлайново и т.д. Поэтому в качестве ответа на такой вопрос просто перечислю основные особенности, которыми обладает OutWiker.


ymimofpxwkjsyoacsh9gf71c2uk.png


  1. Все заметки хранятся в виде папок на диске. Это сделано по двум причинам — для надежности, чтобы, например, при постепенном умирании харда все заметки не отправились на тот свет вместе с одним файлом. И, кроме того, это позволяет просматривать и редактировать заметки без OutWiker. Разумеется, в таком способе хранения есть свои недостатки, но ничего не дается просто так.
  2. К каждой заметке можно прикреплять произвольное количество файлов (на самом деле и папок, но эта возможность не особо афишируется). Одно из назначений прикрепленных файлов (в основном картинок) — это использование их в тексте заметок. Таким образом, изменяя прикрепленную картинку, мы сразу видим новую картинку в тексте страницы.
  3. У OutWiker нет визуального редактора (я думаю, что он когда-нибудь появится, но пока руки до него не доходят), но есть несколько типов страниц — HTML-страницы, викистраницы и Markdown-страницы (после установки соответствующего плагина). В OutWiker упор сделан на викистраницы. Викинотация напоминает pmWiki (не очень распространенный движок для сайтов), но с некоторыми отличиями, об особенностях викинотации и ее реализации я скажу ниже.
  4. Как вы уже поняли по предыдущему пункту, программа умеет работать с плагинами, которых на данный момент больше 20. Например, с помощью плагинов для викистраниц можно добавить раскраску кода для разных языков программирования, поддержку формул в формате LaTeX, возможность вставки разных счетчиков (например, номеров рисунков), графики и диаграммы по данным, можно добавить шаблоны для часто вводимых фраз, создавать страницы, скачивая их из интернета и многое другое.
  5. Каждая заметка может быть помечена произвольным количеством тегов. Облако тегов показывается сбоку в окне программы.
  6. Программа кроссплатформенная, есть сборки под Windows и Linux (с Mac OS X у меня пока дела не складываются).
  7. Исходный код открыт и распространяется по лицензии GPL 3.


edqoiezve0-mrwhfsxwvel0kw0y.png

zvxa3ueyc8m7rqa3bq5osvcb00o.png


Кое-что о внутренней кухне разработки


Язык и основные библиотеки


Теперь я расскажу, как OutWiker устроен изнутри, а также с какими проблемами пришлось столкнуться при разработке.


Весь проект написан на Python, причем на Python 2.7, переход на Python 3.x планируется, но не в ближайшее время (почему, скажу чуть позже). Для создания интерфейса используется библиотека wxPython. На этапе зарождения проекта в качестве библиотеки для интерфейса выбирал между wxPython и PyQt, остальные подобные библиотеки были отвергнуты, потому что они создают интерфейс, который выглядит чужеродно или под Windows, или под Linux, и может быть и там, и там. В результате остановился на wxPython из-за того, что размер программы получался меньше. Точные цифры размеров я уже не назову, да и проект с того времени уже сильно разросся. Выбором wxPython я доволен, хотя с ней были связаны некоторые неприятные моменты (о них тоже скажу позже).


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


Что касается кроссплатформенности, то в программе не так много мест, работа которых зависит от операционной системы, и все они связаны с интерфейсом. Пожалуй, наибольшее различие заключается в способе отображения HTML-страниц. Под Windows используется движок Internet Explorer, а под Linux — WebKit. Раньше OutWiker работал под wxPython 2.8 и движки Internet Explorer и WebKit использовались с помощью хаков, которые перестали быть актуальными с выходом wxPython 3.0, теперь классы для работы с этими движками встроены в библиотеку. Правда, Internet Explorer я продолжаю использовать через хак с вызовом COM-объекта, потому что такое использование дает больше возможностей для настроек поведения движка. Теоретически у класса wx.html2.WebView, который должен под Windows работать через Internet Explorer, а под Linux через WebKit, есть метод GetNativeBackend (), который должен вернуть указатель на «настоящий» движок, но почему-то у меня этот метод всегда возвращал None.


Надо признаться, что на проект большое влияние оказала программа WikidPad. Я сам ей долго пользовался, она тоже написана на Python + wxPython, и иногда я даже подсматривал в ее исходники, чтобы посмотреть, как некоторые моменты там сделаны. Надо сказать, что такое подсматривание сэкономило немало времени.


Викинотация


Для страниц с оформлением предназначены три типа страниц: HTML-страницы, Markdown-страницы (требуется добавить соответствующий плагин) и викистраницы. То, что в программе должны быть викистраницы я решил с момента зарождения проекта. В отличие от HTML, викинотации более лаконичны и их легко расширять, добавляя новые команды. Другое преимущество викинотаций — это возможность преобразования встроенного текстового описания в некий графический объект. Например, в OutWiker есть команда для вставки формул в формате LaTeX, вставки графиков, которые строятся либо по данным из текстового файла, либо по данным, которые вставлены непосредственно в код страницы, есть плагин, который из текстового описания создает описание диаграмм и т.п. Кроме того, поскольку викистраница в итоге преобразуется в HTML для отображения пользователю, то это тоже можно использовать, например, при подготовке статей на сайт, если движок сайта ожидает ввода в формате HTML. Например, сейчас эту статью я пишу в виде викинотации, а в конце переключусь на вкладку HTML и получу готовый HTML-код, который нужно будет только немного подогнать под особенности конкретного сайта.


Викинотация:


y4g-4zw_lmzt7jxkm3utwvzi1aa.png


Результат:


4dvoiiiplvxwvseb8vfckx20zie.png


HTML:


dyvy8ciqoo67asu7bvtlbuflhny.png


В качестве викинотации я ориентируюсь на движок pmWiki, он не очень распространен, но у меня уже был опыт работы с ним и мне нравится продуманность его нотации. В некоторых моментах при реализации парсера я отходил от оригинальной нотации, но в целом стараюсь придерживаться ее. В эту викинотацию очень легко добавлять свои команды в формате (: command_name:)…(: command_nameend:).


Иногда спрашивают, почему я не использовал изначально Markdown, но на мой взгляд это слишком ограниченный язык, к тому же pmWiki более логичный. Например, в Markdown я вечно путаюсь с порядком аргументов для ссылки: [текст](http://example.com/) — два вида скобок, еще важен их порядок. В используемой викинотации ссылка оформляется в виде: [[текст → example.com]]. Стрелка указывает направление ссылки. На самом деле есть еще второй формат ссылок, но лично я обычно пользуюсь таким.


Кстати, в OutWiker не обязательно запоминать викинотацию, все элементы оформления можно вставлять через меню или с помощью панели инструментов. Для HTML- и Markdown-страниц такие кнопки и пункты меню тоже имеются.


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


Локализация


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


y4u1_vhxkdgx0mfyz7hn8n3c8cc.png


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


Вот, например, как выглядит украинская и шведская локализация.


qlsqzxfxpo3lmhzlkmkmlaatjxa.png


m9jzce70dtiboghmbf2vpzxs4uo.png


Для использования локализаций внутри OutWiker используется стандартный модуль gettext.


Особенности сборки


Поскольку Python — интерпретируемый язык программирования, то для того, чтобы пользователь мог запустить программу, теоретически у него должны быть установлены Python и все необходимые библиотеки. Разумеется, заставлять обычных пользователей устанавливать Python, wxPython и другие требуемые библиотеки, да еще и нужных версий, негуманно. Пользователь должен скачать программу, распаковать ее установить с помощью инсталятора и метода «Next — Next — Next», после чего запустить иконку на рабочем столе. К счастью, для программ, написанных на Python, это все делается сравнительно просто. Если говорить про Windows, то интерпретатор Python без стандартных библиотек умещается в dll-ку размером чуть больше 3 МБ. Существуют несколько утилит, которые делают запускаемые файлы (exe-шники, если говорить про Windows) из Python-скриптов. Наиболее известные из них — cx_Freeze и pyInstaller. По возможностям они примерно равноценны (хотя pyInstaller позволяет создавать единственный exe-шник из Python-скрипта, а у cx_Freeze такой возможности нет), но у них разных подход к тому, где хранить файлы *.pyc библиотек, и по поводу того, как задавать параметры сборки.


Для создания exe-шников OutWiker долгое время использовался cx_Freeze. Проблем с ним не было, пока не вышел cx_Freeze 5.0, в этой версии автор сильно переписал внутренности утилиты. Видно из-за этого что-то пошло не так. Сборка OutWiker, созданная с помощью cx_Freeze 5.0 начала виснуть при старте, причем судя по результатам отладки, на ровном месте, где никаких проблем не ожидается (скорее всего это как-то было связано с многопоточностью). После выхода cx_Freeze 5.0.2 эта проблема исчезла, но OutWiker перестал выгружаться из памяти при закрытии программы. Эту проблему можно было решить явным вызовом sys.exit (0) в конце программы, но это костыль, который не хотелось добавлять. К тому же, кто знает, какие еще проблемы могли возникнуть при использовании cx_Freeze 5.0.x. Можно было бы остаться на проверенной временем cx_Freeze 4.x, но я не люблю использовать устаревшие библиотеки. Тогда было решено перейти на pyInstaller. Переезд оказался достаточно быстрым, буквально за один день. Никаких проблем с новой сборкой не возникло. Поэтому теперь для сборки используется pyInstaller. С точки зрения пользователей внешне ничего не должно поменяться.


Те же самые cx_Freeze / pyInstaller можно использовать и для создания бинарных сборок под Linux, чтобы полученная сборка могла запускаться в разных дистрибутивах Linux. Под Windows из полученной сборки создается инсталятор с помощью Inno Setup, а под Linux из подобной сборки создается deb-пакеты (на виртуальных машинах, что будет описано ниже).


Другая особенность сборки заключается в том, как запускаются разные цели сборки. Все начиналось с небольшого Makefile, который постепенно рос, и проблем с ним становилось все больше. В основном они были связаны с кроссплатформенностью — поддерживать единый Makefile под Windows и Linux было тяжело. В свое время мне это надоело и я начал искать альтернативу, желательно, чтобы она была написана на Python. Альтернатива нашлась достаточно быстро в виде программы Fabric. Теперь все бывшие цели Makefile переписаны в обычные функции Python, да и вообще система сборки разрослась неимоверно. Вот, например, как сейчас выглядит список задач Fabric.

$ fab -l
Available commands:

    apiversion            Print current OutWiker API versions
    apiversions           Print current OutWiker API versions
    build                 Create artefacts for current version.
    clear                 Remove artefacts after all assemblies
    create_tree           Create wiki tree for the tests
    deb                   Assemble the deb packages
    deb_binary            Create binary deb package
    deb_binary_clear      Remove binary deb package
    deb_clear             Remove the deb packages
    deb_install           Assemble deb package for current Ubuntu release
    deb_single            Assemble the deb package for the current Ubuntu release
    deb_sources_included  Create files for uploading in PPA (including sources)
    deploy                Upload unstable version to site
    doc                   Build documentation
    linux_binary          Assemble binary builds for Linux
    linux_clear           Remove binary builds for Linux
    locale                Update the localization file (outwiker.pot)
    locale_plugin         Create or update the localization file for pluginname plug-in
    outwiker_changelog    Generate OutWiker's changelog for the site
    plugin_changelog      Generate plugin's changelog for the site
    plugin_locale         Create or update the localization file for pluginname plug-in
    plugins               Create an archive with plugins (7z required)
    plugins_clear         Remove an archive with plugins (7z required)
    plugins_list          Print plugins list for th site
    prepare_virtual       Prepare virtual machine
    run                   Run OutWiker from sources
    site_versions         Compare current OutWiker and plugins versions with versions on the site
    sources               Create the sources archives
    sources_clear         Remove the sources archives.
    test                  Run the unit tests
    test_build            Run the build unit tests
    upload_binary         Upload unstable version to site
    upload_plugin         Upload plugin to site
    upload_plugins_pack   Upload archive with all plugins to site
    vm_halt               Stop virtual machines for build
    vm_linux_binary       Create 32- and 64-bit assembly on virtual machines
    vm_prepare            Prepare virtual machines for build
    vm_remove_keys        Remove local SSH keys for remote virual machines
    vm_run                Run virtual machines for build
    vm_stop               Stop virtual machines for build
    vm_update             Update the virtual machines
    win                   Build OutWiker for Windows with cx_Freeze
    win_clear             Remove assemblies under Windows

Как видите, команд достаточно много, все они описаны в документации. Сюда входят команды для сборки под Windows и Linux, запуска тестов, работы с виртуальными машинами для сборки, обновления локализаций, выкладывания новых версий на сайт и другие. Несмотря на то, что, судя по документации, Fabric больше ориентирован на работу с удаленными серверами (условный аналог Ansible), но в качестве замены Makefile мне он очень нравится.


Войны с Ubuntu


С wxPython были связаны еще несколько неприятных моментов, которые, к счастью, удалось достаточно быстро решить. Все эти проблемы возникали под Ubuntu, на которую я в первую очередь ориентируюсь при разработке (из-за того, что у меня это основная операционная система). Первый неприятный момент был связан с переходом с wxPython 2.8 на 3.0 (не путать с Python 3.x). Дело в том, что при переходе с версии 2.8 к 3.0 были некоторые проблемы с обратной совместимостью, но, к счастью, эти две версии можно было ставить одновременно и из python-скрипта выбирать нужную версию с помощью wxversion. (с выходом wxPython 4.0 такой возможности больше не будет). Поскольку Python — интерпретируемый язык, и в Ubuntu он всегда присутствует, то для запуска под этой операционной системой нет необходимости создания бинарных сборок, а просто при установке deb-пакета исходники раскидываются по нужным папкам и запускаются с помощью команды python runoutwiker.py. Все необходимые библиотеки для работы wxPython и WebKit указаны как зависимости. Однако с выходом Ubuntu 16.04 LTS возникли две проблемы. Первая была связана с тем, что в этой версии Ubuntu убрали wxPython 2.8, поэтому пришлось срочно мигрировать на wxPython 3.0 и отказаться от поддержки другой долгоживущей версии Ubuntu 14.04 LTS. Но это еще не все, оказалось, что в Ubuntu 16.04 (в следующих версиях это исправили) wxPython как-то неправильно скомпилирован, и в нем без шаманств не работает движок WebKit. К счастью, тут помогли продвинутые пользователи OutWiker, которые подсказали, что проблема обходится с помощью LD_PRELOAD и указанием пути до одной библиотеки, относящейся к wxPython. Как я уже сказал, надобность в этом костыле в последующих версиях Ubuntu отпала, но ради поддержки Ubuntu 16.04 его приходится использовать до сих пор.


Чтобы в будущем обезопасить себя и пользователей от подобных изменений в Ubuntu, недавно начал делать бинарные сборки под Linux. Такие сборки делаются для 32- и 64-битных систем. В перспективе это позволит самостоятельно компилировать wxPython с нужными параметрами и не зависеть от имеющихся библиотек в репозиториях Ubuntu. Для того, чтобы создавать бинарные сборки (и deb-пакеты на основе их) используется связка Ansible, Vagrant и VirtualBox. С помощью Vagrant запускаются две виртуальные машины (32 и 64 бита), туда с помощью Ansible через SSH закачиваются исходники программы, там создаются бинарные сборки и deb-пакеты, которые скачиваются обратно на рабочий комп (хост). Но недавно и тут разработчики Ubuntu подложили свинью. Дело в том, что виртуальные машины создаются на основе Ubuntu 17.04, и в перспективе я собирался переводить виртуальные машины на Ubuntu 17.10. Я бы рад был использовать для сборки версию Ubuntu 16.04 LTS, но, как я уже говорил, там есть проблемы с wxPython. Но внезапно разработчики Ubuntu решили отказаться от поддержки 32-битных систем. Возможно, в будущем для сборки на виртуальных машинах придется переходить на другой дистрибутив. Конечно, можно было бы тоже отказаться от поддержки 32-битных систем, но в данный момент такая поддержка мне как разработчику не мешает, а пользователям может быть полезна.


Другие используемые библиотеки


Кроме уже упоминавшихся библиотек wxPython и pyparsing в проекте используются и другие библиотеки. Коротко перечислю их.


  • PyEnchant. Используется для проверки орфографии.
  • Pygments. Используется в плагине Source для раскраски исходников кусков кода. На вход библиотеке мы подаем исходник, на выходе получаем красиво оформленный HTML. Pygments понимает огромное количество языков программирования, в том числе экзотических и редко используемых, а также дает выбирать стиль оформления (к библиотеке прилагается более десятка стилей).
  • Python Markdown. Как следует из названия, предназначен для разбора нотации Markdown.
  • KaTeX. Используется в плагине TeXEquation. JavaScript-библиотека для рендеринга формул в формате LaTeX. Раньше использовалась программа mimeTeX, но формулы, созданные с помощью нее выглядели не очень эстетично и были проблемы с отображением формул на страницах с темным фоном, потому что mimeTeX создает картинку с черным текстом.
  • Beautiful Soup. Используется в плагине WebPage, предназначенном для создания страниц из страниц в интернете (при этом скачиваются все картинки, стили CSS и скрипты JavaScript). Библиотека используется для разбора и правки скачанного HTML-кода. Например, чтобы заменить исходные ссылки на картинки в HTML-коде на пути до скачанных картинок.
  • Chardet. Используется в том же плагине WebPage, чтобы определить кодировку скачанной страницы.
  • Jinja2. Используется в плагине Snippets, который предназначен для вставки в заметки шаблонного текста. Шаблоны создаются в формате Jinja (с некоторыми упрощениями).
  • Blockdiag. Используется в плагине Diagrammer, который по текстовому описанию строит схему из кубиков, связанных между собой стрелками. Основную работу в плагине выполняет библиотека Diagrammer. Ее можно рассматривать как упрощенную версию dot и Graphviz.


Планы на дальнейшее развитие


Проект OutWiker активно развивается, доказательство чему статистика коммитов на github:

wkusetzbid6ruhw7u59gl0fpyk4.png

Сейчас исходники занимают около 90 МБ, куда входят почти 4300 файлов, из них 1772 — python-скрипты.

Планов и идей огромное количество (количество issues на github сейчас чуть меньше 350). Туда попадают любые пожелания пользователей, даже если в ближайшее время до их реализации руки точно не дойдут. Поскольку кодом занимаюсь я практически в одиночку, то новые возможности появляются не так быстро, как хотелось бы. В последнее время стабильные версии OutWiker выходят примерно раз в год, но каждый месяц я выкладываю нестабильные версии. На самом деле их нестабильность — вопрос философский, потому что я не выкладываю откровенно неработающие версии, и иногда нестабильные версии «стабильнее» стабильных в том плане, что после релиза в них исправляются какие-то найденные не критичные недоработки из последнего релиза. Но в нестабильных версиях я могу себе позволить что-то не успеть отшлифовать, а может быть что-то не заметить после добавления новой возможности.


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


В свете описанных выше вечно возникающих проблем с Ubuntu хочется попробовать под Linux сделать snap-пакет или/и flatpak-пакет.


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


В более дальней перспективе нужно будет перейти на wxPython 4.0, который, надеюсь, скоро получит статус релиза, и на Python 3.x. Логично сначала будет перейти на wxPython 4.0, который поддерживает обе версии Python, а потом перейти на Python 3.


Ссылки


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

© Habrahabr.ru