По сути Дополнения 2.1.x — это прежние указания Дополнения 2.0 без раздела о портировании дополнений от версии 1.2, только добавились разделы Папки дополнений, Конфигурирование, Перенос дополнений от Anki 2.0 и изменился раздел Публикация дополнений.

Anki 2.1.x Writing Add-ons

Введение

Другие версии

Этот документ описывает написание дополнений для Anki 2.1.x. Указания по написанию дополнений для бета-версии Anki 2.0 смотри в оригинале и в переводе

Беглый обзор

Anki 2.1.x написана на языке программирования Python. Если вы ещё не знакомы с этим языком, пожалуйста, прочитайте Руководство по языку (на англ.), прежде чем продолжить чтение этого документа.

Поскольку Python — это интерпретируемый (динамический) язык, дополнения к Anki могут быть очень мощными, то есть не просто расширять возможности программы, но они могут также модифицировать совершенно разные ее аспекты, такие как изменение способа планирования работы (расписания просмотра карточек), изменения пользовательского интерфейса и так далее.

Никакой специальной среды для разработки дополнений не требуется. Всё, что вам нужно — это текстовый редактор.

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

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

Anki состоит из двух частей

anki

anki содержит весь код внутренней обработки — ведение коллекций, записей, колод карточек, накопление статистики ответов и т. д. Доступ ко всей этой информации может осуществляться как через пользовательский интерфейс Anki, так и через программы, работающие в пакетном режиме (не использующие графический интерфейс пользователя, а просто запускаемые через консоль (командную строку OC)).

aqt

aqt содержит весь код пользовательского интерфейса Anki, который построен на пакете PyQt (сборка кроссплатформенного пакета QT для Python). PyQt достаточно точно следует API QT, поэтому можете пользоваться Qt документацией, когда захотите узнать, как пользоваться той или иной компонентой пользовательского интерфейса (GUI).

Версии Anki 2.1.x используют Qt 5.9

Когда Anki запускается, то проверяет наличие модулей в папке дополнений и запускает каждый найденный.

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

Папки дополнений

Вы можете открыть папку со всеми дополнениями по команде меню ИнструментыДополнения в главном окне Anki — просто кликните по кнопке Просмотр файлов Если у вас ещё не установлено ни одного дополнения, то откроется папка верхнего уровня. Если у вас было выбрано одно из установленных дополнений, то будет открыта именно его папка; в этом случае для перехода в папку дополнений вам потребуется перейти по иерархии папок на один уровень вверх.

Папка дополнений называется addons21, намекая своим названием на версию Anki 2.1.x Если у вас есть и папочка addons, то это говорит о том, что вы ранее пользовались версией Anki 2.0

Теперь для каждого дополнения в папке addons21 ВСЕГДА создаётся отдельная папка. В момент запуска Anki ищет в каждом таком подкаталоге файл init.py, например:

addons21/my_addon/__init__.py

Если файл в подпапке не будет обнаружен, то Anki пропустит такой каталог.

Важно! Во избежание проблем с питоновской системой модулей
в названии директории следует использовать только
латинские буквы от a до z и арабские цифры от 0 до 9

И пока вы можете использовать любое имя поддиректории, какое вы только пожелаете, когда создаёте подкаталог дополнения для себя, при установке дополнения с сайта AnkiWeb Anki 2.1.x использует идентификатор дополнения как название создаваемой папки, например:

addons21/48927303923/__init__.py

Также Anki поместит файл meta.json в такую папку и сохранит в нём оригинальное название дополнения (таким, каким оно указано на сайте) и флажок-признак активации дополнения (подключено-используется/временно-отключено).

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

Простейшее дополнение

Сохраните следующий текст в файле my_first_addon/init.py в вашем каталоге дополнений
(под Windows обычно это папка C:\Users\ юзернейм \AppData\Roaming\Anki2\addons21\):

# import the main window object (mw) from aqt
from aqt import mw
# import the "show info" tool from utils.py
from aqt.utils import showInfo
# import all of the Qt GUI library
from aqt.qt import *

# We're going to add a menu item below. First we want to create a function to
# be called when the menu item is activated.

def testFunction():
    # get the number of cards in the current collection, which is stored in
    # the main window
    cardCount = mw.col.cardCount()
    # show a message box
    showInfo("Card count: %d" % cardCount)

# create a new menu item, "test"
action = QAction("test", mw)
# set it to call testFunction when it's clicked
action.triggered.connect(testFunction)
# and add it to the tools menu
mw.form.menuTools.addAction(action)

Перезапустите Anki, и в меню ИнструментыДополнения вы увидите новый пункт my_first_addon
Клик по этой новой строке меню вызывает окно, показывающее общее количество карточек в коллекции.

Если вы допустите ошибку в коде дополнения, Anki при запуске покажет сообщение об ошибке, указывающее, где находится место, которое вызывало проблемы.

Подробнее

Коллекция

Все действия над коллекцией доступны через mw.col.

Дальше идут некоторые основные примеры того, что вы можете делать.

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

Очередная карточка к изучению

card = mw.col.sched.getCard()
if not card:
    # в текущей колоде сегодня больше нет карточек, предлагаемых к изучению

Ответ на карточку

mw.col.sched.answerCard(card, ease)

Редактирование записи

Добавляется new через пробел в конец каждого поля

note = card.note()
for (name, value) in note.items():
    note[name] = value + " new"
note.flush()

Идентификаторы карточек

Получить идентификаторы карточек для записей, которые содержат метку x

ids = mw.col.findCards("tag:x")

Вопрос и ответ каждой карточки

for id in ids:
    card = mw.col.getCard(id)
    question = card.q()
    answer = card.a()

Чистка расписания

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

mw.reset()

Импорт записей

из текстового файла в колоду карточек

Import a text file into the collection

from anki.importing import TextImporter
file = u"/path/to/text.txt"
# select deck
did = mw.col.decks.id("ImportDeck")
mw.col.decks.select(did)
# set note type for deck
m = mw.col.models.byName("Basic")
deck = mw.col.decks.get(did)
deck['mid'] = m['id']
mw.col.decks.save(deck)
# import into the collection
ti = TextImporter(mw.col, file)
ti.initMapping()
ti.run()

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

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

from anki import Collection
col = Collection("/path/to/collection.anki2")

Если вы делаете любые изменения в коллекции вне Anki, то вы должны быть уверены, что выполнили col.close() в конце работы, иначе все изменения будут потеряны.

