Merge pull request #369 from agentydragon/typecheck-models

Add types for models, templates and field dicts
This commit is contained in:
Damien Elmes 2019-12-22 08:19:40 +10:00 committed by GitHub
commit 079a00653e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 62 deletions

View File

@ -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:

View File

@ -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("<br( /)?>|<div>", "\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()

View File

@ -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()

View File

@ -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

12
anki/types.py Normal file
View File

@ -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]]

View File

@ -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:
'<u>\\1</u>', s)
return s
def htmlToTextLine(s) -> Any:
def htmlToTextLine(s) -> str:
s = s.replace("<br>", " ")
s = s.replace("<br />", " ")
s = s.replace("<div>", " ")