From 67a39c58cbb88004108e6d82fbb1c97fc7640552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Pokorn=C3=BD=20=28Rai=29?= Date: Sat, 21 Dec 2019 12:02:16 +0100 Subject: [PATCH] Add types for models, templates and field dicts --- anki/collection.py | 7 ++-- anki/latex.py | 12 ++++--- anki/models.py | 83 +++++++++++++++++++++++----------------------- anki/stdmodels.py | 19 ++++++----- anki/types.py | 12 +++++++ anki/utils.py | 8 ++--- 6 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 anki/types.py diff --git a/anki/collection.py b/anki/collection.py index a3fd21096..79a155f6e 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -33,6 +33,7 @@ from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.sound import stripSounds from anki.tags import TagManager +from anki.types import Model, Template from anki.utils import (devMode, fieldChecksum, ids2str, intTime, joinFields, maxID, splitFields, stripHTMLMedia) @@ -354,7 +355,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", avail = self.models.availOrds(model, joinFields(note.fields)) return self._tmplsFromOrds(model, avail) - def _tmplsFromOrds(self, model: Dict[str, Any], avail: List[int]) -> List: + def _tmplsFromOrds(self, model: Model, avail: List[int]) -> List: ok = [] if model['type'] == MODEL_STD: for t in model['tmpls']: @@ -456,7 +457,7 @@ insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""", cards.append(self._newCard(note, template, 1, flush=False, did=did)) return cards - def _newCard(self, note: Note, template: Dict[str, Any], due: int, flush: bool = True, did: None = None) -> anki.cards.Card: + def _newCard(self, note: Note, template: Template, due: int, flush: bool = True, did: None = None) -> anki.cards.Card: "Create a new card." card = anki.cards.Card(self) card.nid = note.id @@ -465,7 +466,7 @@ insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""", # Use template did (deck override) if valid, otherwise did in argument, otherwise model did if not card.did: if template['did'] and str(template['did']) in self.decks.decks: - card.did = template['did'] + card.did = int(template['did']) elif did: card.did = did else: diff --git a/anki/latex.py b/anki/latex.py index 65da0c4e4..86d7f0de0 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Union from anki.hooks import addHook from anki.lang import _ +from anki.types import Model from anki.utils import call, checksum, isMac, namedtmp, stripHTML, tmpdir pngCommands = [ @@ -42,7 +43,8 @@ def stripLatex(text) -> Any: text = text.replace(match.group(), "") return text -def mungeQA(html: str, type: Optional[str], fields: Optional[Dict[str, str]], model: Dict[str, Any], data: Optional[List[Union[int, str]]], col) -> Any: +def mungeQA(html: str, type: Optional[str], fields: Optional[Dict[str, str]], + model: Model, data: Optional[List[Union[int, str]]], col) -> Any: "Convert TEXT with embedded latex tags to image links." for match in regexps['standard'].finditer(html): html = html.replace(match.group(), _imgLink(col, match.group(1), model)) @@ -55,7 +57,7 @@ def mungeQA(html: str, type: Optional[str], fields: Optional[Dict[str, str]], mo "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model)) return html -def _imgLink(col, latex: str, model: Dict[str, Any]) -> Any: +def _imgLink(col, latex: str, model: Model) -> str: "Return an img link for LATEX, creating if necesssary." txt = _latexFromHtml(col, latex) @@ -80,13 +82,13 @@ def _imgLink(col, latex: str, model: Dict[str, Any]) -> Any: else: return link -def _latexFromHtml(col, latex: str) -> Any: +def _latexFromHtml(col, latex: str) -> str: "Convert entities and fix newlines." latex = re.sub("|
", "\n", latex) latex = stripHTML(latex) return latex -def _buildImg(col, latex: str, fname: str, model: Dict[str, Any]) -> Any: +def _buildImg(col, latex: str, fname: str, model: Model) -> Optional[str]: # add header/footer latex = (model["latexPre"] + "\n" + latex + "\n" + @@ -129,7 +131,7 @@ package in the LaTeX header instead.""") % bad return _errMsg(latexCmd[0], texpath) # add to media shutil.copyfile(png, os.path.join(mdir, fname)) - return + return None finally: os.chdir(oldcwd) log.close() diff --git a/anki/models.py b/anki/models.py index 90d757a0b..b6e650d48 100644 --- a/anki/models.py +++ b/anki/models.py @@ -11,6 +11,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union from anki.consts import * from anki.hooks import runHook from anki.lang import _ +from anki.types import Field, Model, Template from anki.utils import checksum, ids2str, intTime, joinFields, splitFields # Models @@ -18,7 +19,7 @@ from anki.utils import checksum, ids2str, intTime, joinFields, splitFields # - careful not to add any lists/dicts/etc here, as they aren't deep copied -defaultModel = { +defaultModel: Model = { 'sortf': 0, 'did': 1, 'latexPre': """\ @@ -46,7 +47,7 @@ defaultModel = { """ } -defaultField: Dict[str, Any] = { +defaultField: Field = { 'name': "", 'ord': None, 'sticky': False, @@ -59,7 +60,7 @@ defaultField: Dict[str, Any] = { 'media': [], } -defaultTemplate = { +defaultTemplate: Template = { 'name': "", 'ord': None, 'qfmt': "", @@ -73,7 +74,7 @@ defaultTemplate = { } class ModelManager: - models: Dict[str, Any] + models: Dict[str, Model] # Saving/loading registry ############################################################# @@ -88,7 +89,7 @@ class ModelManager: self.changed = False self.models = json.loads(json_) - def save(self, m: Optional[Dict[str, Any]] = None, templates: bool = False, updateReqs: bool = True) -> None: + def save(self, m: Optional[Model] = None, templates: bool = False, updateReqs: bool = True) -> None: "Mark M modified if provided, and schedule registry flush." if m and m['id']: m['mod'] = intTime() @@ -125,7 +126,7 @@ class ModelManager: m = self.get(self.col.conf['curModel']) return m or list(self.models.values())[0] - def setCurrent(self, m: Dict[str, Any]) -> None: + def setCurrent(self, m: Model) -> None: self.col.conf['curModel'] = m['id'] self.col.setMod() @@ -148,7 +149,7 @@ class ModelManager: if m['name'] == name: return m - def new(self, name: str) -> Dict[str, Any]: + def new(self, name: str) -> Model: "Create a new model, save it in the registry, and return it." # caller should call save() after modifying m = defaultModel.copy() @@ -160,7 +161,7 @@ class ModelManager: m['id'] = None return m - def rem(self, m: Dict[str, Any]) -> None: + def rem(self, m: Model) -> None: "Delete model, and all its cards/notes." self.col.modSchema(check=True) current = self.current()['id'] == m['id'] @@ -175,26 +176,26 @@ select id from cards where nid in (select id from notes where mid = ?)""", if current: self.setCurrent(list(self.models.values())[0]) - def add(self, m: Dict[str, Any]) -> None: + def add(self, m: Model) -> None: self._setID(m) self.update(m) self.setCurrent(m) self.save(m) - def ensureNameUnique(self, m: Dict[str, Any]) -> None: + def ensureNameUnique(self, m: Model) -> None: for mcur in self.all(): if (mcur['name'] == m['name'] and mcur['id'] != m['id']): m['name'] += "-" + checksum(str(time.time()))[:5] break - def update(self, m: Dict[str, Any]) -> None: + def update(self, m: Model) -> None: "Add or update an existing model. Used for syncing and merging." self.ensureNameUnique(m) self.models[str(m['id'])] = m # mark registry changed, but don't bump mod time self.save() - def _setID(self, m: Dict[str, Any]) -> None: + def _setID(self, m: Model) -> None: while 1: id = str(intTime(1000)) if id not in self.models: @@ -210,17 +211,17 @@ select id from cards where nid in (select id from notes where mid = ?)""", # Tools ################################################## - def nids(self, m: Dict[str, Any]) -> Any: + def nids(self, m: Model) -> Any: "Note ids for M." return self.col.db.list( "select id from notes where mid = ?", m['id']) - def useCount(self, m: Dict[str, Any]) -> Any: + def useCount(self, m: Model) -> Any: "Number of note using M." return self.col.db.scalar( "select count() from notes where mid = ?", m['id']) - def tmplUseCount(self, m, ord) -> Any: + def tmplUseCount(self, m: Model, ord) -> Any: return self.col.db.scalar(""" select count() from cards, notes where cards.nid = notes.id and notes.mid = ? and cards.ord = ?""", m['id'], ord) @@ -228,7 +229,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) # Copying ################################################## - def copy(self, m: Dict[str, Any]) -> Any: + def copy(self, m: Model) -> Any: "Copy, save and return." m2 = copy.deepcopy(m) m2['name'] = _("%s copy") % m2['name'] @@ -238,30 +239,30 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) # Fields ################################################## - def newField(self, name: str) -> Dict[str, Any]: + def newField(self, name: str) -> Field: assert(isinstance(name, str)) f = defaultField.copy() f['name'] = name return f - def fieldMap(self, m: Dict[str, Any]) -> Dict[Any, Tuple[Any, Any]]: + def fieldMap(self, m: Model) -> Dict[str, Tuple[Any, Any]]: "Mapping of field name -> (ord, field)." return dict((f['name'], (f['ord'], f)) for f in m['flds']) - def fieldNames(self, m) -> List: + def fieldNames(self, m: Model) -> List[str]: return [f['name'] for f in m['flds']] - def sortIdx(self, m: Dict[str, Any]) -> Any: + def sortIdx(self, m: Model) -> Any: return m['sortf'] - def setSortIdx(self, m, idx) -> None: + def setSortIdx(self, m: Model, idx: int) -> None: assert 0 <= idx < len(m['flds']) self.col.modSchema(check=True) m['sortf'] = idx self.col.updateFieldCache(self.nids(m)) self.save(m, updateReqs=False) - def addField(self, m: Dict[str, Any], field: Dict[str, Any]) -> None: + def addField(self, m: Model, field: Field) -> None: # only mod schema if model isn't new if m['id']: self.col.modSchema(check=True) @@ -273,7 +274,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) return fields self._transformFields(m, add) - def remField(self, m: Dict[str, Any], field: Dict[str, Any]) -> None: + def remField(self, m: Model, field: Field) -> None: self.col.modSchema(check=True) # save old sort field sortFldName = m['flds'][m['sortf']]['name'] @@ -296,7 +297,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) # saves self.renameField(m, field, None) - def moveField(self, m: Dict[str, Any], field: Dict[str, Any], idx: int) -> None: + def moveField(self, m: Model, field: Field, idx: int) -> None: self.col.modSchema(check=True) oldidx = m['flds'].index(field) if oldidx == idx: @@ -317,7 +318,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) return fields self._transformFields(m, move) - def renameField(self, m: Dict[str, Any], field: Dict[str, Any], newName: Optional[str]) -> None: + def renameField(self, m: Model, field: Field, newName: Optional[str]) -> None: self.col.modSchema(check=True) pat = r'{{([^{}]*)([:#^/]|[^:#/^}][^:}]*?:|)%s}}' def wrap(txt): @@ -335,11 +336,11 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) field['name'] = newName self.save(m) - def _updateFieldOrds(self, m: Dict[str, Any]) -> None: + def _updateFieldOrds(self, m: Model) -> None: for c, f in enumerate(m['flds']): f['ord'] = c - def _transformFields(self, m: Dict[str, Any], fn: Callable) -> None: + def _transformFields(self, m: Model, fn: Callable) -> None: # model hasn't been added yet? if not m['id']: return @@ -354,12 +355,12 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) # Templates ################################################## - def newTemplate(self, name: str) -> Dict[str, Any]: + def newTemplate(self, name: str) -> Template: t = defaultTemplate.copy() t['name'] = name return t - def addTemplate(self, m: Dict[str, Any], template: Dict[str, Union[str, None]]) -> None: + def addTemplate(self, m: Model, template: Template) -> None: "Note: should col.genCards() afterwards." if m['id']: self.col.modSchema(check=True) @@ -367,7 +368,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord) self._updateTemplOrds(m) self.save(m) - def remTemplate(self, m: Dict[str, Any], template: Dict[str, Any]) -> bool: + def remTemplate(self, m: Model, template: Template) -> bool: "False if removing template would leave orphan notes." assert len(m['tmpls']) > 1 # find cards using this template @@ -397,11 +398,11 @@ update cards set ord = ord - 1, usn = ?, mod = ? self.save(m) return True - def _updateTemplOrds(self, m: Dict[str, Any]) -> None: + def _updateTemplOrds(self, m: Model) -> None: for c, t in enumerate(m['tmpls']): t['ord'] = c - def moveTemplate(self, m, template, idx) -> None: + def moveTemplate(self, m: Model, template: Template, idx: int) -> None: oldidx = m['tmpls'].index(template) if oldidx == idx: return @@ -420,7 +421,7 @@ update cards set ord = (case %s end),usn=?,mod=? where nid in ( select id from notes where mid = ?)""" % " ".join(map), self.col.usn(), intTime(), m['id']) - def _syncTemplates(self, m: Dict[str, Any]) -> None: + def _syncTemplates(self, m: Model) -> None: rem = self.col.genCards(self.nids(m)) # Model changing @@ -428,7 +429,7 @@ select id from notes where mid = ?)""" % " ".join(map), # - maps are ord->ord, and there should not be duplicate targets # - newModel should be self if model is not changing - def change(self, m: Dict[str, Any], nids: List[int], newModel: Dict[str, Any], fmap: Any, cmap: Any) -> None: + def change(self, m: Model, nids: List[int], newModel: Model, fmap: Any, cmap: Any) -> None: self.col.modSchema(check=True) assert newModel['id'] == m['id'] or (fmap and cmap) if fmap: @@ -437,7 +438,7 @@ select id from notes where mid = ?)""" % " ".join(map), self._changeCards(nids, m, newModel, cmap) self.col.genCards(nids) - def _changeNotes(self, nids: List[int], newModel: Dict[str, Any], map: Dict[int, Union[None, int]]) -> None: + def _changeNotes(self, nids: List[int], newModel: Model, map: Dict[int, Union[None, int]]) -> None: d = [] nfields = len(newModel['flds']) for (nid, flds) in self.col.db.execute( @@ -456,7 +457,7 @@ select id from notes where mid = ?)""" % " ".join(map), "update notes set flds=:flds,mid=:mid,mod=:m,usn=:u where id = :nid", d) self.col.updateFieldCache(nids) - def _changeCards(self, nids: List[int], oldModel: Dict[str, Any], newModel: Dict[str, Any], map: Dict[int, Union[None, int]]) -> None: + def _changeCards(self, nids: List[int], oldModel: Model, newModel: Model, map: Dict[int, Union[None, int]]) -> None: d = [] deleted = [] for (cid, ord) in self.col.db.execute( @@ -486,7 +487,7 @@ select id from notes where mid = ?)""" % " ".join(map), # Schema hash ########################################################################## - def scmhash(self, m: Dict[str, Any]) -> str: + def scmhash(self, m: Model) -> str: "Return a hash of the schema, to see if models are compatible." s = "" for f in m['flds']: @@ -498,7 +499,7 @@ select id from notes where mid = ?)""" % " ".join(map), # Required field/text cache ########################################################################## - def _updateRequired(self, m: Dict[str, Any]) -> None: + def _updateRequired(self, m: Model) -> None: if m['type'] == MODEL_CLOZE: # nothing to do return @@ -509,7 +510,7 @@ select id from notes where mid = ?)""" % " ".join(map), req.append([t['ord'], ret[0], ret[1]]) m['req'] = req - def _reqForTemplate(self, m: Dict[str, Any], flds: List[str], t: Dict[str, Any]) -> Tuple[Union[str, List[int]], ...]: + def _reqForTemplate(self, m: Model, flds: List[str], t: Template) -> Tuple[Union[str, List[int]], ...]: a = [] b = [] for f in flds: @@ -546,7 +547,7 @@ select id from notes where mid = ?)""" % " ".join(map), req.append(i) return type, req - def availOrds(self, m: Dict[str, Any], flds: str) -> List: + def availOrds(self, m: Model, flds: str) -> List: "Given a joined field string, return available template ordinals." if m['type'] == MODEL_CLOZE: return self._availClozeOrds(m, flds) @@ -580,7 +581,7 @@ select id from notes where mid = ?)""" % " ".join(map), avail.append(ord) return avail - def _availClozeOrds(self, m: Dict[str, Any], flds: str, allowEmpty: bool = True) -> List: + def _availClozeOrds(self, m: Model, flds: str, allowEmpty: bool = True) -> List: sflds = splitFields(flds) map = self.fieldMap(m) ords = set() diff --git a/anki/stdmodels.py b/anki/stdmodels.py index cd8ac6e47..8f8499d96 100644 --- a/anki/stdmodels.py +++ b/anki/stdmodels.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Any, Dict +from typing import Any, Callable, List, Tuple from anki.consts import MODEL_CLOZE from anki.lang import _ +from anki.types import Model -models = [] +models: List[Tuple[Callable[[], str], Callable[[Any], Model]]] = [] # Basic ########################################################################## -def _newBasicModel(col, name=None) -> Dict[str, Any]: +def _newBasicModel(col, name=None) -> Model: mm = col.models m = mm.new(name or _("Basic")) fm = mm.newField(_("Front")) @@ -24,7 +25,7 @@ def _newBasicModel(col, name=None) -> Dict[str, Any]: mm.addTemplate(m, t) return m -def addBasicModel(col) -> Dict[str, Any]: +def addBasicModel(col) -> Model: m = _newBasicModel(col) col.models.add(m) return m @@ -34,7 +35,7 @@ models.append((lambda: _("Basic"), addBasicModel)) # Basic w/ typing ########################################################################## -def addBasicTypingModel(col) -> Dict[str, Any]: +def addBasicTypingModel(col) -> Model: mm = col.models m = _newBasicModel(col, _("Basic (type in the answer)")) t = m['tmpls'][0] @@ -48,7 +49,7 @@ models.append((lambda: _("Basic (type in the answer)"), addBasicTypingModel)) # Forward & Reverse ########################################################################## -def _newForwardReverse(col, name=None) -> Dict[str, Any]: +def _newForwardReverse(col, name=None) -> Model: mm = col.models m = _newBasicModel(col, name or _("Basic (and reversed card)")) t = mm.newTemplate(_("Card 2")) @@ -57,7 +58,7 @@ def _newForwardReverse(col, name=None) -> Dict[str, Any]: mm.addTemplate(m, t) return m -def addForwardReverse(col) -> Dict[str, Any]: +def addForwardReverse(col) -> Model: m = _newForwardReverse(col) col.models.add(m) return m @@ -67,7 +68,7 @@ models.append((lambda: _("Basic (and reversed card)"), addForwardReverse)) # Forward & Optional Reverse ########################################################################## -def addForwardOptionalReverse(col) -> Dict[str, Any]: +def addForwardOptionalReverse(col) -> Model: mm = col.models m = _newForwardReverse(col, _("Basic (optional reversed card)")) av = _("Add Reverse") @@ -84,7 +85,7 @@ models.append((lambda: _("Basic (optional reversed card)"), # Cloze ########################################################################## -def addClozeModel(col) -> Dict[str, Any]: +def addClozeModel(col) -> Model: mm = col.models m = mm.new(_("Cloze")) m['type'] = MODEL_CLOZE diff --git a/anki/types.py b/anki/types.py new file mode 100644 index 000000000..1b7182bf2 --- /dev/null +++ b/anki/types.py @@ -0,0 +1,12 @@ +from typing import Any, Dict, Union + +# Model attributes are stored in a dict keyed by strings. This type alias +# provides more descriptive function signatures than just 'Dict[str, Any]' +# for methods that operate on models. +# TODO: Use https://www.python.org/dev/peps/pep-0589/ when available in +# supported Python versions. +Model = Dict[str, Any] + +Field = Dict[str, Any] + +Template = Dict[str, Union[str, int, None]] diff --git a/anki/utils.py b/anki/utils.py index bc37bf0fc..6f4141568 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -52,7 +52,7 @@ inTimeTable = { "seconds": lambda n: ngettext("in %s second", "in %s seconds", n), } -def shortTimeFmt(type: str) -> Any: +def shortTimeFmt(type: str) -> str: return { #T: year is an abbreviation for year. %s is a number of years "years": _("%sy"), @@ -84,7 +84,7 @@ def fmtTimeSpan(time: Union[int, float], pad: int = 0, point: int = 0, short: bo timestr = "%%%(a)d.%(b)df" % {'a': pad, 'b': point} return locale.format_string(fmt % timestr, time) -def optimalPeriod(time: Union[int, float], point: int, unit: int) -> Tuple[str, Any]: +def optimalPeriod(time: Union[int, float], point: int, unit: int) -> Tuple[str, int]: if abs(time) < 60 or unit < 1: type = "seconds" point -= 1 @@ -152,7 +152,7 @@ def stripHTML(s: str) -> str: s = entsToTxt(s) return s -def stripHTMLMedia(s: str) -> Any: +def stripHTMLMedia(s: str) -> str: "Strip HTML but keep media filenames" s = reMedia.sub(" \\1 ", s) return stripHTML(s) @@ -167,7 +167,7 @@ def minimizeHTML(s) -> str: '\\1', s) return s -def htmlToTextLine(s) -> Any: +def htmlToTextLine(s) -> str: s = s.replace("
", " ") s = s.replace("
", " ") s = s.replace("
", " ")