Работа с базой данных

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

Встроенный объект Anki mw.col.db поддерживает следующие методы:

execute() позволяет выполнять операции вставки или обновления. Подставляйте именованные аргументы с помощью знака вопроса ?

mw.col.db.execute("update cards set ivl = ? where id = ?", newIvl, cardId)

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

data = [[newIvl1, cardId1], [newIvl2, cardId2]]
mw.col.db.executemany(same_sql_as_above, data)

scalar() возвращает единственное значение:

showInfo("card count: %d" % mw.col.db.scalar("select count() from cards"))

list() возвращает список, состоящий из значений первой колонки в результатах запроса:

ids = mw.col.db.list("select id from cards limit 3")

all() возвращает список строк, в котором каждая строка представляет собой список:

ids_and_ivl = mw.col.db.all("select id, ivl from cards")

execute() также может использоваться для перебора результатов запроса без построения промежуточного списка:

for id, ivl in mw.col.db.execute("select id, ivl from cards limit 3"):
    showInfo("card id %d has ivl %d" % (id, ivl))
Осторожно!
Обратите внимание!
Дополнение не должно изменять схему существующих таблиц в базе данных коллекции, потому что это может сломать будущие версии Anki.

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

Если вам надо синхронизировать данные между устройствами, то небольшое количество опций (параметров) можно хранить в mw.col.conf, но, пожалуйста, избегайте помещать туда большие объёмы данных, потому что этот объект копируется при каждой синхронизации.

Подключения (Hooks)

Подключения были придуманы для облегчения написания дополнений.

Они бывают двух типов:
  • собственно подключения (hooks), которые получают аргументы, но ничего не возвращают,

  • и фильтры, которые получают значение и возвращают его (скорее всего, изменённым).

В качестве простого примера можно привести обработку приставучих карточек (leech). Когда планировщик anki/sched.py обнаруживает пиявку, он вызывает обработчик:

runHook("leech", card)

Если вы хотите выполнить какое-то особое действие при обнаружении такой карточки-вымогателя, например, перенести её в колоду Трудности перевода, то вы можете сделать это с помощью следующего кода:

from anki.hooks import addHook
from aqt import mw

def onLeech(card):
    # допускается изменять без .flush(), планировщик сделает это за нас
    card.did = mw.col.decks.id(u"Трудности перевода")
    # если карточка в фильтрованной колоде зубрёжки,
    # мы возвращаем её в постоянную колоду
    # и назначаем время следующего показа,
    # каким оно было у карточки в этой колоде
    card.odid = 0
    if card.odue:
        card.due = card.odue
        card.odue = 0

addHook("leech", onLeech)

Пример использования фильтра можно найти в aqt/editor.py

Редактор вызывает фильтр editFocusLost каждый раз, когда поле теряет фокус, таким образом, применяя его, дополнение может применять свои изменения к записи:

if runFilter(
    "editFocusLost", False, self.note, self.currentField):
    # something updated the note; schedule reload
    def onUpdate():
        self.loadNote()
        self.checkValid()
    self.mw.progress.timer(100, onUpdate, False)
Каждый фильтр в этом примере получает три аргумента:
  • флажок изменений,

  • запись,

  • поле.

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

Дополнение Japanese Support (поддержка японского языка) использует такое подключение для автоматического создания одного поля из другого. Вот слегка упрощенная версия этого дополнения:

def onFocusLost(flag, n, fidx):
    from aqt import mw
    # japanese model?
    if "japanese" not in n.model()['name'].lower():
        return flag
    # have src and dst fields?
    for c, name in enumerate(mw.col.models.fieldNames(n.model())):
        for f in srcFields:
            if name == f:
                src = f
                srcIdx = c
        for f in dstFields:
            if name == f:
                dst = f
    if not src or not dst:
        return flag
    # dst field already filled?
    if n[dst]:
        return flag
    # event coming from src field?
    if fidx != srcIdx:
        return flag
    # grab source text
    srcTxt = mw.col.media.strip(n[src])
    if not srcTxt:
        return flag
    # update field
    try:
        n[dst] = mecab.reading(srcTxt)
    except Exception, e:
        mecab = None
        raise
    return True

addHook('editFocusLost', onFocusLost)

Первый аргумент — это флаг внесения изменений, он должен быть возвращён. В данном случае это флаг, но вообще это может быть любой другой объект.

Например, в anki/collection.py _renderQA() вызывает фильтр mungeQA, который содержит сгенерированный HTML для лицевой и оборотной сторон карточек.

latex.py использует этот фильтр, чтобы превращать теги LaTeX в картинки.

В Anki 2.1 появился крюк (a hook)

setupEditorButtons для добавления кнопок в редактор.
Он может использоваться как-то так:

from aqt.utils import showInfo
from anki.hooks import addHook

# cross out the currently selected text
def onStrike(editor):
    editor.web.eval("wrap('<del>', '</del>');")

def addMyButton(buttons, editor):
    editor._links['strike'] = onStrike
    return buttons + [editor._addButton(
        "iconname", # "/full/path/to/icon.png",
        "strike", # link name
        "tooltip")]

addHook("setupEditorButtons", addMyButton)

Обезьяний патч и метод обёртки

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

В aqt/editor.py есть функция setupButtons(), которая создаёт кнопочки типа полужирный, курсив и тому подобные, которые вы можете видеть в окне редактора. Давайте представим, что вы хотите в своём дополнении добавить ещё одну кнопочку.

Предупреждение
Предупреждение:
Anki 2.1 больше не использует setupButtons()
Этот код ниже всё ещё может быть достаточно полезен для понимания того, как работает обезьяний патч, но по добавлению своих кнопочек в окно редактирования записей, пожалуйста, смотрите о крючке (хук, зацепка) setupEditorButtons
в предыдущем подразделе.

Простейший путь — это скопипастить исходный код функции и добавить свой текст ему в хвост, затем переписать оригинал, типа того:

from aqt.editor import Editor

def mySetupButtons(self):
    <copy & pasted code from original>
    <custom add-on code>

Editor.setupButtons = mySetupButtons

Однако это достаточно хрупкий подход, как только появится очередная версия Anki в будущем, вам придётся повторить свои действия по обновлению дополнения. Лучшим подходом будет сохранить оригинальный код и просто вызвать его в своей настраиваемой версии:

