Anki/pylib/anki/notes.py
Damien Elmes b9251290ca run pyupgrade over codebase [python upgrade required]
This adds Python 3.9 and 3.10 typing syntax to files that import
attributions from __future___. Python 3.9 should be able to cope with
the 3.10 syntax, but Python 3.8 will no longer work.

On Windows/Mac, install the latest Python 3.9 version from python.org.
There are currently no orjson wheels for Python 3.10 on Windows/Mac,
which will break the build unless you have Rust installed separately.

On Linux, modern distros should have Python 3.9 available already. If
you're on an older distro, you'll need to build Python from source first.
2021-10-04 15:05:48 +10:00

198 lines
5.8 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pylint: enable=invalid-name
from __future__ import annotations
import copy
from typing import Any, NewType, Sequence
import anki # pylint: disable=unused-import
from anki import hooks, notes_pb2
from anki._legacy import DeprecatedNamesMixin
from anki.consts import MODEL_STD
from anki.models import NotetypeDict, NotetypeId, TemplateDict
from anki.utils import joinFields
DuplicateOrEmptyResult = notes_pb2.NoteFieldsCheckResponse.State
NoteFieldsCheckResult = notes_pb2.NoteFieldsCheckResponse.State
DefaultsForAdding = notes_pb2.DeckAndNotetype
# types
NoteId = NewType("NoteId", int)
class Note(DeprecatedNamesMixin):
# not currently exposed
flags = 0
data = ""
id: NoteId
mid: NotetypeId
def __init__(
self,
col: anki.collection.Collection,
model: NotetypeDict | NotetypeId | None = None,
id: NoteId | None = None,
) -> None:
assert not (model and id)
notetype_id = model["id"] if isinstance(model, dict) else model
self.col = col.weakref()
if id:
# existing note
self.id = id
self.load()
else:
# new note for provided notetype
self._load_from_backend_note(self.col._backend.new_note(notetype_id))
def load(self) -> None:
note = self.col._backend.get_note(self.id)
assert note
self._load_from_backend_note(note)
def _load_from_backend_note(self, note: notes_pb2.Note) -> None:
self.id = NoteId(note.id)
self.guid = note.guid
self.mid = NotetypeId(note.notetype_id)
self.mod = note.mtime_secs
self.usn = note.usn
self.tags = list(note.tags)
self.fields = list(note.fields)
self._fmap = self.col.models.field_map(self.note_type())
def _to_backend_note(self) -> notes_pb2.Note:
hooks.note_will_flush(self)
return notes_pb2.Note(
id=self.id,
guid=self.guid,
notetype_id=self.mid,
mtime_secs=self.mod,
usn=self.usn,
tags=self.tags,
fields=self.fields,
)
def flush(self) -> None:
"""This preserves any current checkpoint.
For an undo entry, use col.update_note() instead."""
assert self.id != 0
self.col._backend.update_notes(
notes=[self._to_backend_note()], skip_undo_entry=True
)
def joined_fields(self) -> str:
return joinFields(self.fields)
def ephemeral_card(
self,
ord: int = 0,
*,
custom_note_type: NotetypeDict = None,
custom_template: TemplateDict = None,
fill_empty: bool = False,
) -> anki.cards.Card:
card = anki.cards.Card(self.col)
card.ord = ord
card.did = anki.decks.DEFAULT_DECK_ID
model = custom_note_type or self.note_type()
template = copy.copy(
custom_template
or (
model["tmpls"][ord] if model["type"] == MODEL_STD else model["tmpls"][0]
)
)
# may differ in cloze case
template["ord"] = card.ord
output = anki.template.TemplateRenderContext.from_card_layout(
self,
card,
notetype=model,
template=template,
fill_empty=fill_empty,
).render()
card.set_render_output(output)
card._note = self
return card
def cards(self) -> list[anki.cards.Card]:
return [self.col.getCard(id) for id in self.card_ids()]
def card_ids(self) -> Sequence[anki.cards.CardId]:
return self.col.card_ids_of_note(self.id)
def note_type(self) -> NotetypeDict | None:
return self.col.models.get(self.mid)
_note_type = property(note_type)
def cloze_numbers_in_fields(self) -> Sequence[int]:
return self.col._backend.cloze_numbers_in_note(self._to_backend_note())
# Dict interface
##################################################
def keys(self) -> list[str]:
return list(self._fmap.keys())
def values(self) -> list[str]:
return self.fields
def items(self) -> list[tuple[str, str]]:
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
def _field_index(self, key: str) -> int:
try:
return self._fmap[key][0]
except Exception as exc:
raise KeyError(key) from exc
def __getitem__(self, key: str) -> str:
return self.fields[self._field_index(key)]
def __setitem__(self, key: str, value: str) -> None:
self.fields[self._field_index(key)] = value
def __contains__(self, key: str) -> bool:
return key in self._fmap
# Tags
##################################################
def has_tag(self, tag: str) -> bool:
return self.col.tags.inList(tag, self.tags)
def remove_tag(self, tag: str) -> None:
rem = []
for tag_ in self.tags:
if tag_.lower() == tag.lower():
rem.append(tag_)
for tag_ in rem:
self.tags.remove(tag_)
def add_tag(self, tag: str) -> None:
"Add tag. Duplicates will be stripped on save."
self.tags.append(tag)
def string_tags(self) -> Any:
return self.col.tags.join(self.col.tags.canonify(self.tags))
def set_tags_from_str(self, tags: str) -> None:
self.tags = self.col.tags.split(tags)
# Unique/duplicate/cloze check
##################################################
def fields_check(self) -> NoteFieldsCheckResult.V:
return self.col._backend.note_fields_check(self._to_backend_note()).state
dupeOrEmpty = duplicate_or_empty = fields_check
Note.register_deprecated_aliases(
delTag=Note.remove_tag, _fieldOrd=Note._field_index, model=Note.note_type
)