Anki 2.0 Writing Add-ons

Anki 2.0 Написание дополнений

 

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

Введение

Оригинал

на английском языке:

Переводы

Беглый обзор

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

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

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

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

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

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

anki

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

aqt

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

При запуске 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))
Caution
Обратите внимание!
Дополнение не должно изменять таблицы в коллекции, потому что эти изменения могут начать конфликтовать в будущем с новыми версиями 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 в картинки.

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

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

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

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

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 имеет неоценимое значения для обучения созданию виджетов графического пользовательского интерфейса.

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

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

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

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

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

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

Отладка

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

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

В Anki также включена REPL (Read-eval-print loop) — простая интерактивная среда программирования (вики), проще говоря — консоль.

Достаточно нажать Ctrl+: или Command+: для появления всплывающего окна консоли. В верхней области вы можете вводить выражения или команды, после нажатия 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

 


 

 

 

.

 

 

  2017-03-22

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

 

.