from aqt.editor import Editor

def mySetupButtons(self):
    origSetupButtons(self)
    <custom add-on code>

origSetupButtons = Editor.setupButtons
Editor.setupButtons = mySetupButtons

Поскольку это часто выполняемая операция, Anki предоставляет функцию под названием wrap() которая делает этот финт ушами немного более удобным. Вот реальный пример:

from anki.hooks import wrap
from aqt.editor import Editor
from aqt.utils import showInfo

def buttonPressed(self):
    showInfo("pressed " + `self`)

def mySetupButtons(self):
    # - size=False tells Anki not to use a small button
    # - the lambda is necessary to pass the editor instance to the
    #   callback, as we're passing in a function rather than a bound
    #   method
    self._addButton("mybutton", lambda s=self: buttonPressed(self),
                    text="PressMe", size=False)

Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons)

По умолчанию, wrap() выполняет ваш код после исходного кода. Указание третьим аргументом слова before меняет это поведение. Если необходимо выполнить код как до, так и после оригинальной версии, то вы можете поступить так:

from anki.hooks import wrap
from aqt.editor import Editor

def mySetupButtons(self, _old):
    <before code>
    ret = _old(self)
    <after code>
    return ret

Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons, "around")

Если вам необходимо изменить код какой-нибудь функции вместо того, чтобы выполнить свой код перед или после неё, то это хороший аргумент за добавление новой функциональности в исходный код. В этой ситуации, пожалуйста, напишите сообщение на сайт техподдержки anki.tenderapp.com/ и попросите, чтобы новая фишка была добавлена.

Qt

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

Одну конкретную вещь следует иметь ввиду, что Python очищает объекты, как только они перестают использоваться, поэтому если вы сделаете что-то навроде этого:

def myfunc():
    widget = QWidget()
    widget.show()

…​то виджет исчезнет сразу по выходу из функции.

Подсказка Чтобы этого не происходило, присваивайте виджеты самого верхнего уровня
существующему объекту, типа:
def myfunc():
    mw.myWidget = widget = QWidget()
    widget.show()

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

Стандартные модули

Anki поставляется только со стандартными модулями, необходимыми для запуска программы — полная версия Python не включается. По этой причине, если вам нужен ещё какой-то стандартный модуль, который не подключён к Anki, вам потребуется самостоятельно привязать его к своему дополнению.

Это относится только к чисто питоновским модулям — модули, которым требуются расширения C, такие как numpy, встретят гораздо больше трудностей при упаковке, поскольку их придётся компилировать отдельно для каждой ОС, которая поддерживается Anki . Если вы делаете что-то сложное, вам будет гораздо проще заставить своих пользователей устанавливать отдельную копию Python.

Конфигурирование

Если вы подключаете файл config.json со словарём JSON внутри, Anki позволит пользователям редактировать его из окна дополнений.

Простейший пример такого файла config.json:

{"myvar": 5}

Вы можете создать описание настроек в отдельном файле config.md в формате markdown:

This is documentation for this add-on's configuration, in *markdown* format.

Как обратиться к настройкам в тексте вашего дополнения:

from aqt import mw
config = mw.addonManager.getConfig(__name__)
print("var is", config['myvar'])

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

Если вы изменяете значение существующих ключей в config.json то пользователи, которые изменили значение этих ключей в своей конфигурации, будут продолжать использовать свои изменённые значения до тех пор, пока не будут использовать кнопку Восстановить умолчания ("restore defaults").

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

mw.addonManager.writeConfig(__name__, config)
Примечание Если файл config.json не существует, getConfig() вернёт None — даже если вы использовали writeConfig()

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

mw.addonManager.setConfigAction(__name__, myOptionsFunc)

Avoid key names starting with an underscore - they are reserved for future use by Anki.

User Files

When your add-on needs configuration data other than simple keys and values, it can use a special folder called user_files in the root of your add-on’s folder. Any files placed in this folder will be preserved when the add-on is upgraded. All other files in the add-on folder are removed on upgrade.

To ensure the user_files folder is created for the user, you can put a README.txt or similar file inside it before zipping up your add-on.

When Anki upgrades an add-on, it will ignore any files in the .zip that already exist in the user_files folder.

JavaScript при просмотре

Javascript in the question and answer

(coming in 2.1.0beta16)

Anki provides a hook to modify the question and answer HTML before it is displayed in the review screen, preview dialog, and card layout screen. This can be useful for adding Javascript to the card.

An example:

from anki.hooks import addHook
def prepare(html, card, context):
    return html + """
<script>
document.body.style.background = "blue";
</script>"""
addHook('prepareQA', prepare)

The hook takes three arguments: the HTML of the question or answer, the current card object (so you can limit your add-on to specific note types for example), and a string representing the context the hook is running in.

Make sure you return the modified HTML.

Context is one of: "reviewQuestion", "reviewAnswer", "clayoutQuestion", "clayoutAnswer", "previewQuestion" or "previewAnswer".

Примечание The answer preview in the card layout screen, and the previewer set to "show both sides" will only use the "Answer" context. This means Javascript you append on the back side of the card should not depend on Javascript that is only added on the front.

Because Anki fades the previous text out before revealing the new text,

  • onUpdateHook сработает после того, как новая карточка была помещена в DOM, но до того, как она будет показана.

  • onShownHook сработает после того, как новая карточка была показана.

Зацепки сбрасываются каждый раз, как вопрос или ответ показан.

Отладка

Если ваш код выбрасывает исключение, оно будет перехвачено стандартным обработчиком исключений в Anki (который хватает вообще всё, что пишется на stderr).

Если вам надо вывести какую-либо информацию в отладочных целях,
вы можете использовать функцию aqt.utils.showInfo()
либо напрямую выдать запись на stderr через sys.stderr.write("text\n")

Anki also includes a REPL. From within the program, press the shortcut key and a window will open up. You can enter expressions or statements into the top area, and then press ctrl+return/command+return to evaluate them. An example session follows:
— Damien Elmes
Writing Anki 2.0 Add-ons
В Anki также включена REPL (Read-eval-print loop)

простая интерактивная среда программирования (вики),
проще говоря — отладочная консоль.

Для её запуска достаточно нажать Ctrl+Shift+;  
(на Маках Command+Shift+;)

возможно потребуется нажать Ctrl+: а на Маках Command+:  
это зависит от раскладки вашей клавиатуры,
точнее, от того, как на ней расположены

  • : двоеточие colon

  • и ; точка с запятой semicolon

Если они находятся на одной клавише и двоеточие выше точки с запятой,

(а это касается и обычной кириллицы) то есть для того, чтобы набрать двоеточие,
вам приходится сдвигать точку с запятой, в смысле нажимать Shift+;  

то вам для активации отладочной консоли в Anki

придётся использовать именно Ctrl+Shift+;   а не Ctrl+:
на Маках Command+Shift+;
  для появления всплывающего окна консоли.

Вы можете использовать простейшее дополнение,

чтобы создать команду меню для вызова REPL
и через эту команду назначить ещё одну,
удобную именно для вас комбинацию горячих клавиш:

# coding: utf8
from __future__ import unicode_literals

from aqt import mw
from aqt.qt import *

import anki.lang
lang = anki.lang.getLang()

def REPL():
    mw.onDebug()

action = QAction(
    "&Отладка REPL Debug console" if lang == "ru" else
    'R&EPL Debug console', mw)
action.setShortcut(QKeySequence("Ctrl+Shift+D"))
action.triggered.connect(REPL)
mw.form.menuTools.insertAction(mw.form.actionNoteTypes, action)
# mw.form.menuTools.insertSeparator(mw.form.actionNoteTypes)

Также вызовет консоль отладки последовательность
подчёркнутых клавиш меню: Alt+И, О
при русском языке ввода с клавиатуры и русском языке в Anki,
Alt+T, D для английского интерфейса Anki
и D в меню Tools/Инструменты для всех остальных языков.

Последовательность Alt+И, О набирается так:

сначала зажимается Alt, затем, не отпуская Alt, нажимается И.
Далее отпускаются обе клавиши и жмётся О.
Буква О, а не цифра ноль 0.

прим. перев.

В верхней области вы можете вводить выражения или команды,
после нажатия Ctrl+Enter (или Command+Enter)
в нижней области будет показан результат их выполнения.

Например:
>>> mw
<no output>

>>> print(mw)
<aqt.main.aqt object at 0x10c0ddc20>

>>> invalidName
Traceback (most recent call last):
  File "/Users/dae/Lib/anki/qt/aqt/main.py", line 933, in onDebugRet
    exec text
  File "<string>", line 1, in <module>
NameError: name 'invalidName' is not defined

>>> a = [a for a in dir(mw.form) if a.startswith("action")]
... print(a)
... print()
... pp(a)
['actionAbout', 'actionCheckMediaDatabase', ...]

['actionAbout',
 'actionCheckMediaDatabase',
 'actionDocumentation',
 'actionDonate',
 ...]

>>> pp(mw.reviewer.card)
<anki.cards.Card object at 0x112181150>

>>> pp(card()) # shortcut for mw.reviewer.card.__dict__
{'_note': <anki.notes.Note object at 0x11221da90>,
 '_qa': [...]
 'col': <anki.collection._Collection object at 0x1122415d0>,
 'data': u'',
 'did': 1,
 'due': -1,
 'factor': 2350,
 'flags': 0,
 'id': 1307820012852L,
 [...]
}

>>> pp(bcard()) # shortcut for selected card in browser
<as above>
Обратите внимание!

Вы должны явно выводить на печать значение выражения, чтобы увидеть, какое значение оно возвращает.

Anki подключает к консоли функцию pp() pretty print (красивая печать), чтобы можно было быстро разобраться в дампах (распечатках) объектов.

Сочетание клавиш Ctrl+Shift+Enter обёртывает весь текст верхней области в pp( и ) и качественно печатает результат выполнения набранного текста.

Если вы на линухе или гоняете Anki из сырцов, то возможно вести отладку ваших скриптов через pdb Просто поместите следующую строчку куда-либо в свой код, и когда Anki доберётся до этой точки, она выкинет в отладчик на терминале:

from aqt.qt import debug; debug()

Как альтернативу можно выставить переменную окружения DEBUG=1
и тогда исключительные ситуации будут попадать в отладчик.

Узнать Больше

Исходный код Anki доступен на github.com/dae/.
Объект collection определён в anki-модуле collection.py

Другие полезные файлы, которые стоит посмотреть:
  • cards.py

  • notes.py

  • sched.py

  • models.py

  • decks.py

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

Большая часть элементов графического интерфейса определена в .ui-файлах. Вы можете использовать программу Qt Designer для того, чтобы просмотреть их.

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

 

Публикация дополнений

AnkiWeb ожидает .zip-файл, содержащий модули дополнения без указания имени папки.

Для примера, если ваше дополнение представляет из себя:

addons21/myaddon/__init__.py
addons21/myaddon/my.data

то zip-файл должен содержать лишь:

Вот так правильно!
__init__.py
my.data

Ежели же вы включите имя папки в zip-архив, то AnkiWeb НЕ примет ваш zip-файл:

Не делайте так!
myaddon/__init__.py
myaddon/my.data

Имя у вашего zip-файла при этом может быть любое.

Важно! Питон автоматически создаёт папки pycache при выполнении вашего дополнения. Пожалуйста, убедитесь, что вы удалили их прежде создания zip-файла, иначе сайт AnkiWeb не примет от вас архив, содежащий папочки pycache

Вы можете выгрузить .zip-файл, который вы создали, на ankiweb.net/shared/addons/

Перенос дополнений от Anki 2.0

Python 3

Anki 2.1.x для работы требуется Python версии 3.6 или новее.

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

2to3-3.6 --output-dir=aqt3 -W -n aqt
mv aqt aqt-old
mv aqt3 aqt

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

Qt5 / PyQt5

Синтаксис для соединения сигналов и слотов изменился в PyQt5.

Однако, недавние версии PyQt4 поддерживают новый синтаксис столь же хорошо, так что одно и то же написание может использоваться как для 2.0, так и для 2.1

Больше информации доступно на pyqt.sourceforge.net/Docs/PyQt4/new_style_signals_slots.html

Один автор дополнений сообщает, что следующая тулза была полезной для автоматической конвертации кода: github.com/rferrazz/pyqt4topyqt5

Модули Qt теперь находятся в PyQt5 вместо PyQt4. Вы можете делать условный импорт, но намного проще импортировать с помощью Anki:

from aqt.qt import *

В этом случае импортируются все объекты Qt вроде QDialog без необходимости указания специфической версии Qt.

Папка для дополнения обязательна!

Примечание Даже дополнениям, состоящим из единственного .py-файла,
всё равно требуется отдельная папка!

Каждое дополнение теперь хранится в своей собственной директории. Если раньше дополнение называлось просто demo.py то теперь требуется создать каталог demo с файликом init.py внутри.

Если вас не заботит совместимость с 2.0, то вы можете просто переименовать demo.py в demo/init.py

Если вы планируете поддерживать 2.0 в одном и том же файле, то вы можете скопировать оригинальный файл в папку (demo.pydemo/demo.py), а затем импортировать его, используя относительный путь, просто добавив в файл demo/init.py следующий текст:

from . import demo

Папка нуждается в архивировании в .zip-файл при выгрузке на AnkiWeb. Больше информации об этом смотрите в разделе Публикация дополнений.

Папка удаляется при обновлении!

Важно!
Когда дополнение обновляется, все файлы в папке дополнения удаляются.
Единственное исключение — специальная подпапка user_files.

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

Одновременная поддержка 2.0 и 2.1

Большая часть кода Python 3 прекрасно будет бегать и под Python 3, так что вполне возможно обновить ваши дополнения таким образом, что они будут гоняться одновременно как под Anki 2.0, так и под Анки 2.1 Стоит ли так упираться — зависит от тех изменений, которые вы намереваетесь сделать.

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

Наиболее трудная часть — переход от неподдерживаемого более QtWebKit к QtWebEngine. Если вы делаете любую нетривиальную работу с webview, некоторая деятельность потребуется для переноса вашего кода под Anki 2.1, и вы можете обнаружить, что это достаточно трудно — поддерживать одновременно обе версии версии Anki в одном коде.

Если вы находите, что ваше дополнение выполняется и так, без дальнейших изменений, либо требуются минимальные изменения, то проще всего будет добавить несколько ветвлений if в код и загрузить на AnkiWeb один и тот же файл как для версии 2.0, так и для версии 2.1

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

Изменения Webview

Пакет Qt5 отказался от поддержки WebKit в пользу WebEngine, базирующемся на Chromium, так что весь интерфейс Anki теперь работает через WebEngine. Что следует знать об этом:

  • Теперь вы можете вести отладку, используя внешний браузер Chrome, просто установив переменную окружения QTWEBENGINE_REMOTE_DEBUGGING в значение 8080 до запуска Anki, а затем лишь перейти по адресу localhost:8080 в Chrome.

  • WebEngine использует различные методы общения с Python для получения обратной связи. Обёртка AnkiWebView() предоставляет функцию pycmd(str) в JavaScript, которая вызывает onBridgeCmd(str) метод ankiwebview. Различные части пользовательского интерфейса Anki вроде reviewer.py и deckbrowser.py потребовали измения для того, чтобы использовать это.

  • JavaScript выполняется асинхронно, поэтому если вам надо получить результат его выполнения, то используйте ankiwebview-функцию evalWithCallback()

  • Как результат такого асинхронного поведения editor.saveNow() теперь предполагает обратную связь. Если ваше дополнение выполняет действия в Обзоре, похоже, что вам потребуется сначала выполнить editor.saveNow() и лишь затем остальную часть вашего кода в ответку. Вызовы .onSearch() потребуется заменить на .search()/.onSearchActivated() Для примера смотрите .deleteNotes()

  • Различные действия, которые раньше поддерживались WebKit навроде setScrollPosition() теперь нуждаются в реализации через JavaScript.

  • Действия над странице по типу mw.web.triggerPageAction(QWebEnginePage.Copy) теперь также асинхронны и нуждаются в задержке или переписывании на javascript

  • WebEngine не предоставляет keyPressEvent() как это делал WebKit, поэтому код, который захватывается горячими клавишами, не указанными в главном меню, также изменяется. Пример можно подсмотреть в reviewer.py вызывающем setStateShortcuts()

Изменения при просмотре

Anki 2.1.x постепенно скрывает предыдущую карточку, прежде чем показать следующую, так что следующая карточка не будет доступна через DOM в момент работы зацепки по showQuestion

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

Конфигурирование дополнений

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

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

Если же вам требуется поддержка подходов к проблеме обоих версий (например, если ваше дополнение намерено поддерживать одновременно обе версии Anki), то вы можете использовать код типа такого:

if getattr(mw.addonsManager, "getConfig", None):
    config = mw.addonManager.getConfig(__name__)
else:
    config = dict(optionA=123, optionB=456)

 

 

Anki 2.0 Writing Add-ons

Подсказка Вы можете установить себе дополнение 2086742987
и читать мануал локально с диска через меню Помощь

Введение

Другие версии

Этот документ описывает написание дополнений для Anki 2.0.x. Указания по написанию дополнений для бета-версии Anki 2.1.x смотри в оригинале и в переводе

Беглый обзор

Anki 2.0 написана на языке программирования Python. Если вы ещё не знакомы с этим языком, пожалуйста, прочитайте Руководство по языку (на англ.), прежде чем продолжить чтение этого документа.

Поскольку Python — это интерпретируемый (динамический) язык, дополнения к Anki могут быть очень мощными, то есть не просто расширять возможности программы, но они могут также модифицировать совершенно разные ее аспекты, такие как изменение способа планирования работы (расписания просмотра карточек), изменения пользовательского интерфейса и так далее.

Никакой специальной среды для разработки дополнений не требуется. Всё, что вам нужно — это текстовый редактор.

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

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

Anki состоит из двух частей

anki

anki содержит весь код внутренней обработки — ведение коллекций, записей, колод карточек, накопление статистики ответов и т. д. Доступ ко всей этой информации может осуществляться как через пользовательский интерфейс Anki, так и через программы, работающие в пакетном режиме (не использующие графический интерфейс пользователя, а просто запускаемые через консоль (командную строку OC)).

aqt

aqt содержит весь код пользовательского интерфейса Anki, который построен на пакете PyQt (сборка кроссплатформенного пакета QT для Python). PyQt достаточно точно следует API QT, поэтому можете пользоваться Qt документацией, когда захотите узнать, как пользоваться той или иной компонентой пользовательского интерфейса (GUI).

  • Anki 2.0.x юзает Qt 4.8

  • Anki 2.1.x пользует Qt 5.9

При запуске Anki выполняет каждый файл с расширением .py
который только найдёт в каталоге Ваши документы \Anki\addons

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

Простейшее дополнение

Сохраните следующий текст в файле test.py в вашем каталоге дополнений
(обычно это папка C:\Users\ юзер \Documents\Anki\addons):

# import the main window object (mw) from aqt
from aqt import mw
# import the "show info" tool from utils.py
from aqt.utils import showInfo
# import all of the Qt GUI library
from aqt.qt import *

# We're going to add a menu item below. First we want to create a function to
# be called when the menu item is activated.

def testFunction():
    # get the number of cards in the current collection, which is stored in
    # the main window
    cardCount = mw.col.cardCount()
    # show a message box
    showInfo("Card count: %d" % cardCount)

# create a new menu item, "test"
action = QAction("test", mw)
# set it to call testFunction when it's clicked
action.triggered.connect(testFunction)
# and add it to the tools menu
mw.form.menuTools.addAction(action)

Перезапустите Anki, и в меню ИнструментыДополнения вы увидите новый пункт test
Клик по этой новой строке меню вызывает окно, показывающее общее количество карточек в коллекции.

Если вы допустите ошибку в коде дополнения, Anki при запуске покажет сообщение об ошибке, указывающее, где находится место, которое вызывало проблемы.

Подробнее

Коллекция

Все действия над коллекцией доступны через mw.col.

Дальше идут некоторые основные примеры того, что вы можете делать.

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

Очередная карточка к изучению

card = mw.col.sched.getCard()
if not card:
    # в текущей колоде сегодня больше нет карточек, предлагаемых к изучению

Ответ на карточку

mw.col.sched.answerCard(card, ease)

Редактирование записи

Добавляется new через пробел в конец каждого поля

note = card.note()
for (name, value) in note.items():
    note[name] = value + " new"
note.flush()

Идентификаторы карточек

Получить идентификаторы карточек для записей, которые содержат метку x

ids = mw.col.findCards("tag:x")

Вопрос и ответ каждой карточки

for id in ids:
    card = mw.col.getCard(id)
    question = card.q()
    answer = card.a()

Чистка расписания

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

mw.reset()

Импорт записей

из текстового файла в колоду карточек

Import a text file into the collection

from anki.importing import TextImporter
file = u"/path/to/text.txt"
# select deck
did = mw.col.decks.id("ImportDeck")
mw.col.decks.select(did)
# set note type for deck
m = mw.col.models.byName("Basic")
deck = mw.col.decks.get(did)
deck['mid'] = m['id']
mw.col.decks.save(deck)
# import into the collection
ti = TextImporter(mw.col, file)
ti.initMapping()
ti.run()

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

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

from anki import Collection
col = Collection("/path/to/collection.anki2")

Если вы делаете любые изменения в коллекции вне Anki, то вы должны быть уверены, что выполнили col.close() в конце работы, иначе все изменения будут потеряны.

Работа с базой данных

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

Встроенный объект Anki mw.col.db поддерживает следующие методы:

execute() позволяет выполнять операции вставки или обновления. Подставляйте именованные аргументы с помощью знака вопроса ?

mw.col.db.execute("update cards set ivl = ? where id = ?", newIvl, cardId)

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

data = [[newIvl1, cardId1], [newIvl2, cardId2]]
mw.col.db.executemany(same_sql_as_above, data)

scalar() возвращает единственное значение:

showInfo("card count: %d" % mw.col.db.scalar("select count() from cards"))

list() возвращает список, состоящий из значений первой колонки в результатах запроса:

ids = mw.col.db.list("select id from cards limit 3")

all() возвращает список строк, в котором каждая строка представляет собой список:

ids_and_ivl = mw.col.db.all("select id, ivl from cards")

execute() также может использоваться для перебора результатов запроса без построения промежуточного списка:

for id, ivl in mw.col.db.execute("select id, ivl from cards limit 3"):
    showInfo("card id %d has ivl %d" % (id, ivl))
Осторожно!
Обратите внимание!
Дополнение не должно изменять таблицы в коллекции, потому что эти изменения могут начать конфликтовать в будущем с новыми версиями Anki.

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

Небольшое количество опций (параметров) можно хранить в mw.col.conf, но, пожалуйста, избегайте помещать туда большие объёмы данных, потому что этот объект копируется при каждой синхронизации.

Подключения (Hooks)

Подключения были придуманы для облегчения написания дополнений.

Они бывают двух типов:
  • собственно подключения (hooks), которые получают аргументы, но ничего не возвращают,

  • и фильтры, которые получают значение и возвращают его (скорее всего, изменённым).

В качестве простого примера можно привести обработку приставучих карточек (leech). Когда планировщик anki/sched.py обнаруживает пиявку, он вызывает обработчик:

runHook("leech", card)

Если вы хотите выполнить какое-то особое действие при обнаружении такой карточки-вымогателя, например, перенести её в колоду Трудности перевода, то вы можете сделать это с помощью следующего кода:

from anki.hooks import addHook
from aqt import mw

def onLeech(card):
    # допускается изменять без .flush(), планировщик сделает это за нас
    card.did = mw.col.decks.id(u"Трудности перевода")
    # если карточка в фильтрованной колоде зубрёжки,
    # мы возвращаем её в постоянную колоду
    # и назначаем время следующего показа,
    # каким оно было у карточки в этой колоде
    card.odid = 0
    if card.odue:
        card.due = card.odue
        card.odue = 0

addHook("leech", onLeech)

Пример использования фильтра можно найти в aqt/editor.py

Редактор вызывает фильтр editFocusLost каждый раз, когда поле теряет фокус, таким образом, применяя его, дополнение может применять свои изменения к записи:

if runFilter(
    "editFocusLost", False, self.note, self.currentField):
    # something updated the note; schedule reload
    def onUpdate():
        self.loadNote()
        self.checkValid()
    self.mw.progress.timer(100, onUpdate, False)
Каждый фильтр в этом примере получает три аргумента:
  • флажок изменений,

  • запись,

  • поле.

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

Дополнение Japanese Support (поддержка японского языка) использует такое подключение для автоматического создания одного поля из другого. Вот слегка упрощенная версия этого дополнения:

def onFocusLost(flag, n, fidx):
    from aqt import mw
    # japanese model?
    if "japanese" not in n.model()['name'].lower():
        return flag
    # have src and dst fields?
    for c, name in enumerate(mw.col.models.fieldNames(n.model())):
        for f in srcFields:
            if name == f:
                src = f
                srcIdx = c
        for f in dstFields:
            if name == f:
                dst = f
    if not src or not dst:
        return flag
    # dst field already filled?
    if n[dst]:
        return flag
    # event coming from src field?
    if fidx != srcIdx:
        return flag
    # grab source text
    srcTxt = mw.col.media.strip(n[src])
    if not srcTxt:
        return flag
    # update field
    try:
        n[dst] = mecab.reading(srcTxt)
    except Exception, e:
        mecab = None
        raise
    return True

addHook('editFocusLost', onFocusLost)

Первый аргумент — это флаг внесения изменений, он должен быть возвращён. В данном случае это флаг, но вообще это может быть любой другой объект.

Например, в anki/collection.py _renderQA() вызывает фильтр mungeQA, который содержит сгенерированный HTML для лицевой и оборотной сторон карточек.

latex.py использует этот фильтр, чтобы превращать теги LaTeX в картинки.

В Anki 2.1 появился крюк (a hook)

setupEditorButtons для добавления кнопок в редактор.
Он может использоваться как-то так:

from aqt.utils import showInfo
from anki.hooks import addHook

# cross out the currently selected text
def onStrike(editor):
    editor.web.eval("wrap('<del>', '</del>');")

def addMyButton(buttons, editor):
    editor._links['strike'] = onStrike
    return buttons + [editor._addButton(
        "iconname", # "/full/path/to/icon.png",
        "strike", # link name
        "tooltip")]

addHook("setupEditorButtons", addMyButton)

Обезьяний патч и метод обёртки

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

В aqt/editor.py есть функция setupButtons(), которая создаёт кнопочки типа полужирный, курсив и тому подобные, которые вы можете видеть в окне редактора. Давайте представим, что вы хотите в своём дополнении добавить ещё одну кнопочку.

Предупреждение
Предупреждение:
Anki 2.1 больше не использует setupButtons()
Этот код ниже всё ещё может быть достаточно полезен для понимания того, как работает обезьяний патч, но по добавлению своих кнопочек в окно редактирования записей, пожалуйста, смотрите о крючке (хук, зацепка) setupEditorButtons
в предыдущем подразделе.

Простейший путь — это скопипастить исходный код функции и добавить свой текст ему в хвост, затем переписать оригинал, типа того:

from aqt.editor import Editor

def mySetupButtons(self):
    <copy & pasted code from original>
    <custom add-on code>

Editor.setupButtons = mySetupButtons

Однако это достаточно хрупкий подход, как только появится очередная версия Anki в будущем, вам придётся повторить свои действия по обновлению дополнения. Лучшим подходом будет сохранить оригинальный код и просто вызвать его в своей настраиваемой версии:

from aqt.editor import Editor

def mySetupButtons(self):
    origSetupButtons(self)
    <custom add-on code>

origSetupButtons = Editor.setupButtons
Editor.setupButtons = mySetupButtons

Поскольку это часто выполняемая операция, Anki предоставляет функцию под названием wrap() которая делает этот финт ушами немного более удобным. Вот реальный пример:

from anki.hooks import wrap
from aqt.editor import Editor
from aqt.utils import showInfo

def buttonPressed(self):
    showInfo("pressed " + `self`)

def mySetupButtons(self):
    # - size=False tells Anki not to use a small button
    # - the lambda is necessary to pass the editor instance to the
    #   callback, as we're passing in a function rather than a bound
    #   method
    self._addButton("mybutton", lambda s=self: buttonPressed(self),
                    text="PressMe", size=False)

Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons)

По умолчанию, wrap() выполняет ваш код после исходного кода. Указание третьим аргументом слова before меняет это поведение. Если необходимо выполнить код как до, так и после оригинальной версии, то вы можете поступить так:

from anki.hooks import wrap
from aqt.editor import Editor

def mySetupButtons(self, _old):
    <before code>
    ret = _old(self)
    <after code>
    return ret

Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons, "around")

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

Qt

Как уже говорилось, документация по Qt 4.8 и 5.9 имеет неоценимое значение для обучения созданию виджетов графического пользовательского интерфейса.

Одну конкретную вещь следует иметь ввиду, что Python очищает объекты, как только они перестают использоваться, поэтому если вы сделаете что-то навроде этого:

def myfunc():
    widget = QWidget()
    widget.show()

…​то виджет исчезнет сразу по выходу из функции.

Подсказка Чтобы этого не происходило, присваивайте виджеты самого верхнего уровня
существующему объекту, типа:
def myfunc():
    mw.myWidget = widget = QWidget()
    widget.show()

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

Стандартные модули

Anki поставляется только со стандартными модулями, необходимыми для запуска программы — полная версия Python не включается. По этой причине, если вам нужен ещё какой-то стандартный модуль, который не подключён к Anki, вам потребуется самостоятельно привязать его к своему дополнению.

Отладка

Если ваш код выбрасывает исключение, оно будет перехвачено стандартным обработчиком исключений в Anki (который хватает вообще всё, что пишется на stderr).

Если вам надо вывести какую-либо информацию в отладочных целях,
вы можете использовать функцию aqt.utils.showInfo()
либо напрямую выдать запись на stderr через sys.stderr.write("text\n")

Anki also includes a REPL. From within the program, press the shortcut key and a window will open up. You can enter expressions or statements into the top area, and then press ctrl+return/command+return to evaluate them. An example session follows:
— Damien Elmes
Writing Anki 2.0 Add-ons
В Anki также включена REPL (Read-eval-print loop)

простая интерактивная среда программирования (вики),
проще говоря — отладочная консоль.

Для её запуска достаточно нажать Ctrl+Shift+;  
(на Маках Command+Shift+;)

возможно потребуется нажать Ctrl+: а на Маках Command+:  
это зависит от раскладки вашей клавиатуры,
точнее, от того, как на ней расположены

  • : двоеточие colon

  • и ; точка с запятой semicolon

Если они находятся на одной клавише и двоеточие выше точки с запятой,

(а это касается и обычной кириллицы) то есть для того, чтобы набрать двоеточие,
вам приходится сдвигать точку с запятой, в смысле нажимать Shift+;  

то вам для активации отладочной консоли в Anki

придётся использовать именно Ctrl+Shift+;   а не Ctrl+:
на Маках Command+Shift+;
  для появления всплывающего окна консоли.

Вы можете использовать простейшее дополнение,

чтобы создать команду меню для вызова REPL
и через эту команду назначить ещё одну,
удобную именно для вас комбинацию горячих клавиш:

# coding: utf8
from __future__ import unicode_literals

from aqt import mw
from aqt.qt import *

import anki.lang
lang = anki.lang.getLang()

def REPL():
    mw.onDebug()

action = QAction(
    "&Отладка REPL Debug console" if lang == "ru" else
    'R&EPL Debug console', mw)
action.setShortcut(QKeySequence("Ctrl+Shift+D"))
action.triggered.connect(REPL)
mw.form.menuTools.insertAction(mw.form.actionNoteTypes, action)
# mw.form.menuTools.insertSeparator(mw.form.actionNoteTypes)

Также вызовет консоль отладки последовательность
подчёркнутых клавиш меню: Alt+И, О
при русском языке ввода с клавиатуры и русском языке в Anki,
Alt+T, D для английского интерфейса Anki
и D в меню Tools/Инструменты для всех остальных языков.

Последовательность Alt+И, О набирается так:

сначала зажимается Alt, затем, не отпуская Alt, нажимается И.
Далее отпускаются обе клавиши и жмётся О.
Буква О, а не цифра ноль 0.

прим. перев.

В верхней области вы можете вводить выражения или команды,
после нажатия Ctrl+Enter (или Command+Enter)
в нижней области будет показан результат их выполнения.

Например:
>>> mw
<no output>

>>> print(mw)
<aqt.main.aqt object at 0x10c0ddc20>

>>> invalidName
Traceback (most recent call last):
  File "/Users/dae/Lib/anki/qt/aqt/main.py", line 933, in onDebugRet
    exec text
  File "<string>", line 1, in <module>
NameError: name 'invalidName' is not defined

>>> a = [a for a in dir(mw.form) if a.startswith("action")]
... print(a)
... print()
... pp(a)
['actionAbout', 'actionCheckMediaDatabase', ...]

['actionAbout',
 'actionCheckMediaDatabase',
 'actionDocumentation',
 'actionDonate',
 ...]

>>> pp(mw.reviewer.card)
<anki.cards.Card object at 0x112181150>

>>> pp(card()) # shortcut for mw.reviewer.card.__dict__
{'_note': <anki.notes.Note object at 0x11221da90>,
 '_qa': [...]
 'col': <anki.collection._Collection object at 0x1122415d0>,
 'data': u'',
 'did': 1,
 'due': -1,
 'factor': 2350,
 'flags': 0,
 'id': 1307820012852L,
 [...]
}

>>> pp(bcard()) # shortcut for selected card in browser
<as above>
Обратите внимание!

Вы должны явно выводить на печать значение выражения, чтобы увидеть, какое значение оно возвращает.

Anki подключает к консоли функцию pp() pretty print (красивая печать), чтобы можно было быстро разобраться в дампах (распечатках) объектов.

Сочетание клавиш Ctrl+Shift+Enter обёртывает весь текст верхней области в pp( и ) и качественно печатает результат выполнения набранного текста.

Если вы на линухе или гоняете Anki из сырцов, то возможно вести отладку ваших скриптов через pdb Просто поместите следующую строчку куда-либо в свой код, и когда Anki доберётся до этой точки, она выкинет в отладчик на терминале:

from aqt.qt import debug; debug()

Как альтернативу можно выставить переменную окружения DEBUG=1
и тогда исключительные ситуации будут попадать в отладчик.

Узнать Больше

Как anki, так и aqt доступны на github.com/dae/.
Объект collection определён в anki-модуле collection.py

Другие полезные файлы, которые стоит посмотреть:
  • cards.py

  • notes.py

  • sched.py

  • models.py

  • decks.py

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

Большая часть элементов графического интерфейса определена в .ui-файлах. Вы можете использовать программу Qt Designer для того, чтобы просмотреть их.

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

Портирование плагинов Анки 1.2

Некоторые из основных изменений, чтобы быть в курсе:

  • Изменения в таблицах:

    • факты теперь записи (facts → notes),

    • reviewHistory → revlog

  • Поля теперь хранятся в таблице записей notes

    • все вместе в одном-единственном поле, которое называется flds

    • разделителем полей является \x1f

  • Больше нет таблицы cardTags

    • Для поиска используйте col.findCards("tag:x note:y card:z")

  • Весь код планировщика теперь в модуле sched.py

    • вся работа с колодами собрана в модуле collection.py

  • Если вы делаете массовые обновления таблицы записей notes

    • и не используете findReplace()

    • то убедитесь, что вызываете col.updateFieldCache()

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

  • Вместо старой системы откатов (Undo) вызывайте mw.checkpoint("Undo Name") для сохранения коллекции, прежде чем начнёте вносить изменения. Когда пользователь отменяет операцию, возврат выполняется к сохранённому состоянию.

  • В целях обеспечения синхронизации изменений, при изменении записей или карточек в базе данных убедитесь, что вы обновили mod и выставили usn в col.usn()

  • Кроме того, при изменений моделей (типов записей) или колод, убедитесь, что вызываете save() в соответствующем менеджере.

  • Если вы устанавливаете таймер, то используйте mw.progress.timer(), чтобы убедиться, что таймер не срабатывает во время выполнения операций с базой данных.

  • Больше нет таблицы stats поскольку её невозможно объединить при синхронизации. Вся статистика сейчас производится из таблицы revlog

Публикация дополнений

Пожалуйста, делитесь своими дополнениями на сайте ankiweb.net/shared/addons/

Для выгрузки на сайт дополнения, состоящего из единственного .py-файла,
достаточно просто отправить через форму сам этот файл.

Для выгрузки дополнения, состоящего из нескольких .py-файлов,
пожалуйста, заархивируйте папку с ними,
а также запускающий их модуль, в единый .zip-файл.

Папка должна быть оформлена как пакет по правилам языка Python.
Запускающий модуль должен просто импортировать этот пакет.

Примерная структура файлов выглядит так:
  japanese/file1.py
  japanese/file2.py
  japanese/__init__.py # can be empty; marks the folder as a package
  japanese/<binary support files>
  jp.py

 


¡иɯʎdʞ ин ʞɐʞ 'ɐнɔɐdʞǝdu qнεиЖ

 

2017-12-05

The content here is distributed under the CC BY-SA license:
creativecommons.org/licenses/by-sa/4.0/   (лицензия по-русски)

.

.