add pytype inferred types to anki/

I've corrected some obvious issues, and we can fix others over time.
Mypy tests are currently broken, as adding the type hints has increased
mypy's testing surface.
This commit is contained in:
Damien Elmes 2019-12-19 13:02:45 +10:00
parent 5ff4327db1
commit 6ecfff56c5
38 changed files with 967 additions and 919 deletions

View File

@ -7,6 +7,7 @@ import time
from anki.hooks import runHook from anki.hooks import runHook
from anki.utils import intTime, timestampID, joinFields from anki.utils import intTime, timestampID, joinFields
from anki.consts import * from anki.consts import *
from typing import Any, Optional
# Cards # Cards
########################################################################## ##########################################################################
@ -21,7 +22,7 @@ from anki.consts import *
class Card: class Card:
def __init__(self, col, id=None): def __init__(self, col, id=None) -> None:
self.col = col self.col = col
self.timerStarted = None self.timerStarted = None
self._qa = None self._qa = None
@ -46,7 +47,7 @@ class Card:
self.flags = 0 self.flags = 0
self.data = "" self.data = ""
def load(self): def load(self) -> None:
(self.id, (self.id,
self.nid, self.nid,
self.did, self.did,
@ -69,7 +70,7 @@ class Card:
self._qa = None self._qa = None
self._note = None self._note = None
def flush(self): def flush(self) -> None:
self.mod = intTime() self.mod = intTime()
self.usn = self.col.usn() self.usn = self.col.usn()
# bug check # bug check
@ -100,7 +101,7 @@ insert or replace into cards values
self.data) self.data)
self.col.log(self) self.col.log(self)
def flushSched(self): def flushSched(self) -> None:
self.mod = intTime() self.mod = intTime()
self.usn = self.col.usn() self.usn = self.col.usn()
# bug checks # bug checks
@ -116,16 +117,16 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
self.left, self.odue, self.odid, self.did, self.id) self.left, self.odue, self.odid, self.did, self.id)
self.col.log(self) self.col.log(self)
def q(self, reload=False, browser=False): def q(self, reload=False, browser=False) -> str:
return self.css() + self._getQA(reload, browser)['q'] return self.css() + self._getQA(reload, browser)['q']
def a(self): def a(self) -> str:
return self.css() + self._getQA()['a'] return self.css() + self._getQA()['a']
def css(self): def css(self) -> str:
return "<style>%s</style>" % self.model()['css'] return "<style>%s</style>" % self.model()['css']
def _getQA(self, reload=False, browser=False): def _getQA(self, reload=False, browser=False) -> Any:
if not self._qa or reload: if not self._qa or reload:
f = self.note(reload); m = self.model(); t = self.template() f = self.note(reload); m = self.model(); t = self.template()
data = [self.id, f.id, m['id'], self.odid or self.did, self.ord, data = [self.id, f.id, m['id'], self.odid or self.did, self.ord,
@ -137,45 +138,45 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
self._qa = self.col._renderQA(data, *args) self._qa = self.col._renderQA(data, *args)
return self._qa return self._qa
def note(self, reload=False): def note(self, reload=False) -> Any:
if not self._note or reload: if not self._note or reload:
self._note = self.col.getNote(self.nid) self._note = self.col.getNote(self.nid)
return self._note return self._note
def model(self): def model(self) -> Any:
return self.col.models.get(self.note().mid) return self.col.models.get(self.note().mid)
def template(self): def template(self) -> Any:
m = self.model() m = self.model()
if m['type'] == MODEL_STD: if m['type'] == MODEL_STD:
return self.model()['tmpls'][self.ord] return self.model()['tmpls'][self.ord]
else: else:
return self.model()['tmpls'][0] return self.model()['tmpls'][0]
def startTimer(self): def startTimer(self) -> None:
self.timerStarted = time.time() self.timerStarted = time.time()
def timeLimit(self): def timeLimit(self) -> Any:
"Time limit for answering in milliseconds." "Time limit for answering in milliseconds."
conf = self.col.decks.confForDid(self.odid or self.did) conf = self.col.decks.confForDid(self.odid or self.did)
return conf['maxTaken']*1000 return conf['maxTaken']*1000
def shouldShowTimer(self): def shouldShowTimer(self) -> Any:
conf = self.col.decks.confForDid(self.odid or self.did) conf = self.col.decks.confForDid(self.odid or self.did)
return conf['timer'] return conf['timer']
def timeTaken(self): def timeTaken(self) -> Any:
"Time taken to answer card, in integer MS." "Time taken to answer card, in integer MS."
total = int((time.time() - self.timerStarted)*1000) total = int((time.time() - self.timerStarted)*1000)
return min(total, self.timeLimit()) return min(total, self.timeLimit())
def isEmpty(self): def isEmpty(self) -> Optional[bool]:
ords = self.col.models.availOrds( ords = self.col.models.availOrds(
self.model(), joinFields(self.note().fields)) self.model(), joinFields(self.note().fields))
if self.ord not in ords: if self.ord not in ords:
return True return True
def __repr__(self): def __repr__(self) -> str:
d = dict(self.__dict__) d = dict(self.__dict__)
# remove non-useful elements # remove non-useful elements
del d['_note'] del d['_note']
@ -184,9 +185,9 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
del d['timerStarted'] del d['timerStarted']
return pprint.pformat(d, width=300) return pprint.pformat(d, width=300)
def userFlag(self): def userFlag(self) -> Any:
return self.flags & 0b111 return self.flags & 0b111
def setUserFlag(self, flag): def setUserFlag(self, flag) -> None:
assert 0 <= flag <= 7 assert 0 <= flag <= 7
self.flags = (self.flags & ~0b111) | flag self.flags = (self.flags & ~0b111) | flag

View File

@ -29,6 +29,7 @@ import anki.cards
import anki.notes import anki.notes
import anki.template import anki.template
import anki.find import anki.find
from typing import Any, List, Optional, Tuple, Union, Dict
defaultConf = { defaultConf = {
# review options # review options
@ -49,7 +50,7 @@ defaultConf = {
'schedVer': 2, 'schedVer': 2,
} }
def timezoneOffset(): def timezoneOffset() -> int:
if time.localtime().tm_isdst: if time.localtime().tm_isdst:
return time.altzone//60 return time.altzone//60
else: else:
@ -58,7 +59,7 @@ def timezoneOffset():
# this is initialized by storage.Collection # this is initialized by storage.Collection
class _Collection: class _Collection:
def __init__(self, db, server=False, log=False): def __init__(self, db, server=False, log=False) -> None:
self._debugLog = log self._debugLog = log
self.db = db self.db = db
self.path = db._path self.path = db._path
@ -85,7 +86,7 @@ class _Collection:
self.conf['newBury'] = True self.conf['newBury'] = True
self.setMod() self.setMod()
def name(self): def name(self) -> Any:
n = os.path.splitext(os.path.basename(self.path))[0] n = os.path.splitext(os.path.basename(self.path))[0]
return n return n
@ -94,14 +95,14 @@ class _Collection:
supportedSchedulerVersions = (1, 2) supportedSchedulerVersions = (1, 2)
def schedVer(self): def schedVer(self) -> Any:
ver = self.conf.get("schedVer", 1) ver = self.conf.get("schedVer", 1)
if ver in self.supportedSchedulerVersions: if ver in self.supportedSchedulerVersions:
return ver return ver
else: else:
raise Exception("Unsupported scheduler version") raise Exception("Unsupported scheduler version")
def _loadScheduler(self): def _loadScheduler(self) -> None:
ver = self.schedVer() ver = self.schedVer()
if ver == 1: if ver == 1:
from anki.sched import Scheduler from anki.sched import Scheduler
@ -110,7 +111,7 @@ class _Collection:
self.sched = Scheduler(self) self.sched = Scheduler(self)
def changeSchedulerVer(self, ver): def changeSchedulerVer(self, ver) -> None:
if ver == self.schedVer(): if ver == self.schedVer():
return return
if ver not in self.supportedSchedulerVersions: if ver not in self.supportedSchedulerVersions:
@ -135,7 +136,7 @@ class _Collection:
# DB-related # DB-related
########################################################################## ##########################################################################
def load(self): def load(self) -> None:
(self.crt, (self.crt,
self.mod, self.mod,
self.scm, self.scm,
@ -154,14 +155,14 @@ conf, models, decks, dconf, tags from col""")
self.decks.load(decks, dconf) self.decks.load(decks, dconf)
self.tags.load(tags) self.tags.load(tags)
def setMod(self): def setMod(self) -> None:
"""Mark DB modified. """Mark DB modified.
DB operations and the deck/tag/model managers do this automatically, so this DB operations and the deck/tag/model managers do this automatically, so this
is only necessary if you modify properties of this object or the conf dict.""" is only necessary if you modify properties of this object or the conf dict."""
self.db.mod = True self.db.mod = True
def flush(self, mod=None): def flush(self, mod=None) -> None:
"Flush state to DB, updating mod time." "Flush state to DB, updating mod time."
self.mod = intTime(1000) if mod is None else mod self.mod = intTime(1000) if mod is None else mod
self.db.execute( self.db.execute(
@ -170,7 +171,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.crt, self.mod, self.scm, self.dty, self.crt, self.mod, self.scm, self.dty,
self._usn, self.ls, json.dumps(self.conf)) self._usn, self.ls, json.dumps(self.conf))
def save(self, name=None, mod=None): def save(self, name=None, mod=None) -> None:
"Flush, commit DB, and take out another write lock." "Flush, commit DB, and take out another write lock."
# let the managers conditionally flush # let the managers conditionally flush
self.models.flush() self.models.flush()
@ -185,19 +186,19 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self._markOp(name) self._markOp(name)
self._lastSave = time.time() self._lastSave = time.time()
def autosave(self): def autosave(self) -> Optional[bool]:
"Save if 5 minutes has passed since last save. True if saved." "Save if 5 minutes has passed since last save. True if saved."
if time.time() - self._lastSave > 300: if time.time() - self._lastSave > 300:
self.save() self.save()
return True return True
def lock(self): def lock(self) -> None:
# make sure we don't accidentally bump mod time # make sure we don't accidentally bump mod time
mod = self.db.mod mod = self.db.mod
self.db.execute("update col set mod=mod") self.db.execute("update col set mod=mod")
self.db.mod = mod self.db.mod = mod
def close(self, save=True): def close(self, save=True) -> None:
"Disconnect from DB." "Disconnect from DB."
if self.db: if self.db:
if save: if save:
@ -213,7 +214,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.media.close() self.media.close()
self._closeLog() self._closeLog()
def reopen(self): def reopen(self) -> None:
"Reconnect to DB (after changing threads, etc)." "Reconnect to DB (after changing threads, etc)."
import anki.db import anki.db
if not self.db: if not self.db:
@ -221,12 +222,12 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.media.connect() self.media.connect()
self._openLog() self._openLog()
def rollback(self): def rollback(self) -> None:
self.db.rollback() self.db.rollback()
self.load() self.load()
self.lock() self.lock()
def modSchema(self, check): def modSchema(self, check) -> None:
"Mark schema modified. Call this first so user can abort if necessary." "Mark schema modified. Call this first so user can abort if necessary."
if not self.schemaChanged(): if not self.schemaChanged():
if check and not runFilter("modSchema", True): if check and not runFilter("modSchema", True):
@ -234,14 +235,14 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.scm = intTime(1000) self.scm = intTime(1000)
self.setMod() self.setMod()
def schemaChanged(self): def schemaChanged(self) -> Any:
"True if schema changed since last sync." "True if schema changed since last sync."
return self.scm > self.ls return self.scm > self.ls
def usn(self): def usn(self) -> Any:
return self._usn if self.server else -1 return self._usn if self.server else -1
def beforeUpload(self): def beforeUpload(self) -> None:
"Called before a full upload." "Called before a full upload."
tbls = "notes", "cards", "revlog" tbls = "notes", "cards", "revlog"
for t in tbls: for t in tbls:
@ -263,44 +264,44 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
# Object creation helpers # Object creation helpers
########################################################################## ##########################################################################
def getCard(self, id): def getCard(self, id) -> anki.cards.Card:
return anki.cards.Card(self, id) return anki.cards.Card(self, id)
def getNote(self, id): def getNote(self, id) -> anki.notes.Note:
return anki.notes.Note(self, id=id) return anki.notes.Note(self, id=id)
# Utils # Utils
########################################################################## ##########################################################################
def nextID(self, type, inc=True): def nextID(self, type, inc=True) -> Any:
type = "next"+type.capitalize() type = "next"+type.capitalize()
id = self.conf.get(type, 1) id = self.conf.get(type, 1)
if inc: if inc:
self.conf[type] = id+1 self.conf[type] = id+1
return id return id
def reset(self): def reset(self) -> None:
"Rebuild the queue and reload data after DB modified." "Rebuild the queue and reload data after DB modified."
self.sched.reset() self.sched.reset()
# Deletion logging # Deletion logging
########################################################################## ##########################################################################
def _logRem(self, ids, type): def _logRem(self, ids, type) -> None:
self.db.executemany("insert into graves values (%d, ?, %d)" % ( self.db.executemany("insert into graves values (%d, ?, %d)" % (
self.usn(), type), ([x] for x in ids)) self.usn(), type), ([x] for x in ids))
# Notes # Notes
########################################################################## ##########################################################################
def noteCount(self): def noteCount(self) -> Any:
return self.db.scalar("select count() from notes") return self.db.scalar("select count() from notes")
def newNote(self, forDeck=True): def newNote(self, forDeck=True) -> anki.notes.Note:
"Return a new note with the current model." "Return a new note with the current model."
return anki.notes.Note(self, self.models.current(forDeck)) return anki.notes.Note(self, self.models.current(forDeck))
def addNote(self, note): def addNote(self, note) -> int:
"Add a note to the collection. Return number of new cards." "Add a note to the collection. Return number of new cards."
# check we have card models available, then save # check we have card models available, then save
cms = self.findTemplates(note) cms = self.findTemplates(note)
@ -316,11 +317,11 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
ncards += 1 ncards += 1
return ncards return ncards
def remNotes(self, ids): def remNotes(self, ids) -> None:
self.remCards(self.db.list("select id from cards where nid in "+ self.remCards(self.db.list("select id from cards where nid in "+
ids2str(ids))) ids2str(ids)))
def _remNotes(self, ids): def _remNotes(self, ids) -> None:
"Bulk delete notes by ID. Don't call this directly." "Bulk delete notes by ID. Don't call this directly."
if not ids: if not ids:
return return
@ -334,13 +335,13 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
# Card creation # Card creation
########################################################################## ##########################################################################
def findTemplates(self, note): def findTemplates(self, note) -> List:
"Return (active), non-empty templates." "Return (active), non-empty templates."
model = note.model() model = note.model()
avail = self.models.availOrds(model, joinFields(note.fields)) avail = self.models.availOrds(model, joinFields(note.fields))
return self._tmplsFromOrds(model, avail) return self._tmplsFromOrds(model, avail)
def _tmplsFromOrds(self, model, avail): def _tmplsFromOrds(self, model, avail) -> List:
ok = [] ok = []
if model['type'] == MODEL_STD: if model['type'] == MODEL_STD:
for t in model['tmpls']: for t in model['tmpls']:
@ -354,7 +355,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
ok.append(t) ok.append(t)
return ok return ok
def genCards(self, nids): def genCards(self, nids) -> List:
"Generate cards for non-empty templates, return ids to remove." "Generate cards for non-empty templates, return ids to remove."
# build map of (nid,ord) so we don't create dupes # build map of (nid,ord) so we don't create dupes
snids = ids2str(nids) snids = ids2str(nids)
@ -428,7 +429,7 @@ insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""",
# type 0 - when previewing in add dialog, only non-empty # type 0 - when previewing in add dialog, only non-empty
# type 1 - when previewing edit, only existing # type 1 - when previewing edit, only existing
# type 2 - when previewing in models dialog, all templates # type 2 - when previewing in models dialog, all templates
def previewCards(self, note, type=0, did=None): def previewCards(self, note, type=0, did=None) -> List:
if type == 0: if type == 0:
cms = self.findTemplates(note) cms = self.findTemplates(note)
elif type == 1: elif type == 1:
@ -442,7 +443,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)) cards.append(self._newCard(note, template, 1, flush=False, did=did))
return cards return cards
def _newCard(self, note, template, due, flush=True, did=None): def _newCard(self, note, template, due, flush=True, did=None) -> anki.cards.Card:
"Create a new card." "Create a new card."
card = anki.cards.Card(self) card = anki.cards.Card(self)
card.nid = note.id card.nid = note.id
@ -469,7 +470,7 @@ insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""",
card.flush() card.flush()
return card return card
def _dueForDid(self, did, due): def _dueForDid(self, did, due: int) -> int:
conf = self.decks.confForDid(did) conf = self.decks.confForDid(did)
# in order due? # in order due?
if conf['new']['order'] == NEW_CARDS_DUE: if conf['new']['order'] == NEW_CARDS_DUE:
@ -484,13 +485,13 @@ insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""",
# Cards # Cards
########################################################################## ##########################################################################
def isEmpty(self): def isEmpty(self) -> bool:
return not self.db.scalar("select 1 from cards limit 1") return not self.db.scalar("select 1 from cards limit 1")
def cardCount(self): def cardCount(self) -> Any:
return self.db.scalar("select count() from cards") return self.db.scalar("select count() from cards")
def remCards(self, ids, notes=True): def remCards(self, ids, notes=True) -> None:
"Bulk delete cards by ID." "Bulk delete cards by ID."
if not ids: if not ids:
return return
@ -507,13 +508,13 @@ select id from notes where id in %s and id not in (select nid from cards)""" %
ids2str(nids)) ids2str(nids))
self._remNotes(nids) self._remNotes(nids)
def emptyCids(self): def emptyCids(self) -> List:
rem = [] rem = []
for m in self.models.all(): for m in self.models.all():
rem += self.genCards(self.models.nids(m)) rem += self.genCards(self.models.nids(m))
return rem return rem
def emptyCardReport(self, cids): def emptyCardReport(self, cids) -> str:
rep = "" rep = ""
for ords, cnt, flds in self.db.all(""" for ords, cnt, flds in self.db.all("""
select group_concat(ord+1), count(), flds from cards c, notes n select group_concat(ord+1), count(), flds from cards c, notes n
@ -525,11 +526,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
# Field checksums and sorting fields # Field checksums and sorting fields
########################################################################## ##########################################################################
def _fieldData(self, snids): def _fieldData(self, snids) -> Any:
return self.db.execute( return self.db.execute(
"select id, mid, flds from notes where id in "+snids) "select id, mid, flds from notes where id in "+snids)
def updateFieldCache(self, nids): def updateFieldCache(self, nids) -> None:
"Update field checksums and sort cache, after find&replace, etc." "Update field checksums and sort cache, after find&replace, etc."
snids = ids2str(nids) snids = ids2str(nids)
r = [] r = []
@ -548,7 +549,7 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
# Q/A generation # Q/A generation
########################################################################## ##########################################################################
def renderQA(self, ids=None, type="card"): def renderQA(self, ids=None, type="card") -> List:
# gather metadata # gather metadata
if type == "card": if type == "card":
where = "and c.id in " + ids2str(ids) where = "and c.id in " + ids2str(ids)
@ -563,7 +564,7 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
return [self._renderQA(row) return [self._renderQA(row)
for row in self._qaData(where)] for row in self._qaData(where)]
def _renderQA(self, data, qfmt=None, afmt=None): def _renderQA(self, data, qfmt=None, afmt=None) -> Dict:
"Returns hash of id, question, answer." "Returns hash of id, question, answer."
# data is [cid, nid, mid, did, ord, tags, flds, cardFlags] # data is [cid, nid, mid, did, ord, tags, flds, cardFlags]
# unpack fields and create dict # unpack fields and create dict
@ -610,7 +611,7 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
"<a href=%s#cloze>%s</a>" % (HELP_SITE, _("help")))) "<a href=%s#cloze>%s</a>" % (HELP_SITE, _("help"))))
return d return d
def _qaData(self, where=""): def _qaData(self, where="") -> Any:
"Return [cid, nid, mid, did, ord, tags, flds, cardFlags] db query" "Return [cid, nid, mid, did, ord, tags, flds, cardFlags] db query"
return self.db.execute(""" return self.db.execute("""
select c.id, f.id, f.mid, c.did, c.ord, f.tags, f.flds, c.flags select c.id, f.id, f.mid, c.did, c.ord, f.tags, f.flds, c.flags
@ -618,7 +619,7 @@ from cards c, notes f
where c.nid == f.id where c.nid == f.id
%s""" % where) %s""" % where)
def _flagNameFromCardFlags(self, flags): def _flagNameFromCardFlags(self, flags) -> str:
flag = flags & 0b111 flag = flags & 0b111
if not flag: if not flag:
return "" return ""
@ -627,37 +628,37 @@ where c.nid == f.id
# Finding cards # Finding cards
########################################################################## ##########################################################################
def findCards(self, query, order=False): def findCards(self, query, order=False) -> Any:
return anki.find.Finder(self).findCards(query, order) return anki.find.Finder(self).findCards(query, order)
def findNotes(self, query): def findNotes(self, query) -> Any:
return anki.find.Finder(self).findNotes(query) return anki.find.Finder(self).findNotes(query)
def findReplace(self, nids, src, dst, regex=None, field=None, fold=True): def findReplace(self, nids, src, dst, regex=None, field=None, fold=True) -> int:
return anki.find.findReplace(self, nids, src, dst, regex, field, fold) return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
def findDupes(self, fieldName, search=""): def findDupes(self, fieldName, search="") -> List[Tuple[Any, list]]:
return anki.find.findDupes(self, fieldName, search) return anki.find.findDupes(self, fieldName, search)
# Stats # Stats
########################################################################## ##########################################################################
def cardStats(self, card): def cardStats(self, card) -> str:
from anki.stats import CardStats from anki.stats import CardStats
return CardStats(self, card).report() return CardStats(self, card).report()
def stats(self): def stats(self) -> "anki.stats.CollectionStats":
from anki.stats import CollectionStats from anki.stats import CollectionStats
return CollectionStats(self) return CollectionStats(self)
# Timeboxing # Timeboxing
########################################################################## ##########################################################################
def startTimebox(self): def startTimebox(self) -> None:
self._startTime = time.time() self._startTime = time.time()
self._startReps = self.sched.reps self._startReps = self.sched.reps
def timeboxReached(self): def timeboxReached(self) -> Optional[Union[bool, Tuple[Any, int]]]:
"Return (elapsedTime, reps) if timebox reached, or False." "Return (elapsedTime, reps) if timebox reached, or False."
if not self.conf['timeLim']: if not self.conf['timeLim']:
# timeboxing disabled # timeboxing disabled
@ -669,24 +670,24 @@ where c.nid == f.id
# Undo # Undo
########################################################################## ##########################################################################
def clearUndo(self): def clearUndo(self) -> None:
# [type, undoName, data] # [type, undoName, data]
# type 1 = review; type 2 = checkpoint # type 1 = review; type 2 = checkpoint
self._undo = None self._undo = None
def undoName(self): def undoName(self) -> Any:
"Undo menu item name, or None if undo unavailable." "Undo menu item name, or None if undo unavailable."
if not self._undo: if not self._undo:
return None return None
return self._undo[1] return self._undo[1]
def undo(self): def undo(self) -> Any:
if self._undo[0] == 1: if self._undo[0] == 1:
return self._undoReview() return self._undoReview()
else: else:
self._undoOp() self._undoOp()
def markReview(self, card): def markReview(self, card) -> None:
old = [] old = []
if self._undo: if self._undo:
if self._undo[0] == 1: if self._undo[0] == 1:
@ -695,7 +696,7 @@ where c.nid == f.id
wasLeech = card.note().hasTag("leech") or False wasLeech = card.note().hasTag("leech") or False
self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech] self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
def _undoReview(self): def _undoReview(self) -> Any:
data = self._undo[2] data = self._undo[2]
wasLeech = self._undo[3] wasLeech = self._undo[3]
c = data.pop() # pytype: disable=attribute-error c = data.pop() # pytype: disable=attribute-error
@ -723,7 +724,7 @@ where c.nid == f.id
self.sched.reps -= 1 self.sched.reps -= 1
return c.id return c.id
def _markOp(self, name): def _markOp(self, name) -> None:
"Call via .save()" "Call via .save()"
if name: if name:
self._undo = [2, name] self._undo = [2, name]
@ -732,14 +733,14 @@ where c.nid == f.id
if self._undo and self._undo[0] == 2: if self._undo and self._undo[0] == 2:
self.clearUndo() self.clearUndo()
def _undoOp(self): def _undoOp(self) -> None:
self.rollback() self.rollback()
self.clearUndo() self.clearUndo()
# DB maintenance # DB maintenance
########################################################################## ##########################################################################
def basicCheck(self): def basicCheck(self) -> Optional[bool]:
"Basic integrity check for syncing. True if ok." "Basic integrity check for syncing. True if ok."
# cards without notes # cards without notes
if self.db.scalar(""" if self.db.scalar("""
@ -763,7 +764,7 @@ select id from notes where mid = ?) limit 1""" %
return return
return True return True
def fixIntegrity(self): def fixIntegrity(self) -> Tuple[Any, bool]:
"Fix possible problems and rebuild caches." "Fix possible problems and rebuild caches."
problems = [] problems = []
curs = self.db.cursor() curs = self.db.cursor()
@ -901,7 +902,7 @@ and type=0""", [intTime(), self.usn()])
self.save() self.save()
return ("\n".join(problems), ok) return ("\n".join(problems), ok)
def optimize(self): def optimize(self) -> None:
self.db.setAutocommit(True) self.db.setAutocommit(True)
self.db.execute("vacuum") self.db.execute("vacuum")
self.db.execute("analyze") self.db.execute("analyze")
@ -911,7 +912,7 @@ and type=0""", [intTime(), self.usn()])
# Logging # Logging
########################################################################## ##########################################################################
def log(self, *args, **kwargs): def log(self, *args, **kwargs) -> None:
if not self._debugLog: if not self._debugLog:
return return
def customRepr(x): def customRepr(x):
@ -926,7 +927,7 @@ and type=0""", [intTime(), self.usn()])
if devMode: if devMode:
print(buf) print(buf)
def _openLog(self): def _openLog(self) -> None:
if not self._debugLog: if not self._debugLog:
return return
lpath = re.sub(r"\.anki2$", ".log", self.path) lpath = re.sub(r"\.anki2$", ".log", self.path)
@ -937,7 +938,7 @@ and type=0""", [intTime(), self.usn()])
os.rename(lpath, lpath2) os.rename(lpath, lpath2)
self._logHnd = open(lpath, "a", encoding="utf8") self._logHnd = open(lpath, "a", encoding="utf8")
def _closeLog(self): def _closeLog(self) -> None:
if not self._debugLog: if not self._debugLog:
return return
self._logHnd.close() self._logHnd.close()
@ -946,7 +947,7 @@ and type=0""", [intTime(), self.usn()])
# Card Flags # Card Flags
########################################################################## ##########################################################################
def setUserFlag(self, flag, cids): def setUserFlag(self, flag, cids) -> None:
assert 0 <= flag <= 7 assert 0 <= flag <= 7
self.db.execute("update cards set flags = (flags & ~?) | ?, usn=?, mod=? where id in %s" % self.db.execute("update cards set flags = (flags & ~?) | ?, usn=?, mod=? where id in %s" %
ids2str(cids), 0b111, flag, self.usn(), intTime()) ids2str(cids), 0b111, flag, self.usn(), intTime())

View File

@ -3,6 +3,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki.lang import _ from anki.lang import _
from typing import Any, Dict
# whether new cards should be mixed with reviews, or shown first or last # whether new cards should be mixed with reviews, or shown first or last
NEW_CARDS_DISTRIBUTE = 0 NEW_CARDS_DISTRIBUTE = 0
@ -57,27 +58,27 @@ HELP_SITE="http://ankisrs.net/docs/manual.html"
# Labels # Labels
########################################################################## ##########################################################################
def newCardOrderLabels(): def newCardOrderLabels() -> Dict[int, Any]:
return { return {
0: _("Show new cards in random order"), 0: _("Show new cards in random order"),
1: _("Show new cards in order added") 1: _("Show new cards in order added")
} }
def newCardSchedulingLabels(): def newCardSchedulingLabels() -> Dict[int, Any]:
return { return {
0: _("Mix new cards and reviews"), 0: _("Mix new cards and reviews"),
1: _("Show new cards after reviews"), 1: _("Show new cards after reviews"),
2: _("Show new cards before reviews"), 2: _("Show new cards before reviews"),
} }
def alignmentLabels(): def alignmentLabels() -> Dict[int, Any]:
return { return {
0: _("Center"), 0: _("Center"),
1: _("Left"), 1: _("Left"),
2: _("Right"), 2: _("Right"),
} }
def dynOrderLabels(): def dynOrderLabels() -> Dict[int, Any]:
return { return {
0: _("Oldest seen first"), 0: _("Oldest seen first"),
1: _("Random"), 1: _("Random"),

View File

@ -6,18 +6,19 @@ import os
import time import time
from sqlite3 import dbapi2 as sqlite, Cursor from sqlite3 import dbapi2 as sqlite, Cursor
from typing import Any, List
DBError = sqlite.Error DBError = sqlite.Error
class DB: class DB:
def __init__(self, path, timeout=0): def __init__(self, path, timeout=0) -> None:
self._db = sqlite.connect(path, timeout=timeout) self._db = sqlite.connect(path, timeout=timeout)
self._db.text_factory = self._textFactory self._db.text_factory = self._textFactory
self._path = path self._path = path
self.echo = os.environ.get("DBECHO") self.echo = os.environ.get("DBECHO")
self.mod = False self.mod = False
def execute(self, sql, *a, **ka): def execute(self, sql, *a, **ka) -> Cursor:
s = sql.strip().lower() s = sql.strip().lower()
# mark modified? # mark modified?
for stmt in "insert", "update", "delete": for stmt in "insert", "update", "delete":
@ -37,7 +38,7 @@ class DB:
print(a, ka) print(a, ka)
return res return res
def executemany(self, sql, l): def executemany(self, sql, l) -> None:
self.mod = True self.mod = True
t = time.time() t = time.time()
self._db.executemany(sql, l) self._db.executemany(sql, l)
@ -46,68 +47,68 @@ class DB:
if self.echo == "2": if self.echo == "2":
print(l) print(l)
def commit(self): def commit(self) -> None:
t = time.time() t = time.time()
self._db.commit() self._db.commit()
if self.echo: if self.echo:
print("commit %0.3fms" % ((time.time() - t)*1000)) print("commit %0.3fms" % ((time.time() - t)*1000))
def executescript(self, sql): def executescript(self, sql) -> None:
self.mod = True self.mod = True
if self.echo: if self.echo:
print(sql) print(sql)
self._db.executescript(sql) self._db.executescript(sql)
def rollback(self): def rollback(self) -> None:
self._db.rollback() self._db.rollback()
def scalar(self, *a, **kw): def scalar(self, *a, **kw) -> Any:
res = self.execute(*a, **kw).fetchone() res = self.execute(*a, **kw).fetchone()
if res: if res:
return res[0] return res[0]
return None return None
def all(self, *a, **kw): def all(self, *a, **kw) -> List:
return self.execute(*a, **kw).fetchall() return self.execute(*a, **kw).fetchall()
def first(self, *a, **kw): def first(self, *a, **kw) -> Any:
c = self.execute(*a, **kw) c = self.execute(*a, **kw)
res = c.fetchone() res = c.fetchone()
c.close() c.close()
return res return res
def list(self, *a, **kw): def list(self, *a, **kw) -> List:
return [x[0] for x in self.execute(*a, **kw)] return [x[0] for x in self.execute(*a, **kw)]
def close(self): def close(self) -> None:
self._db.text_factory = None self._db.text_factory = None
self._db.close() self._db.close()
def set_progress_handler(self, *args): def set_progress_handler(self, *args) -> None:
self._db.set_progress_handler(*args) self._db.set_progress_handler(*args)
def __enter__(self): def __enter__(self) -> "DB":
self._db.execute("begin") self._db.execute("begin")
return self return self
def __exit__(self, exc_type, *args): def __exit__(self, exc_type, *args) -> None:
self._db.close() self._db.close()
def totalChanges(self): def totalChanges(self) -> Any:
return self._db.total_changes return self._db.total_changes
def interrupt(self): def interrupt(self) -> None:
self._db.interrupt() self._db.interrupt()
def setAutocommit(self, autocommit): def setAutocommit(self, autocommit) -> None:
if autocommit: if autocommit:
self._db.isolation_level = None self._db.isolation_level = None
else: else:
self._db.isolation_level = '' self._db.isolation_level = ''
# strip out invalid utf-8 when reading from db # strip out invalid utf-8 when reading from db
def _textFactory(self, data): def _textFactory(self, data) -> str:
return str(data, errors="ignore") return str(data, errors="ignore")
def cursor(self, factory=Cursor): def cursor(self, factory=Cursor) -> Cursor:
return self._db.cursor(factory) return self._db.cursor(factory)

View File

@ -11,6 +11,7 @@ from anki.hooks import runHook
from anki.consts import * from anki.consts import *
from anki.lang import _ from anki.lang import _
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from typing import Any, Dict, List, Optional, Tuple
# fixmes: # fixmes:
# - make sure users can't set grad interval < 1 # - make sure users can't set grad interval < 1
@ -94,12 +95,12 @@ class DeckManager:
# Registry save/load # Registry save/load
############################################################# #############################################################
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self.decks = {} self.decks = {}
self.dconf = {} self.dconf = {}
def load(self, decks, dconf): def load(self, decks, dconf) -> None:
self.decks = json.loads(decks) self.decks = json.loads(decks)
self.dconf = json.loads(dconf) self.dconf = json.loads(dconf)
# set limits to within bounds # set limits to within bounds
@ -114,14 +115,14 @@ class DeckManager:
if not found: if not found:
self.changed = False self.changed = False
def save(self, g=None): def save(self, g=None) -> None:
"Can be called with either a deck or a deck configuration." "Can be called with either a deck or a deck configuration."
if g: if g:
g['mod'] = intTime() g['mod'] = intTime()
g['usn'] = self.col.usn() g['usn'] = self.col.usn()
self.changed = True self.changed = True
def flush(self): def flush(self) -> None:
if self.changed: if self.changed:
self.col.db.execute("update col set decks=?, dconf=?", self.col.db.execute("update col set decks=?, dconf=?",
json.dumps(self.decks), json.dumps(self.decks),
@ -131,7 +132,7 @@ class DeckManager:
# Deck save/load # Deck save/load
############################################################# #############################################################
def id(self, name, create=True, type=None): def id(self, name, create=True, type=None) -> Optional[int]:
"Add a deck with NAME. Reuse deck if already exists. Return id as int." "Add a deck with NAME. Reuse deck if already exists. Return id as int."
if type is None: if type is None:
type = defaultDeck type = defaultDeck
@ -158,7 +159,7 @@ class DeckManager:
runHook("newDeck") runHook("newDeck")
return int(id) return int(id)
def rem(self, did, cardsToo=False, childrenToo=True): def rem(self, did, cardsToo=False, childrenToo=True) -> None:
"Remove the deck. If cardsToo, delete any cards inside." "Remove the deck. If cardsToo, delete any cards inside."
if str(did) == '1': if str(did) == '1':
# we won't allow the default deck to be deleted, but if it's a # we won't allow the default deck to be deleted, but if it's a
@ -208,58 +209,58 @@ class DeckManager:
self.select(int(list(self.decks.keys())[0])) self.select(int(list(self.decks.keys())[0]))
self.save() self.save()
def allNames(self, dyn=True, forceDefault=True): def allNames(self, dyn=True, forceDefault=True) -> List:
"An unsorted list of all deck names." "An unsorted list of all deck names."
if dyn: if dyn:
return [x['name'] for x in self.all(forceDefault=forceDefault)] return [x['name'] for x in self.all(forceDefault=forceDefault)]
else: else:
return [x['name'] for x in self.all(forceDefault=forceDefault) if not x['dyn']] return [x['name'] for x in self.all(forceDefault=forceDefault) if not x['dyn']]
def all(self, forceDefault=True): def all(self, forceDefault=True) -> List:
"A list of all decks." "A list of all decks."
decks = list(self.decks.values()) decks = list(self.decks.values())
if not forceDefault and not self.col.db.scalar("select 1 from cards where did = 1 limit 1") and len(decks)>1: if not forceDefault and not self.col.db.scalar("select 1 from cards where did = 1 limit 1") and len(decks)>1:
decks = [deck for deck in decks if deck['id'] != 1] decks = [deck for deck in decks if deck['id'] != 1]
return decks return decks
def allIds(self): def allIds(self) -> List[str]:
return list(self.decks.keys()) return list(self.decks.keys())
def collapse(self, did): def collapse(self, did) -> None:
deck = self.get(did) deck = self.get(did)
deck['collapsed'] = not deck['collapsed'] deck['collapsed'] = not deck['collapsed']
self.save(deck) self.save(deck)
def collapseBrowser(self, did): def collapseBrowser(self, did) -> None:
deck = self.get(did) deck = self.get(did)
collapsed = deck.get('browserCollapsed', False) collapsed = deck.get('browserCollapsed', False)
deck['browserCollapsed'] = not collapsed deck['browserCollapsed'] = not collapsed
self.save(deck) self.save(deck)
def count(self): def count(self) -> int:
return len(self.decks) return len(self.decks)
def get(self, did, default=True): def get(self, did, default=True) -> Any:
id = str(did) id = str(did)
if id in self.decks: if id in self.decks:
return self.decks[id] return self.decks[id]
elif default: elif default:
return self.decks['1'] return self.decks['1']
def byName(self, name): def byName(self, name) -> Any:
"""Get deck with NAME, ignoring case.""" """Get deck with NAME, ignoring case."""
for m in list(self.decks.values()): for m in list(self.decks.values()):
if self.equalName(m['name'], name): if self.equalName(m['name'], name):
return m return m
def update(self, g): def update(self, g) -> None:
"Add or update an existing deck. Used for syncing and merging." "Add or update an existing deck. Used for syncing and merging."
self.decks[str(g['id'])] = g self.decks[str(g['id'])] = g
self.maybeAddToActive() self.maybeAddToActive()
# mark registry changed, but don't bump mod time # mark registry changed, but don't bump mod time
self.save() self.save()
def rename(self, g, newName): def rename(self, g, newName) -> None:
"Rename deck prefix to NAME if not exists. Updates children." "Rename deck prefix to NAME if not exists. Updates children."
# make sure target node doesn't already exist # make sure target node doesn't already exist
if self.byName(newName): if self.byName(newName):
@ -284,7 +285,7 @@ class DeckManager:
# renaming may have altered active did order # renaming may have altered active did order
self.maybeAddToActive() self.maybeAddToActive()
def renameForDragAndDrop(self, draggedDeckDid, ontoDeckDid): def renameForDragAndDrop(self, draggedDeckDid, ontoDeckDid) -> None:
draggedDeck = self.get(draggedDeckDid) draggedDeck = self.get(draggedDeckDid)
draggedDeckName = draggedDeck['name'] draggedDeckName = draggedDeck['name']
ontoDeckName = self.get(ontoDeckDid)['name'] ontoDeckName = self.get(ontoDeckDid)['name']
@ -299,7 +300,7 @@ class DeckManager:
assert ontoDeckName.strip() assert ontoDeckName.strip()
self.rename(draggedDeck, ontoDeckName + "::" + self._basename(draggedDeckName)) self.rename(draggedDeck, ontoDeckName + "::" + self._basename(draggedDeckName))
def _canDragAndDrop(self, draggedDeckName, ontoDeckName): def _canDragAndDrop(self, draggedDeckName, ontoDeckName) -> bool:
if draggedDeckName == ontoDeckName \ if draggedDeckName == ontoDeckName \
or self._isParent(ontoDeckName, draggedDeckName) \ or self._isParent(ontoDeckName, draggedDeckName) \
or self._isAncestor(draggedDeckName, ontoDeckName): or self._isAncestor(draggedDeckName, ontoDeckName):
@ -307,19 +308,19 @@ class DeckManager:
else: else:
return True return True
def _isParent(self, parentDeckName, childDeckName): def _isParent(self, parentDeckName, childDeckName) -> Any:
return self._path(childDeckName) == self._path(parentDeckName) + [ self._basename(childDeckName) ] return self._path(childDeckName) == self._path(parentDeckName) + [ self._basename(childDeckName) ]
def _isAncestor(self, ancestorDeckName, descendantDeckName): def _isAncestor(self, ancestorDeckName, descendantDeckName) -> Any:
ancestorPath = self._path(ancestorDeckName) ancestorPath = self._path(ancestorDeckName)
return ancestorPath == self._path(descendantDeckName)[0:len(ancestorPath)] return ancestorPath == self._path(descendantDeckName)[0:len(ancestorPath)]
def _path(self, name): def _path(self, name) -> Any:
return name.split("::") return name.split("::")
def _basename(self, name): def _basename(self, name) -> Any:
return self._path(name)[-1] return self._path(name)[-1]
def _ensureParents(self, name): def _ensureParents(self, name) -> Any:
"Ensure parents exist, and return name with case matching parents." "Ensure parents exist, and return name with case matching parents."
s = "" s = ""
path = self._path(name) path = self._path(name)
@ -340,11 +341,11 @@ class DeckManager:
# Deck configurations # Deck configurations
############################################################# #############################################################
def allConf(self): def allConf(self) -> List:
"A list of all deck config." "A list of all deck config."
return list(self.dconf.values()) return list(self.dconf.values())
def confForDid(self, did): def confForDid(self, did) -> Any:
deck = self.get(did, default=False) deck = self.get(did, default=False)
assert deck assert deck
if 'conf' in deck: if 'conf' in deck:
@ -354,14 +355,14 @@ class DeckManager:
# dynamic decks have embedded conf # dynamic decks have embedded conf
return deck return deck
def getConf(self, confId): def getConf(self, confId) -> Any:
return self.dconf[str(confId)] return self.dconf[str(confId)]
def updateConf(self, g): def updateConf(self, g) -> None:
self.dconf[str(g['id'])] = g self.dconf[str(g['id'])] = g
self.save() self.save()
def confId(self, name, cloneFrom=None): def confId(self, name, cloneFrom=None) -> int:
"Create a new configuration and return id." "Create a new configuration and return id."
if cloneFrom is None: if cloneFrom is None:
cloneFrom = defaultConf cloneFrom = defaultConf
@ -376,7 +377,7 @@ class DeckManager:
self.save(c) self.save(c)
return id return id
def remConf(self, id): def remConf(self, id) -> None:
"Remove a configuration and update all decks using it." "Remove a configuration and update all decks using it."
assert int(id) != 1 assert int(id) != 1
self.col.modSchema(check=True) self.col.modSchema(check=True)
@ -389,18 +390,18 @@ class DeckManager:
g['conf'] = 1 g['conf'] = 1
self.save(g) self.save(g)
def setConf(self, grp, id): def setConf(self, grp, id) -> None:
grp['conf'] = id grp['conf'] = id
self.save(grp) self.save(grp)
def didsForConf(self, conf): def didsForConf(self, conf) -> List:
dids = [] dids = []
for deck in list(self.decks.values()): for deck in list(self.decks.values()):
if 'conf' in deck and deck['conf'] == conf['id']: if 'conf' in deck and deck['conf'] == conf['id']:
dids.append(deck['id']) dids.append(deck['id'])
return dids return dids
def restoreToDefault(self, conf): def restoreToDefault(self, conf) -> None:
oldOrder = conf['new']['order'] oldOrder = conf['new']['order']
new = copy.deepcopy(defaultConf) new = copy.deepcopy(defaultConf)
new['id'] = conf['id'] new['id'] = conf['id']
@ -414,29 +415,29 @@ class DeckManager:
# Deck utils # Deck utils
############################################################# #############################################################
def name(self, did, default=False): def name(self, did, default=False) -> Any:
deck = self.get(did, default=default) deck = self.get(did, default=default)
if deck: if deck:
return deck['name'] return deck['name']
return _("[no deck]") return _("[no deck]")
def nameOrNone(self, did): def nameOrNone(self, did) -> Any:
deck = self.get(did, default=False) deck = self.get(did, default=False)
if deck: if deck:
return deck['name'] return deck['name']
return None return None
def setDeck(self, cids, did): def setDeck(self, cids, did) -> None:
self.col.db.execute( self.col.db.execute(
"update cards set did=?,usn=?,mod=? where id in "+ "update cards set did=?,usn=?,mod=? where id in "+
ids2str(cids), did, self.col.usn(), intTime()) ids2str(cids), did, self.col.usn(), intTime())
def maybeAddToActive(self): def maybeAddToActive(self) -> None:
# reselect current deck, or default if current has disappeared # reselect current deck, or default if current has disappeared
c = self.current() c = self.current()
self.select(c['id']) self.select(c['id'])
def cids(self, did, children=False): def cids(self, did, children=False) -> Any:
if not children: if not children:
return self.col.db.list("select id from cards where did=?", did) return self.col.db.list("select id from cards where did=?", did)
dids = [did] dids = [did]
@ -445,14 +446,14 @@ class DeckManager:
return self.col.db.list("select id from cards where did in "+ return self.col.db.list("select id from cards where did in "+
ids2str(dids)) ids2str(dids))
def _recoverOrphans(self): def _recoverOrphans(self) -> None:
dids = list(self.decks.keys()) dids = list(self.decks.keys())
mod = self.col.db.mod mod = self.col.db.mod
self.col.db.execute("update cards set did = 1 where did not in "+ self.col.db.execute("update cards set did = 1 where did not in "+
ids2str(dids)) ids2str(dids))
self.col.db.mod = mod self.col.db.mod = mod
def _checkDeckTree(self): def _checkDeckTree(self) -> None:
decks = self.col.decks.all() decks = self.col.decks.all()
decks.sort(key=operator.itemgetter('name')) decks.sort(key=operator.itemgetter('name'))
names = set() names = set()
@ -480,25 +481,25 @@ class DeckManager:
names.add(deck['name']) names.add(deck['name'])
def checkIntegrity(self): def checkIntegrity(self) -> None:
self._recoverOrphans() self._recoverOrphans()
self._checkDeckTree() self._checkDeckTree()
# Deck selection # Deck selection
############################################################# #############################################################
def active(self): def active(self) -> Any:
"The currrently active dids. Make sure to copy before modifying." "The currrently active dids. Make sure to copy before modifying."
return self.col.conf['activeDecks'] return self.col.conf['activeDecks']
def selected(self): def selected(self) -> Any:
"The currently selected did." "The currently selected did."
return self.col.conf['curDeck'] return self.col.conf['curDeck']
def current(self): def current(self) -> Any:
return self.get(self.selected()) return self.get(self.selected())
def select(self, did): def select(self, did) -> None:
"Select a new branch." "Select a new branch."
# make sure arg is an int # make sure arg is an int
did = int(did) did = int(did)
@ -510,7 +511,7 @@ class DeckManager:
self.col.conf['activeDecks'] = [did] + [a[1] for a in actv] self.col.conf['activeDecks'] = [did] + [a[1] for a in actv]
self.changed = True self.changed = True
def children(self, did): def children(self, did) -> List[Tuple[Any, Any]]:
"All children of did, as (name, id)." "All children of did, as (name, id)."
name = self.get(did)['name'] name = self.get(did)['name']
actv = [] actv = []
@ -519,7 +520,7 @@ class DeckManager:
actv.append((g['name'], g['id'])) actv.append((g['name'], g['id']))
return actv return actv
def childDids(self, did, childMap): def childDids(self, did, childMap) -> List:
def gather(node, arr): def gather(node, arr):
for did, child in node.items(): for did, child in node.items():
arr.append(did) arr.append(did)
@ -529,7 +530,7 @@ class DeckManager:
gather(childMap[did], arr) gather(childMap[did], arr)
return arr return arr
def childMap(self): def childMap(self) -> Dict[Any, Dict[Any, dict]]:
nameMap = self.nameMap() nameMap = self.nameMap()
childMap = {} childMap = {}
@ -547,7 +548,7 @@ class DeckManager:
return childMap return childMap
def parents(self, did, nameMap=None): def parents(self, did, nameMap=None) -> List:
"All parents of did." "All parents of did."
# get parent and grandparent names # get parent and grandparent names
parents = [] parents = []
@ -565,7 +566,7 @@ class DeckManager:
parents[c] = deck parents[c] = deck
return parents return parents
def parentsByName(self, name): def parentsByName(self, name) -> List:
"All existing parents of name" "All existing parents of name"
if "::" not in name: if "::" not in name:
return [] return []
@ -581,13 +582,13 @@ class DeckManager:
return parents return parents
def nameMap(self): def nameMap(self) -> dict:
return dict((d['name'], d) for d in self.decks.values()) return dict((d['name'], d) for d in self.decks.values())
# Sync handling # Sync handling
########################################################################## ##########################################################################
def beforeUpload(self): def beforeUpload(self) -> None:
for d in self.all(): for d in self.all():
d['usn'] = 0 d['usn'] = 0
for c in self.allConf(): for c in self.allConf():
@ -597,19 +598,19 @@ class DeckManager:
# Dynamic decks # Dynamic decks
########################################################################## ##########################################################################
def newDyn(self, name): def newDyn(self, name) -> int:
"Return a new dynamic deck and set it as the current deck." "Return a new dynamic deck and set it as the current deck."
did = self.id(name, type=defaultDynamicDeck) did = self.id(name, type=defaultDynamicDeck)
self.select(did) self.select(did)
return did return did
def isDyn(self, did): def isDyn(self, did) -> Any:
return self.get(did)['dyn'] return self.get(did)['dyn']
@staticmethod @staticmethod
def normalizeName(name): def normalizeName(name) -> str:
return unicodedata.normalize("NFC", name.lower()) return unicodedata.normalize("NFC", name.lower())
@staticmethod @staticmethod
def equalName(name1, name2): def equalName(name1, name2) -> bool:
return DeckManager.normalizeName(name1) == DeckManager.normalizeName(name2) return DeckManager.normalizeName(name1) == DeckManager.normalizeName(name2)

View File

@ -1,20 +1,21 @@
from typing import Any
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
class AnkiError(Exception): class AnkiError(Exception):
def __init__(self, type, **data): def __init__(self, type, **data) -> None:
super().__init__() super().__init__()
self.type = type self.type = type
self.data = data self.data = data
def __str__(self): def __str__(self) -> Any:
m = self.type m = self.type
if self.data: if self.data:
m += ": %s" % repr(self.data) m += ": %s" % repr(self.data)
return m return m
class DeckRenameError(Exception): class DeckRenameError(Exception):
def __init__(self, description): def __init__(self, description) -> None:
super().__init__() super().__init__()
self.description = description self.description = description
def __str__(self): def __str__(self):

View File

@ -9,24 +9,25 @@ from anki.lang import _
from anki.utils import ids2str, splitFields, namedtmp, stripHTML from anki.utils import ids2str, splitFields, namedtmp, stripHTML
from anki.hooks import runHook from anki.hooks import runHook
from anki.storage import Collection from anki.storage import Collection
from typing import Any, Dict, List, Tuple
class Exporter: class Exporter:
includeHTML: typing.Union[bool, None] = None includeHTML: typing.Union[bool, None] = None
def __init__(self, col, did=None): def __init__(self, col, did=None) -> None:
self.col = col self.col = col
self.did = did self.did = did
def doExport(self, path): def doExport(self, path) -> None:
raise Exception("not implemented") raise Exception("not implemented")
def exportInto(self, path): def exportInto(self, path) -> None:
self._escapeCount = 0 self._escapeCount = 0
file = open(path, "wb") file = open(path, "wb")
self.doExport(file) self.doExport(file)
file.close() file.close()
def processText(self, text): def processText(self, text) -> str:
if self.includeHTML is False: if self.includeHTML is False:
text = self.stripHTML(text) text = self.stripHTML(text)
@ -34,7 +35,7 @@ class Exporter:
return text return text
def escapeText(self, text): def escapeText(self, text) -> str:
"Escape newlines, tabs, CSS and quotechar." "Escape newlines, tabs, CSS and quotechar."
# fixme: we should probably quote fields with newlines # fixme: we should probably quote fields with newlines
# instead of converting them to spaces # instead of converting them to spaces
@ -46,7 +47,7 @@ class Exporter:
text = "\"" + text.replace("\"", "\"\"") + "\"" text = "\"" + text.replace("\"", "\"\"") + "\""
return text return text
def stripHTML(self, text): def stripHTML(self, text) -> str:
# very basic conversion to text # very basic conversion to text
s = text s = text
s = re.sub(r"(?i)<(br ?/?|div|p)>", " ", s) s = re.sub(r"(?i)<(br ?/?|div|p)>", " ", s)
@ -56,7 +57,7 @@ class Exporter:
s = s.strip() s = s.strip()
return s return s
def cardIds(self): def cardIds(self) -> Any:
if not self.did: if not self.did:
cids = self.col.db.list("select id from cards") cids = self.col.db.list("select id from cards")
else: else:
@ -73,10 +74,10 @@ class TextCardExporter(Exporter):
ext = ".txt" ext = ".txt"
includeHTML = True includeHTML = True
def __init__(self, col): def __init__(self, col) -> None:
Exporter.__init__(self, col) Exporter.__init__(self, col)
def doExport(self, file): def doExport(self, file) -> None:
ids = sorted(self.cardIds()) ids = sorted(self.cardIds())
strids = ids2str(ids) strids = ids2str(ids)
def esc(s): def esc(s):
@ -100,11 +101,11 @@ class TextNoteExporter(Exporter):
includeTags = True includeTags = True
includeHTML = True includeHTML = True
def __init__(self, col): def __init__(self, col) -> None:
Exporter.__init__(self, col) Exporter.__init__(self, col)
self.includeID = False self.includeID = False
def doExport(self, file): def doExport(self, file) -> None:
cardIds = self.cardIds() cardIds = self.cardIds()
data = [] data = []
for id, flds, tags in self.col.db.execute(""" for id, flds, tags in self.col.db.execute("""
@ -137,10 +138,10 @@ class AnkiExporter(Exporter):
includeSched: typing.Union[bool, None] = False includeSched: typing.Union[bool, None] = False
includeMedia = True includeMedia = True
def __init__(self, col): def __init__(self, col) -> None:
Exporter.__init__(self, col) Exporter.__init__(self, col)
def exportInto(self, path): def exportInto(self, path) -> None:
# sched info+v2 scheduler not compatible w/ older clients # sched info+v2 scheduler not compatible w/ older clients
self._v2sched = self.col.schedVer() != 1 and self.includeSched self._v2sched = self.col.schedVer() != 1 and self.includeSched
@ -253,15 +254,15 @@ class AnkiExporter(Exporter):
self.postExport() self.postExport()
self.dst.close() self.dst.close()
def postExport(self): def postExport(self) -> None:
# overwrite to apply customizations to the deck before it's closed, # overwrite to apply customizations to the deck before it's closed,
# such as update the deck description # such as update the deck description
pass pass
def removeSystemTags(self, tags): def removeSystemTags(self, tags) -> Any:
return self.src.tags.remFromStr("marked leech", tags) return self.src.tags.remFromStr("marked leech", tags)
def _modelHasMedia(self, model, fname): def _modelHasMedia(self, model, fname) -> bool:
# First check the styling # First check the styling
if fname in model["css"]: if fname in model["css"]:
return True return True
@ -290,7 +291,7 @@ class AnkiPackageExporter(AnkiExporter):
z.writestr("media", json.dumps(media)) z.writestr("media", json.dumps(media))
z.close() z.close()
def doExport(self, z, path): def doExport(self, z, path) -> Dict[str, str]:
# export into the anki2 file # export into the anki2 file
colfile = path.replace(".apkg", ".anki2") colfile = path.replace(".apkg", ".anki2")
AnkiExporter.exportInto(self, colfile) AnkiExporter.exportInto(self, colfile)
@ -314,7 +315,7 @@ class AnkiPackageExporter(AnkiExporter):
shutil.rmtree(path.replace(".apkg", ".media")) shutil.rmtree(path.replace(".apkg", ".media"))
return media return media
def _exportMedia(self, z, files, fdir): def _exportMedia(self, z, files, fdir) -> Dict[str, str]:
media = {} media = {}
for c, file in enumerate(files): for c, file in enumerate(files):
cStr = str(c) cStr = str(c)
@ -331,14 +332,14 @@ class AnkiPackageExporter(AnkiExporter):
return media return media
def prepareMedia(self): def prepareMedia(self) -> None:
# chance to move each file in self.mediaFiles into place before media # chance to move each file in self.mediaFiles into place before media
# is zipped up # is zipped up
pass pass
# create a dummy collection to ensure older clients don't try to read # create a dummy collection to ensure older clients don't try to read
# data they don't understand # data they don't understand
def _addDummyCollection(self, zip): def _addDummyCollection(self, zip) -> None:
path = namedtmp("dummy.anki2") path = namedtmp("dummy.anki2")
c = Collection(path) c = Collection(path)
n = c.newNote() n = c.newNote()
@ -383,7 +384,7 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter):
# Export modules # Export modules
########################################################################## ##########################################################################
def exporters(): def exporters() -> List[Tuple[str, Any]]:
def id(obj): def id(obj):
return ("%s (*%s)" % (obj.key, obj.ext), obj) return ("%s (*%s)" % (obj.key, obj.ext), obj)
exps = [ exps = [

View File

@ -9,6 +9,7 @@ import unicodedata
from anki.utils import ids2str, splitFields, joinFields, intTime, fieldChecksum, stripHTMLMedia from anki.utils import ids2str, splitFields, joinFields, intTime, fieldChecksum, stripHTMLMedia
from anki.consts import * from anki.consts import *
from anki.hooks import * from anki.hooks import *
from typing import Any, List, Optional, Tuple
# Find # Find
@ -16,7 +17,7 @@ from anki.hooks import *
class Finder: class Finder:
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self.search = dict( self.search = dict(
added=self._findAdded, added=self._findAdded,
@ -35,7 +36,7 @@ class Finder:
self.search['is'] = self._findCardState self.search['is'] = self._findCardState
runHook("search", self.search) runHook("search", self.search)
def findCards(self, query, order=False): def findCards(self, query, order=False) -> Any:
"Return a list of card ids for QUERY." "Return a list of card ids for QUERY."
tokens = self._tokenize(query) tokens = self._tokenize(query)
preds, args = self._where(tokens) preds, args = self._where(tokens)
@ -52,7 +53,7 @@ class Finder:
res.reverse() res.reverse()
return res return res
def findNotes(self, query): def findNotes(self, query) -> Any:
tokens = self._tokenize(query) tokens = self._tokenize(query)
preds, args = self._where(tokens) preds, args = self._where(tokens)
if preds is None: if preds is None:
@ -73,7 +74,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
# Tokenizing # Tokenizing
###################################################################### ######################################################################
def _tokenize(self, query): def _tokenize(self, query) -> List:
inQuote = False inQuote = False
tokens = [] tokens = []
token = "" token = ""
@ -127,7 +128,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
# Query building # Query building
###################################################################### ######################################################################
def _where(self, tokens): def _where(self, tokens) -> Tuple[Any, Optional[List[str]]]:
# state and query # state and query
s = dict(isnot=False, isor=False, join=False, q="", bad=False) s = dict(isnot=False, isor=False, join=False, q="", bad=False)
args = [] args = []
@ -185,7 +186,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
return None, None return None, None
return s['q'], args return s['q'], args
def _query(self, preds, order): def _query(self, preds, order) -> str:
# can we skip the note table? # can we skip the note table?
if "n." not in preds and "n." not in order: if "n." not in preds and "n." not in order:
sql = "select c.id from cards c where " sql = "select c.id from cards c where "
@ -204,7 +205,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
# Ordering # Ordering
###################################################################### ######################################################################
def _order(self, order): def _order(self, order) -> Tuple[Any, Any]:
if not order: if not order:
return "", False return "", False
elif order is not True: elif order is not True:
@ -241,7 +242,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
# Commands # Commands
###################################################################### ######################################################################
def _findTag(self, args): def _findTag(self, args) -> str:
(val, args) = args (val, args) = args
if val == "none": if val == "none":
return 'n.tags = ""' return 'n.tags = ""'
@ -253,7 +254,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
args.append(val) args.append(val)
return "n.tags like ? escape '\\'" return "n.tags like ? escape '\\'"
def _findCardState(self, args): def _findCardState(self, args) -> Optional[str]:
(val, args) = args (val, args) = args
if val in ("review", "new", "learn"): if val in ("review", "new", "learn"):
if val == "review": if val == "review":
@ -273,7 +274,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
(c.queue = 1 and c.due <= %d)""" % ( (c.queue = 1 and c.due <= %d)""" % (
self.col.sched.today, self.col.sched.dayCutoff) self.col.sched.today, self.col.sched.dayCutoff)
def _findFlag(self, args): def _findFlag(self, args) -> Optional[str]:
(val, args) = args (val, args) = args
if not val or len(val)!=1 or val not in "01234": if not val or len(val)!=1 or val not in "01234":
return return
@ -281,7 +282,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
mask = 2**3 - 1 mask = 2**3 - 1
return "(c.flags & %d) == %d" % (mask, val) return "(c.flags & %d) == %d" % (mask, val)
def _findRated(self, args): def _findRated(self, args) -> Optional[str]:
# days(:optional_ease) # days(:optional_ease)
(val, args) = args (val, args) = args
r = val.split(":") r = val.split(":")
@ -300,7 +301,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
return ("c.id in (select cid from revlog where id>%d %s)" % return ("c.id in (select cid from revlog where id>%d %s)" %
(cutoff, ease)) (cutoff, ease))
def _findAdded(self, args): def _findAdded(self, args) -> Optional[str]:
(val, args) = args (val, args) = args
try: try:
days = int(val) days = int(val)
@ -309,7 +310,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
cutoff = (self.col.sched.dayCutoff - 86400*days)*1000 cutoff = (self.col.sched.dayCutoff - 86400*days)*1000
return "c.id > %d" % cutoff return "c.id > %d" % cutoff
def _findProp(self, args): def _findProp(self, args) -> Optional[str]:
# extract # extract
(val, args) = args (val, args) = args
m = re.match("(^.+?)(<=|>=|!=|=|<|>)(.+?$)", val) m = re.match("(^.+?)(<=|>=|!=|=|<|>)(.+?$)", val)
@ -340,31 +341,31 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
q.append("(%s %s %s)" % (prop, cmp, val)) q.append("(%s %s %s)" % (prop, cmp, val))
return " and ".join(q) return " and ".join(q)
def _findText(self, val, args): def _findText(self, val, args) -> str:
val = val.replace("*", "%") val = val.replace("*", "%")
args.append("%"+val+"%") args.append("%"+val+"%")
args.append("%"+val+"%") args.append("%"+val+"%")
return "(n.sfld like ? escape '\\' or n.flds like ? escape '\\')" return "(n.sfld like ? escape '\\' or n.flds like ? escape '\\')"
def _findNids(self, args): def _findNids(self, args) -> Optional[str]:
(val, args) = args (val, args) = args
if re.search("[^0-9,]", val): if re.search("[^0-9,]", val):
return return
return "n.id in (%s)" % val return "n.id in (%s)" % val
def _findCids(self, args): def _findCids(self, args) -> Optional[str]:
(val, args) = args (val, args) = args
if re.search("[^0-9,]", val): if re.search("[^0-9,]", val):
return return
return "c.id in (%s)" % val return "c.id in (%s)" % val
def _findMid(self, args): def _findMid(self, args) -> Optional[str]:
(val, args) = args (val, args) = args
if re.search("[^0-9]", val): if re.search("[^0-9]", val):
return return
return "n.mid = %s" % val return "n.mid = %s" % val
def _findModel(self, args): def _findModel(self, args) -> str:
(val, args) = args (val, args) = args
ids = [] ids = []
val = val.lower() val = val.lower()
@ -373,7 +374,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
ids.append(m['id']) ids.append(m['id'])
return "n.mid in %s" % ids2str(ids) return "n.mid in %s" % ids2str(ids)
def _findDeck(self, args): def _findDeck(self, args) -> Optional[str]:
# if searching for all decks, skip # if searching for all decks, skip
(val, args) = args (val, args) = args
if val == "*": if val == "*":
@ -404,7 +405,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
sids = ids2str(ids) sids = ids2str(ids)
return "c.did in %s or c.odid in %s" % (sids, sids) return "c.did in %s or c.odid in %s" % (sids, sids)
def _findTemplate(self, args): def _findTemplate(self, args) -> str:
# were we given an ordinal number? # were we given an ordinal number?
(val, args) = args (val, args) = args
try: try:
@ -428,7 +429,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
m['id'], t['ord'])) m['id'], t['ord']))
return " or ".join(lims) return " or ".join(lims)
def _findField(self, field, val): def _findField(self, field, val) -> Optional[str]:
field = field.lower() field = field.lower()
val = val.replace("*", "%") val = val.replace("*", "%")
# find models that have that field # find models that have that field
@ -460,7 +461,7 @@ where mid in %s and flds like ? escape '\\'""" % (
return "0" return "0"
return "n.id in %s" % ids2str(nids) return "n.id in %s" % ids2str(nids)
def _findDupes(self, args): def _findDupes(self, args) -> Optional[str]:
# caller must call stripHTMLMedia on passed val # caller must call stripHTMLMedia on passed val
(val, args) = args (val, args) = args
try: try:
@ -479,7 +480,7 @@ where mid in %s and flds like ? escape '\\'""" % (
# Find and replace # Find and replace
########################################################################## ##########################################################################
def findReplace(col, nids, src, dst, regex=False, field=None, fold=True): def findReplace(col, nids, src, dst, regex=False, field=None, fold=True) -> int:
"Find and replace fields in a note." "Find and replace fields in a note."
mmap = {} mmap = {}
if field: if field:
@ -529,7 +530,7 @@ def findReplace(col, nids, src, dst, regex=False, field=None, fold=True):
col.genCards(nids) col.genCards(nids)
return len(d) return len(d)
def fieldNames(col, downcase=True): def fieldNames(col, downcase=True) -> List:
fields = set() fields = set()
for m in col.models.all(): for m in col.models.all():
for f in m['flds']: for f in m['flds']:
@ -538,7 +539,7 @@ def fieldNames(col, downcase=True):
fields.add(name) fields.add(name)
return list(fields) return list(fields)
def fieldNamesForNotes(col, nids): def fieldNamesForNotes(col, nids) -> List:
fields = set() fields = set()
mids = col.db.list("select distinct mid from notes where id in %s" % ids2str(nids)) mids = col.db.list("select distinct mid from notes where id in %s" % ids2str(nids))
for mid in mids: for mid in mids:
@ -551,7 +552,7 @@ def fieldNamesForNotes(col, nids):
# Find duplicates # Find duplicates
########################################################################## ##########################################################################
# returns array of ("dupestr", [nids]) # returns array of ("dupestr", [nids])
def findDupes(col, fieldName, search=""): def findDupes(col, fieldName, search="") -> List[Tuple[Any, List]]:
# limit search to notes with applicable field name # limit search to notes with applicable field name
if search: if search:
search = "("+search+") " search = "("+search+") "

View File

@ -21,7 +21,7 @@ from typing import Dict, List, Callable, Any
_hooks: Dict[str, List[Callable[..., Any]]] = {} _hooks: Dict[str, List[Callable[..., Any]]] = {}
def runHook(hook, *args): def runHook(hook, *args) -> None:
"Run all functions on hook." "Run all functions on hook."
hook = _hooks.get(hook, None) hook = _hooks.get(hook, None)
if hook: if hook:
@ -32,7 +32,7 @@ def runHook(hook, *args):
hook.remove(func) hook.remove(func)
raise raise
def runFilter(hook, arg, *args): def runFilter(hook, arg, *args) -> Any:
hook = _hooks.get(hook, None) hook = _hooks.get(hook, None)
if hook: if hook:
for func in hook: for func in hook:
@ -43,14 +43,14 @@ def runFilter(hook, arg, *args):
raise raise
return arg return arg
def addHook(hook, func): def addHook(hook, func) -> None:
"Add a function to hook. Ignore if already on hook." "Add a function to hook. Ignore if already on hook."
if not _hooks.get(hook, None): if not _hooks.get(hook, None):
_hooks[hook] = [] _hooks[hook] = []
if func not in _hooks[hook]: if func not in _hooks[hook]:
_hooks[hook].append(func) _hooks[hook].append(func)
def remHook(hook, func): def remHook(hook, func) -> None:
"Remove a function if is on hook." "Remove a function if is on hook."
hook = _hooks.get(hook, []) hook = _hooks.get(hook, [])
if func in hook: if func in hook:
@ -59,7 +59,7 @@ def remHook(hook, func):
# Instrumenting # Instrumenting
############################################################################## ##############################################################################
def wrap(old, new, pos="after"): def wrap(old, new, pos="after") -> Callable:
"Override an existing function." "Override an existing function."
def repl(*args, **kwargs): def repl(*args, **kwargs):
if pos == "after": if pos == "after":

View File

@ -8,6 +8,7 @@ from anki.storage import Collection
from anki.utils import intTime, splitFields, joinFields from anki.utils import intTime, splitFields, joinFields
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.lang import _ from anki.lang import _
from typing import Any
GUID = 1 GUID = 1
MID = 2 MID = 2
@ -27,7 +28,7 @@ class Anki2Importer(Importer):
self._decks = {} self._decks = {}
self.mustResetLearning = False self.mustResetLearning = False
def run(self, media=None): def run(self, media=None) -> None:
self._prepareFiles() self._prepareFiles()
if media is not None: if media is not None:
# Anki1 importer has provided us with a custom media folder # Anki1 importer has provided us with a custom media folder
@ -37,7 +38,7 @@ class Anki2Importer(Importer):
finally: finally:
self.src.close(save=False) self.src.close(save=False)
def _prepareFiles(self): def _prepareFiles(self) -> None:
importingV2 = self.file.endswith(".anki21") importingV2 = self.file.endswith(".anki21")
self.mustResetLearning = False self.mustResetLearning = False
@ -49,7 +50,7 @@ class Anki2Importer(Importer):
if self.src.db.scalar("select 1 from cards where queue != 0 limit 1"): if self.src.db.scalar("select 1 from cards where queue != 0 limit 1"):
self.mustResetLearning = True self.mustResetLearning = True
def _import(self): def _import(self) -> None:
self._decks = {} self._decks = {}
if self.deckPrefix: if self.deckPrefix:
id = self.dst.decks.id(self.deckPrefix) id = self.dst.decks.id(self.deckPrefix)
@ -68,13 +69,13 @@ class Anki2Importer(Importer):
# Notes # Notes
###################################################################### ######################################################################
def _logNoteRow(self, action, noteRow): def _logNoteRow(self, action, noteRow) -> None:
self.log.append("[%s] %s" % ( self.log.append("[%s] %s" % (
action, action,
noteRow[6].replace("\x1f", ", ") noteRow[6].replace("\x1f", ", ")
)) ))
def _importNotes(self): def _importNotes(self) -> None:
# build guid -> (id,mod,mid) hash & map of existing note ids # build guid -> (id,mod,mid) hash & map of existing note ids
self._notes = {} self._notes = {}
existing = {} existing = {}
@ -185,7 +186,7 @@ class Anki2Importer(Importer):
# determine if note is a duplicate, and adjust mid and/or guid as required # determine if note is a duplicate, and adjust mid and/or guid as required
# returns true if note should be added # returns true if note should be added
def _uniquifyNote(self, note): def _uniquifyNote(self, note) -> bool:
origGuid = note[GUID] origGuid = note[GUID]
srcMid = note[MID] srcMid = note[MID]
dstMid = self._mid(srcMid) dstMid = self._mid(srcMid)
@ -207,11 +208,11 @@ class Anki2Importer(Importer):
# the schemas don't match, we increment the mid and try again, creating a # the schemas don't match, we increment the mid and try again, creating a
# new model if necessary. # new model if necessary.
def _prepareModels(self): def _prepareModels(self) -> None:
"Prepare index of schema hashes." "Prepare index of schema hashes."
self._modelMap = {} self._modelMap = {}
def _mid(self, srcMid): def _mid(self, srcMid) -> Any:
"Return local id for remote MID." "Return local id for remote MID."
# already processed this mid? # already processed this mid?
if srcMid in self._modelMap: if srcMid in self._modelMap:
@ -248,7 +249,7 @@ class Anki2Importer(Importer):
# Decks # Decks
###################################################################### ######################################################################
def _did(self, did): def _did(self, did) -> Any:
"Given did in src col, return local id." "Given did in src col, return local id."
# already converted? # already converted?
if did in self._decks: if did in self._decks:
@ -295,7 +296,7 @@ class Anki2Importer(Importer):
# Cards # Cards
###################################################################### ######################################################################
def _importCards(self): def _importCards(self) -> None:
if self.mustResetLearning: if self.mustResetLearning:
self.src.changeSchedulerVer(2) self.src.changeSchedulerVer(2)
# build map of (guid, ord) -> cid and used id cache # build map of (guid, ord) -> cid and used id cache
@ -382,7 +383,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog)
# note: this func only applies to imports of .anki2. for .apkg files, the # note: this func only applies to imports of .anki2. for .apkg files, the
# apkg importer does the copying # apkg importer does the copying
def _importStaticMedia(self): def _importStaticMedia(self) -> None:
# Import any '_foo' prefixed media files regardless of whether # Import any '_foo' prefixed media files regardless of whether
# they're used on notes or not # they're used on notes or not
dir = self.src.media.dir() dir = self.src.media.dir()
@ -392,7 +393,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog)
if fname.startswith("_") and not self.dst.media.have(fname): if fname.startswith("_") and not self.dst.media.have(fname):
self._writeDstMedia(fname, self._srcMediaData(fname)) self._writeDstMedia(fname, self._srcMediaData(fname))
def _mediaData(self, fname, dir=None): def _mediaData(self, fname, dir=None) -> bytes:
if not dir: if not dir:
dir = self.src.media.dir() dir = self.src.media.dir()
path = os.path.join(dir, fname) path = os.path.join(dir, fname)
@ -402,15 +403,15 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog)
except (IOError, OSError): except (IOError, OSError):
return return
def _srcMediaData(self, fname): def _srcMediaData(self, fname) -> bytes:
"Data for FNAME in src collection." "Data for FNAME in src collection."
return self._mediaData(fname, self.src.media.dir()) return self._mediaData(fname, self.src.media.dir())
def _dstMediaData(self, fname): def _dstMediaData(self, fname) -> bytes:
"Data for FNAME in dst collection." "Data for FNAME in dst collection."
return self._mediaData(fname, self.dst.media.dir()) return self._mediaData(fname, self.dst.media.dir())
def _writeDstMedia(self, fname, data): def _writeDstMedia(self, fname, data) -> None:
path = os.path.join(self.dst.media.dir(), path = os.path.join(self.dst.media.dir(),
unicodedata.normalize("NFC", fname)) unicodedata.normalize("NFC", fname))
try: try:
@ -420,7 +421,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog)
# the user likely used subdirectories # the user likely used subdirectories
pass pass
def _mungeMedia(self, mid, fields): def _mungeMedia(self, mid, fields) -> str:
fields = splitFields(fields) fields = splitFields(fields)
def repl(match): def repl(match):
fname = match.group("fname") fname = match.group("fname")
@ -450,7 +451,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog)
# Post-import cleanup # Post-import cleanup
###################################################################### ######################################################################
def _postImport(self): def _postImport(self) -> None:
for did in list(self._decks.values()): for did in list(self._decks.values()):
self.col.sched.maybeRandomizeDeck(did) self.col.sched.maybeRandomizeDeck(did)
# make sure new position is correct # make sure new position is correct

View File

@ -7,6 +7,7 @@ import unicodedata
import json import json
from anki.utils import tmpfile from anki.utils import tmpfile
from anki.importing.anki2 import Anki2Importer from anki.importing.anki2 import Anki2Importer
from typing import Any
class AnkiPackageImporter(Anki2Importer): class AnkiPackageImporter(Anki2Importer):
@ -16,7 +17,7 @@ class AnkiPackageImporter(Anki2Importer):
self.nameToNum = {} self.nameToNum = {}
self.zip = None self.zip = None
def run(self): def run(self) -> None:
# extract the deck from the zip file # extract the deck from the zip file
self.zip = z = zipfile.ZipFile(self.file) self.zip = z = zipfile.ZipFile(self.file)
# v2 scheduler? # v2 scheduler?
@ -52,7 +53,7 @@ class AnkiPackageImporter(Anki2Importer):
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(z.read(c)) f.write(z.read(c))
def _srcMediaData(self, fname): def _srcMediaData(self, fname) -> Any:
if fname in self.nameToNum: if fname in self.nameToNum:
return self.zip.read(self.nameToNum[fname]) return self.zip.read(self.nameToNum[fname])
return None return None

View File

@ -3,6 +3,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki.utils import maxID from anki.utils import maxID
from typing import Any
# Base importer # Base importer
########################################################################## ##########################################################################
@ -12,14 +13,14 @@ class Importer:
needMapper = False needMapper = False
needDelimiter = False needDelimiter = False
def __init__(self, col, file): def __init__(self, col, file) -> None:
self.file = file self.file = file
self.log = [] self.log = []
self.col = col self.col = col
self.total = 0 self.total = 0
self.dst = None self.dst = None
def run(self): def run(self) -> None:
pass pass
# Timestamps # Timestamps
@ -28,9 +29,9 @@ class Importer:
# and a previous import may have created timestamps in the future, so we # and a previous import may have created timestamps in the future, so we
# need to make sure our starting point is safe. # need to make sure our starting point is safe.
def _prepareTS(self): def _prepareTS(self) -> None:
self._ts = maxID(self.dst.db) self._ts = maxID(self.dst.db)
def ts(self): def ts(self) -> Any:
self._ts += 1 self._ts += 1
return self._ts return self._ts

View File

@ -7,6 +7,7 @@ import re
from anki.importing.noteimp import NoteImporter, ForeignNote from anki.importing.noteimp import NoteImporter, ForeignNote
from anki.lang import _ from anki.lang import _
from typing import List
class TextImporter(NoteImporter): class TextImporter(NoteImporter):
@ -22,7 +23,7 @@ class TextImporter(NoteImporter):
self.tagsToAdd = [] self.tagsToAdd = []
self.numFields = 0 self.numFields = 0
def foreignNotes(self): def foreignNotes(self) -> List[ForeignNote]:
self.open() self.open()
# process all lines # process all lines
log = [] log = []
@ -60,12 +61,12 @@ class TextImporter(NoteImporter):
# load & look for the right pattern # load & look for the right pattern
self.cacheFile() self.cacheFile()
def cacheFile(self): def cacheFile(self) -> None:
"Read file into self.lines if not already there." "Read file into self.lines if not already there."
if not self.fileobj: if not self.fileobj:
self.openFile() self.openFile()
def openFile(self): def openFile(self) -> None:
self.dialect = None self.dialect = None
self.fileobj = open(self.file, "r", encoding='utf-8-sig') self.fileobj = open(self.file, "r", encoding='utf-8-sig')
self.data = self.fileobj.read() self.data = self.fileobj.read()
@ -81,7 +82,7 @@ class TextImporter(NoteImporter):
if not self.dialect and not self.delimiter: if not self.dialect and not self.delimiter:
raise Exception("unknownFormat") raise Exception("unknownFormat")
def updateDelimiter(self): def updateDelimiter(self) -> None:
def err(): def err():
raise Exception("unknownFormat") raise Exception("unknownFormat")
self.dialect = None self.dialect = None
@ -126,7 +127,7 @@ class TextImporter(NoteImporter):
self.open() self.open()
return self.numFields return self.numFields
def noteFromFields(self, fields): def noteFromFields(self, fields) -> ForeignNote:
note = ForeignNote() note = ForeignNote()
note.fields.extend([x for x in fields]) note.fields.extend([x for x in fields])
note.tags.extend(self.tagsToAdd) note.tags.extend(self.tagsToAdd)

View File

@ -101,7 +101,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""):
def fields(self): def fields(self):
return self._fields return self._fields
def _mungeField(self, fld): def _mungeField(self, fld) -> str:
# \n -> br # \n -> br
fld = re.sub("\r?\n", "<br>", fld) fld = re.sub("\r?\n", "<br>", fld)
# latex differences # latex differences
@ -110,7 +110,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""):
fld = re.sub("<audio src=\"(.+?)\">(</audio>)?", "[sound:\\1]", fld) fld = re.sub("<audio src=\"(.+?)\">(</audio>)?", "[sound:\\1]", fld)
return fld return fld
def _addFronts(self, notes, model=None, fields=("f", "b")): def _addFronts(self, notes, model=None, fields=("f", "b")) -> None:
data = [] data = []
for orig in notes: for orig in notes:
# create a foreign note object # create a foreign note object
@ -135,7 +135,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""):
# import # import
self.importNotes(data) self.importNotes(data)
def _addFrontBacks(self, notes): def _addFrontBacks(self, notes) -> None:
m = addBasicModel(self.col) m = addBasicModel(self.col)
m['name'] = "Mnemosyne-FrontBack" m['name'] = "Mnemosyne-FrontBack"
mm = self.col.models mm = self.col.models
@ -145,7 +145,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""):
mm.addTemplate(m, t) mm.addTemplate(m, t)
self._addFronts(notes, m) self._addFronts(notes, m)
def _addVocabulary(self, notes): def _addVocabulary(self, notes) -> None:
mm = self.col.models mm = self.col.models
m = mm.new("Mnemosyne-Vocabulary") m = mm.new("Mnemosyne-Vocabulary")
for f in "Expression", "Pronunciation", "Meaning", "Notes": for f in "Expression", "Pronunciation", "Meaning", "Notes":
@ -164,7 +164,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""):
mm.add(m) mm.add(m)
self._addFronts(notes, m, fields=("f", "p_1", "m_1", "n")) self._addFronts(notes, m, fields=("f", "p_1", "m_1", "n"))
def _addCloze(self, notes): def _addCloze(self, notes) -> None:
data = [] data = []
notes = list(notes.values()) notes = list(notes.values())
for orig in notes: for orig in notes:

View File

@ -12,20 +12,21 @@ from anki.utils import fieldChecksum, guid64, timestampID, \
joinFields, intTime, splitFields joinFields, intTime, splitFields
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.lang import ngettext from anki.lang import ngettext
from typing import Any, List, Optional
# Stores a list of fields, tags and deck # Stores a list of fields, tags and deck
###################################################################### ######################################################################
class ForeignNote: class ForeignNote:
"An temporary object storing fields and attributes." "An temporary object storing fields and attributes."
def __init__(self): def __init__(self) -> None:
self.fields = [] self.fields = []
self.tags = [] self.tags = []
self.deck = None self.deck = None
self.cards = {} # map of ord -> card self.cards = {} # map of ord -> card
class ForeignCard: class ForeignCard:
def __init__(self): def __init__(self) -> None:
self.due = 0 self.due = 0
self.ivl = 1 self.ivl = 1
self.factor = STARTING_FACTOR self.factor = STARTING_FACTOR
@ -66,11 +67,11 @@ class NoteImporter(Importer):
c = self.foreignNotes() c = self.foreignNotes()
self.importNotes(c) self.importNotes(c)
def fields(self): def fields(self) -> int:
"The number of fields." "The number of fields."
return 0 return 0
def initMapping(self): def initMapping(self) -> None:
flds = [f['name'] for f in self.model['flds']] flds = [f['name'] for f in self.model['flds']]
# truncate to provided count # truncate to provided count
flds = flds[0:self.fields()] flds = flds[0:self.fields()]
@ -81,18 +82,18 @@ class NoteImporter(Importer):
flds = flds + [None] * (self.fields() - len(flds)) flds = flds + [None] * (self.fields() - len(flds))
self.mapping = flds self.mapping = flds
def mappingOk(self): def mappingOk(self) -> bool:
return self.model['flds'][0]['name'] in self.mapping return self.model['flds'][0]['name'] in self.mapping
def foreignNotes(self): def foreignNotes(self) -> List:
"Return a list of foreign notes for importing." "Return a list of foreign notes for importing."
return [] return []
def open(self): def open(self) -> None:
"Open file and ensure it's in the right format." "Open file and ensure it's in the right format."
return return
def importNotes(self, notes): def importNotes(self, notes) -> None:
"Convert each card into a note, apply attributes and add to col." "Convert each card into a note, apply attributes and add to col."
assert self.mappingOk() assert self.mappingOk()
# note whether tags are mapped # note whether tags are mapped
@ -219,7 +220,7 @@ This can happen when you have empty fields or when you have not mapped the \
content in the text file to the correct fields.""")) content in the text file to the correct fields."""))
self.total = len(self._ids) self.total = len(self._ids)
def newData(self, n): def newData(self, n) -> Optional[list]:
id = self._nextID id = self._nextID
self._nextID += 1 self._nextID += 1
self._ids.append(id) self._ids.append(id)
@ -233,12 +234,12 @@ content in the text file to the correct fields."""))
intTime(), self.col.usn(), self.col.tags.join(n.tags), intTime(), self.col.usn(), self.col.tags.join(n.tags),
n.fieldsStr, "", "", 0, ""] n.fieldsStr, "", "", 0, ""]
def addNew(self, rows): def addNew(self, rows) -> None:
self.col.db.executemany( self.col.db.executemany(
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
rows) rows)
def updateData(self, n, id, sflds): def updateData(self, n, id, sflds) -> Optional[list]:
self._ids.append(id) self._ids.append(id)
if not self.processFields(n, sflds): if not self.processFields(n, sflds):
return return
@ -251,7 +252,7 @@ content in the text file to the correct fields."""))
return [intTime(), self.col.usn(), n.fieldsStr, return [intTime(), self.col.usn(), n.fieldsStr,
id, n.fieldsStr] id, n.fieldsStr]
def addUpdates(self, rows): def addUpdates(self, rows) -> None:
old = self.col.db.totalChanges() old = self.col.db.totalChanges()
if self._tagsMapped: if self._tagsMapped:
self.col.db.executemany(""" self.col.db.executemany("""
@ -263,7 +264,7 @@ update notes set mod = ?, usn = ?, flds = ?
where id = ? and flds != ?""", rows) where id = ? and flds != ?""", rows)
self.updateCount = self.col.db.totalChanges() - old self.updateCount = self.col.db.totalChanges() - old
def processFields(self, note, fields=None): def processFields(self, note, fields=None) -> Any:
if not fields: if not fields:
fields = [""]*len(self.model['flds']) fields = [""]*len(self.model['flds'])
for c, f in enumerate(self.mapping): for c, f in enumerate(self.mapping):
@ -280,7 +281,7 @@ where id = ? and flds != ?""", rows)
self._emptyNotes = True self._emptyNotes = True
return ords return ords
def updateCards(self): def updateCards(self) -> None:
data = [] data = []
for nid, ord, c in self._cards: for nid, ord, c in self._cards:
data.append((c.ivl, c.due, c.factor, c.reps, c.lapses, nid, ord)) data.append((c.ivl, c.due, c.factor, c.reps, c.lapses, nid, ord))

View File

@ -6,6 +6,7 @@ import gzip, math, random, time, html
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from anki.importing.noteimp import NoteImporter, ForeignNote, ForeignCard from anki.importing.noteimp import NoteImporter, ForeignNote, ForeignCard
from anki.stdmodels import addForwardReverse from anki.stdmodels import addForwardReverse
from typing import List
ONE_DAY = 60*60*24 ONE_DAY = 60*60*24
@ -28,7 +29,7 @@ class PaukerImporter(NoteImporter):
'''Pauker is Front/Back''' '''Pauker is Front/Back'''
return 2 return 2
def foreignNotes(self): def foreignNotes(self) -> List[ForeignNote]:
'''Build and return a list of notes.''' '''Build and return a list of notes.'''
notes = [] notes = []
@ -66,7 +67,7 @@ class PaukerImporter(NoteImporter):
return notes return notes
def _learnedCard(self, batch, timestamp): def _learnedCard(self, batch, timestamp) -> ForeignCard:
ivl = math.exp(batch) ivl = math.exp(batch)
now = time.time() now = time.time()
due = ivl - (now - timestamp/1000.0)/ONE_DAY due = ivl - (now - timestamp/1000.0)/ONE_DAY

View File

@ -13,6 +13,7 @@ from anki.lang import ngettext
from xml.dom import minidom from xml.dom import minidom
from string import capwords from string import capwords
import re, unicodedata, time import re, unicodedata, time
from typing import Any, List
class SmartDict(dict): class SmartDict(dict):
""" """
@ -25,7 +26,7 @@ class SmartDict(dict):
x.get('first_name'). x.get('first_name').
""" """
def __init__(self, *a, **kw): def __init__(self, *a, **kw) -> None:
if a: if a:
if isinstance(type(a[0]), dict): if isinstance(type(a[0]), dict):
kw.update(a[0]) kw.update(a[0])
@ -119,17 +120,17 @@ class SupermemoXmlImporter(NoteImporter):
## TOOLS ## TOOLS
def _fudgeText(self, text): def _fudgeText(self, text) -> Any:
"Replace sm syntax to Anki syntax" "Replace sm syntax to Anki syntax"
text = text.replace("\n\r", "<br>") text = text.replace("\n\r", "<br>")
text = text.replace("\n", "<br>") text = text.replace("\n", "<br>")
return text return text
def _unicode2ascii(self,str): def _unicode2ascii(self,str) -> str:
"Remove diacritic punctuation from strings (titles)" "Remove diacritic punctuation from strings (titles)"
return "".join([ c for c in unicodedata.normalize('NFKD', str) if not unicodedata.combining(c)]) return "".join([ c for c in unicodedata.normalize('NFKD', str) if not unicodedata.combining(c)])
def _decode_htmlescapes(self,s): def _decode_htmlescapes(self,s) -> str:
"""Unescape HTML code.""" """Unescape HTML code."""
#In case of bad formated html you can import MinimalSoup etc.. see btflsoup source code #In case of bad formated html you can import MinimalSoup etc.. see btflsoup source code
from bs4 import BeautifulSoup as btflsoup from bs4 import BeautifulSoup as btflsoup
@ -142,7 +143,7 @@ class SupermemoXmlImporter(NoteImporter):
return str(btflsoup(s, "html.parser")) return str(btflsoup(s, "html.parser"))
def _afactor2efactor(self, af): def _afactor2efactor(self, af) -> Any:
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm> # Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
# Ranges for A-factors and E-factors # Ranges for A-factors and E-factors
@ -166,7 +167,7 @@ class SupermemoXmlImporter(NoteImporter):
## DEFAULT IMPORTER METHODS ## DEFAULT IMPORTER METHODS
def foreignNotes(self): def foreignNotes(self) -> List[ForeignNote]:
# Load file and parse it by minidom # Load file and parse it by minidom
self.loadSource(self.file) self.loadSource(self.file)
@ -187,7 +188,7 @@ class SupermemoXmlImporter(NoteImporter):
## PARSER METHODS ## PARSER METHODS
def addItemToCards(self,item): def addItemToCards(self,item) -> None:
"This method actually do conversion" "This method actually do conversion"
# new anki card # new anki card
@ -247,7 +248,7 @@ class SupermemoXmlImporter(NoteImporter):
self.notes.append(note) self.notes.append(note)
def logger(self,text,level=1): def logger(self,text,level=1) -> None:
"Wrapper for Anki logger" "Wrapper for Anki logger"
dLevels={0:'',1:'Info',2:'Verbose',3:'Debug'} dLevels={0:'',1:'Info',2:'Verbose',3:'Debug'}
@ -259,7 +260,7 @@ class SupermemoXmlImporter(NoteImporter):
# OPEN AND LOAD # OPEN AND LOAD
def openAnything(self,source): def openAnything(self,source) -> Any:
"Open any source / actually only openig of files is used" "Open any source / actually only openig of files is used"
if source == "-": if source == "-":
@ -282,7 +283,7 @@ class SupermemoXmlImporter(NoteImporter):
import io import io
return io.StringIO(str(source)) return io.StringIO(str(source))
def loadSource(self, source): def loadSource(self, source) -> None:
"""Load source file and parse with xml.dom.minidom""" """Load source file and parse with xml.dom.minidom"""
self.source = source self.source = source
self.logger('Load started...') self.logger('Load started...')
@ -293,7 +294,7 @@ class SupermemoXmlImporter(NoteImporter):
# PARSE # PARSE
def parse(self, node=None): def parse(self, node=None) -> None:
"Parse method - parses document elements" "Parse method - parses document elements"
if node is None and self.xmldoc is not None: if node is None and self.xmldoc is not None:
@ -306,12 +307,12 @@ class SupermemoXmlImporter(NoteImporter):
else: else:
self.logger('No handler for method %s' % _method, level=3) self.logger('No handler for method %s' % _method, level=3)
def parse_Document(self, node): def parse_Document(self, node) -> None:
"Parse XML document" "Parse XML document"
self.parse(node.documentElement) self.parse(node.documentElement)
def parse_Element(self, node): def parse_Element(self, node) -> None:
"Parse XML element" "Parse XML element"
_method = "do_%s" % node.tagName _method = "do_%s" % node.tagName
@ -322,7 +323,7 @@ class SupermemoXmlImporter(NoteImporter):
self.logger('No handler for method %s' % _method, level=3) self.logger('No handler for method %s' % _method, level=3)
#print traceback.print_exc() #print traceback.print_exc()
def parse_Text(self, node): def parse_Text(self, node) -> None:
"Parse text inside elements. Text is stored into local buffer." "Parse text inside elements. Text is stored into local buffer."
text = node.data text = node.data
@ -336,12 +337,12 @@ class SupermemoXmlImporter(NoteImporter):
# DO # DO
def do_SuperMemoCollection(self, node): def do_SuperMemoCollection(self, node) -> None:
"Process SM Collection" "Process SM Collection"
for child in node.childNodes: self.parse(child) for child in node.childNodes: self.parse(child)
def do_SuperMemoElement(self, node): def do_SuperMemoElement(self, node) -> None:
"Process SM Element (Type - Title,Topics)" "Process SM Element (Type - Title,Topics)"
self.logger('='*45, level=3) self.logger('='*45, level=3)
@ -391,14 +392,14 @@ class SupermemoXmlImporter(NoteImporter):
t = self.cntMeta['title'].pop() t = self.cntMeta['title'].pop()
self.logger('End of topic \t- %s' % (t), level=2) self.logger('End of topic \t- %s' % (t), level=2)
def do_Content(self, node): def do_Content(self, node) -> None:
"Process SM element Content" "Process SM element Content"
for child in node.childNodes: for child in node.childNodes:
if hasattr(child,'tagName') and child.firstChild is not None: if hasattr(child,'tagName') and child.firstChild is not None:
self.cntElm[-1][child.tagName]=child.firstChild.data self.cntElm[-1][child.tagName]=child.firstChild.data
def do_LearningData(self, node): def do_LearningData(self, node) -> None:
"Process SM element LearningData" "Process SM element LearningData"
for child in node.childNodes: for child in node.childNodes:
@ -415,7 +416,7 @@ class SupermemoXmlImporter(NoteImporter):
# for child in node.childNodes: self.parse(child) # for child in node.childNodes: self.parse(child)
# self.cntElm[-1][node.tagName]=self.cntBuf.pop() # self.cntElm[-1][node.tagName]=self.cntBuf.pop()
def do_Title(self, node): def do_Title(self, node) -> None:
"Process SM element Title" "Process SM element Title"
t = self._decode_htmlescapes(node.firstChild.data) t = self._decode_htmlescapes(node.firstChild.data)
@ -425,7 +426,7 @@ class SupermemoXmlImporter(NoteImporter):
self.logger('Start of topic \t- ' + " / ".join(self.cntMeta['title']), level=2) self.logger('Start of topic \t- ' + " / ".join(self.cntMeta['title']), level=2)
def do_Type(self, node): def do_Type(self, node) -> None:
"Process SM element Type" "Process SM element Type"
if len(self.cntBuf) >=1 : if len(self.cntBuf) >=1 :

View File

@ -5,6 +5,7 @@
import os, sys, re import os, sys, re
import gettext import gettext
import threading import threading
from typing import Any
langs = sorted([ langs = sorted([
("Afrikaans", "af_ZA"), ("Afrikaans", "af_ZA"),
@ -108,20 +109,20 @@ threadLocal = threading.local()
currentLang = None currentLang = None
currentTranslation = None currentTranslation = None
def localTranslation(): def localTranslation() -> Any:
"Return the translation local to this thread, or the default." "Return the translation local to this thread, or the default."
if getattr(threadLocal, 'currentTranslation', None): if getattr(threadLocal, 'currentTranslation', None):
return threadLocal.currentTranslation return threadLocal.currentTranslation
else: else:
return currentTranslation return currentTranslation
def _(str): def _(str) -> Any:
return localTranslation().gettext(str) return localTranslation().gettext(str)
def ngettext(single, plural, n): def ngettext(single, plural, n) -> Any:
return localTranslation().ngettext(single, plural, n) return localTranslation().ngettext(single, plural, n)
def langDir(): def langDir() -> str:
from anki.utils import isMac from anki.utils import isMac
filedir = os.path.dirname(os.path.abspath(__file__)) filedir = os.path.dirname(os.path.abspath(__file__))
if isMac: if isMac:
@ -134,7 +135,7 @@ def langDir():
dir = os.path.abspath(os.path.join(filedir, "..", "locale")) dir = os.path.abspath(os.path.join(filedir, "..", "locale"))
return dir return dir
def setLang(lang, local=True): def setLang(lang, local=True) -> None:
lang = mungeCode(lang) lang = mungeCode(lang)
trans = gettext.translation( trans = gettext.translation(
'anki', langDir(), languages=[lang], fallback=True) 'anki', langDir(), languages=[lang], fallback=True)
@ -146,18 +147,18 @@ def setLang(lang, local=True):
currentLang = lang currentLang = lang
currentTranslation = trans currentTranslation = trans
def getLang(): def getLang() -> Any:
"Return the language local to this thread, or the default." "Return the language local to this thread, or the default."
if getattr(threadLocal, 'currentLang', None): if getattr(threadLocal, 'currentLang', None):
return threadLocal.currentLang return threadLocal.currentLang
else: else:
return currentLang return currentLang
def noHint(str): def noHint(str) -> str:
"Remove translation hint from end of string." "Remove translation hint from end of string."
return re.sub(r"(^.*?)( ?\(.+?\))?$", "\\1", str) return re.sub(r"(^.*?)( ?\(.+?\))?$", "\\1", str)
def mungeCode(code): def mungeCode(code) -> Any:
code = code.replace("-", "_") code = code.replace("-", "_")
if code in compatMap: if code in compatMap:
code = compatMap[code] code = compatMap[code]

View File

@ -6,6 +6,7 @@ import re, os, shutil, html
from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML
from anki.hooks import addHook from anki.hooks import addHook
from anki.lang import _ from anki.lang import _
from typing import Any
pngCommands = [ pngCommands = [
["latex", "-interaction=nonstopmode", "tmp.tex"], ["latex", "-interaction=nonstopmode", "tmp.tex"],
@ -28,7 +29,7 @@ regexps = {
if isMac: if isMac:
os.environ['PATH'] += ":/usr/texbin:/Library/TeX/texbin" os.environ['PATH'] += ":/usr/texbin:/Library/TeX/texbin"
def stripLatex(text): def stripLatex(text) -> Any:
for match in regexps['standard'].finditer(text): for match in regexps['standard'].finditer(text):
text = text.replace(match.group(), "") text = text.replace(match.group(), "")
for match in regexps['expression'].finditer(text): for match in regexps['expression'].finditer(text):
@ -37,7 +38,7 @@ def stripLatex(text):
text = text.replace(match.group(), "") text = text.replace(match.group(), "")
return text return text
def mungeQA(html, type, fields, model, data, col): def mungeQA(html, type, fields, model, data, col) -> Any:
"Convert TEXT with embedded latex tags to image links." "Convert TEXT with embedded latex tags to image links."
for match in regexps['standard'].finditer(html): for match in regexps['standard'].finditer(html):
html = html.replace(match.group(), _imgLink(col, match.group(1), model)) html = html.replace(match.group(), _imgLink(col, match.group(1), model))
@ -50,7 +51,7 @@ def mungeQA(html, type, fields, model, data, col):
"\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model)) "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model))
return html return html
def _imgLink(col, latex, model): def _imgLink(col, latex, model) -> Any:
"Return an img link for LATEX, creating if necesssary." "Return an img link for LATEX, creating if necesssary."
txt = _latexFromHtml(col, latex) txt = _latexFromHtml(col, latex)
@ -75,13 +76,13 @@ def _imgLink(col, latex, model):
else: else:
return link return link
def _latexFromHtml(col, latex): def _latexFromHtml(col, latex) -> Any:
"Convert entities and fix newlines." "Convert entities and fix newlines."
latex = re.sub("<br( /)?>|<div>", "\n", latex) latex = re.sub("<br( /)?>|<div>", "\n", latex)
latex = stripHTML(latex) latex = stripHTML(latex)
return latex return latex
def _buildImg(col, latex, fname, model): def _buildImg(col, latex, fname, model) -> Any:
# add header/footer # add header/footer
latex = (model["latexPre"] + "\n" + latex = (model["latexPre"] + "\n" +
latex + "\n" + latex + "\n" +
@ -129,7 +130,7 @@ package in the LaTeX header instead.""") % bad
os.chdir(oldcwd) os.chdir(oldcwd)
log.close() log.close()
def _errMsg(type, texpath): def _errMsg(type, texpath) -> Any:
msg = (_("Error executing %s.") % type) + "<br>" msg = (_("Error executing %s.") % type) + "<br>"
msg += (_("Generated file: %s") % texpath) + "<br>" msg += (_("Generated file: %s") % texpath) + "<br>"
try: try:

View File

@ -17,6 +17,9 @@ from anki.db import DB, DBError
from anki.consts import * from anki.consts import *
from anki.latex import mungeQA from anki.latex import mungeQA
from anki.lang import _ from anki.lang import _
from typing import Any, List, Optional, Tuple, TypeVar, Union
_T0 = TypeVar('_T0')
class MediaManager: class MediaManager:
@ -29,7 +32,7 @@ class MediaManager:
] ]
regexps = soundRegexps + imgRegexps regexps = soundRegexps + imgRegexps
def __init__(self, col, server): def __init__(self, col, server) -> None:
self.col = col self.col = col
if server: if server:
self._dir = None self._dir = None
@ -50,7 +53,7 @@ class MediaManager:
# change database # change database
self.connect() self.connect()
def connect(self): def connect(self) -> None:
if self.col.server: if self.col.server:
return return
path = self.dir()+".db2" path = self.dir()+".db2"
@ -61,7 +64,7 @@ class MediaManager:
self._initDB() self._initDB()
self.maybeUpgrade() self.maybeUpgrade()
def _initDB(self): def _initDB(self) -> None:
self.db.executescript(""" self.db.executescript("""
create table media ( create table media (
fname text not null primary key, fname text not null primary key,
@ -75,7 +78,7 @@ create index idx_media_dirty on media (dirty);
create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
""") """)
def maybeUpgrade(self): def maybeUpgrade(self) -> None:
oldpath = self.dir()+".db" oldpath = self.dir()+".db"
if os.path.exists(oldpath): if os.path.exists(oldpath):
self.db.execute('attach "../collection.media.db" as old') self.db.execute('attach "../collection.media.db" as old')
@ -102,7 +105,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
os.unlink(npath) os.unlink(npath)
os.rename("../collection.media.db", npath) os.rename("../collection.media.db", npath)
def close(self): def close(self) -> None:
if self.col.server: if self.col.server:
return return
self.db.close() self.db.close()
@ -115,16 +118,16 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# may have been deleted # may have been deleted
pass pass
def _deleteDB(self): def _deleteDB(self) -> None:
path = self.db._path path = self.db._path
self.close() self.close()
os.unlink(path) os.unlink(path)
self.connect() self.connect()
def dir(self): def dir(self) -> Any:
return self._dir return self._dir
def _isFAT32(self): def _isFAT32(self) -> Optional[bool]:
if not isWin: if not isWin:
return return
# pylint: disable=import-error # pylint: disable=import-error
@ -141,11 +144,11 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
########################################################################## ##########################################################################
# opath must be in unicode # opath must be in unicode
def addFile(self, opath): def addFile(self, opath) -> Any:
with open(opath, "rb") as f: with open(opath, "rb") as f:
return self.writeData(opath, f.read()) return self.writeData(opath, f.read())
def writeData(self, opath, data, typeHint=None): def writeData(self, opath, data, typeHint=None) -> Any:
# if fname is a full path, use only the basename # if fname is a full path, use only the basename
fname = os.path.basename(opath) fname = os.path.basename(opath)
@ -193,7 +196,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# String manipulation # String manipulation
########################################################################## ##########################################################################
def filesInStr(self, mid, string, includeRemote=False): def filesInStr(self, mid, string, includeRemote=False) -> List[str]:
l = [] l = []
model = self.col.models.get(mid) model = self.col.models.get(mid)
strings = [] strings = []
@ -215,7 +218,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
l.append(fname) l.append(fname)
return l return l
def _expandClozes(self, string): def _expandClozes(self, string) -> List[str]:
ords = set(re.findall(r"{{c(\d+)::.+?}}", string)) ords = set(re.findall(r"{{c(\d+)::.+?}}", string))
strings = [] strings = []
from anki.template.template import clozeReg from anki.template.template import clozeReg
@ -233,17 +236,17 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
strings.append(re.sub(clozeReg%".+?", arepl, string)) strings.append(re.sub(clozeReg%".+?", arepl, string))
return strings return strings
def transformNames(self, txt, func): def transformNames(self, txt, func) -> Any:
for reg in self.regexps: for reg in self.regexps:
txt = re.sub(reg, func, txt) txt = re.sub(reg, func, txt)
return txt return txt
def strip(self, txt): def strip(self, txt: _T0) -> Union[str, _T0]:
for reg in self.regexps: for reg in self.regexps:
txt = re.sub(reg, "", txt) txt = re.sub(reg, "", txt)
return txt return txt
def escapeImages(self, string, unescape=False): def escapeImages(self, string: _T0, unescape=False) -> Union[str, _T0]:
if unescape: if unescape:
fn = urllib.parse.unquote fn = urllib.parse.unquote
else: else:
@ -261,7 +264,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Rebuilding DB # Rebuilding DB
########################################################################## ##########################################################################
def check(self, local=None): def check(self, local=None) -> Any:
"Return (missingFiles, unusedFiles)." "Return (missingFiles, unusedFiles)."
mdir = self.dir() mdir = self.dir()
# gather all media references in NFC form # gather all media references in NFC form
@ -335,7 +338,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
_("Anki does not support files in subfolders of the collection.media folder.")) _("Anki does not support files in subfolders of the collection.media folder."))
return (nohave, unused, warnings) return (nohave, unused, warnings)
def _normalizeNoteRefs(self, nid): def _normalizeNoteRefs(self, nid) -> None:
note = self.col.getNote(nid) note = self.col.getNote(nid)
for c, fld in enumerate(note.fields): for c, fld in enumerate(note.fields):
nfc = unicodedata.normalize("NFC", fld) nfc = unicodedata.normalize("NFC", fld)
@ -346,7 +349,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Copying on import # Copying on import
########################################################################## ##########################################################################
def have(self, fname): def have(self, fname) -> bool:
return os.path.exists(os.path.join(self.dir(), fname)) return os.path.exists(os.path.join(self.dir(), fname))
# Illegal characters and paths # Illegal characters and paths
@ -354,7 +357,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]') _illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
def stripIllegal(self, str): def stripIllegal(self, str) -> str:
return re.sub(self._illegalCharReg, "", str) return re.sub(self._illegalCharReg, "", str)
def hasIllegal(self, s: str): def hasIllegal(self, s: str):
@ -366,7 +369,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return True return True
return False return False
def cleanFilename(self, fname): def cleanFilename(self, fname) -> str:
fname = self.stripIllegal(fname) fname = self.stripIllegal(fname)
fname = self._cleanWin32Filename(fname) fname = self._cleanWin32Filename(fname)
fname = self._cleanLongFilename(fname) fname = self._cleanLongFilename(fname)
@ -375,7 +378,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return fname return fname
def _cleanWin32Filename(self, fname): def _cleanWin32Filename(self, fname: _T0) -> Union[str, _T0]:
if not isWin: if not isWin:
return fname return fname
@ -387,7 +390,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return fname return fname
def _cleanLongFilename(self, fname): def _cleanLongFilename(self, fname) -> Any:
# a fairly safe limit that should work on typical windows # a fairly safe limit that should work on typical windows
# paths and on eCryptfs partitions, even with a duplicate # paths and on eCryptfs partitions, even with a duplicate
# suffix appended # suffix appended
@ -416,22 +419,22 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Tracking changes # Tracking changes
########################################################################## ##########################################################################
def findChanges(self): def findChanges(self) -> None:
"Scan the media folder if it's changed, and note any changes." "Scan the media folder if it's changed, and note any changes."
if self._changed(): if self._changed():
self._logChanges() self._logChanges()
def haveDirty(self): def haveDirty(self) -> Any:
return self.db.scalar("select 1 from media where dirty=1 limit 1") return self.db.scalar("select 1 from media where dirty=1 limit 1")
def _mtime(self, path): def _mtime(self, path) -> int:
return int(os.stat(path).st_mtime) return int(os.stat(path).st_mtime)
def _checksum(self, path): def _checksum(self, path) -> str:
with open(path, "rb") as f: with open(path, "rb") as f:
return checksum(f.read()) return checksum(f.read())
def _changed(self): def _changed(self) -> int:
"Return dir mtime if it has changed since the last findChanges()" "Return dir mtime if it has changed since the last findChanges()"
# doesn't track edits, but user can add or remove a file to update # doesn't track edits, but user can add or remove a file to update
mod = self.db.scalar("select dirMod from meta") mod = self.db.scalar("select dirMod from meta")
@ -440,7 +443,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return False return False
return mtime return mtime
def _logChanges(self): def _logChanges(self) -> None:
(added, removed) = self._changes() (added, removed) = self._changes()
media = [] media = []
for f, mtime in added: for f, mtime in added:
@ -453,7 +456,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
self.db.execute("update meta set dirMod = ?", self._mtime(self.dir())) self.db.execute("update meta set dirMod = ?", self._mtime(self.dir()))
self.db.commit() self.db.commit()
def _changes(self): def _changes(self) -> Tuple[List[Tuple[str, int]], List[str]]:
self.cache = {} self.cache = {}
for (name, csum, mod) in self.db.execute( for (name, csum, mod) in self.db.execute(
"select fname, csum, mtime from media where csum is not null"): "select fname, csum, mtime from media where csum is not null"):
@ -515,37 +518,37 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Syncing-related # Syncing-related
########################################################################## ##########################################################################
def lastUsn(self): def lastUsn(self) -> Any:
return self.db.scalar("select lastUsn from meta") return self.db.scalar("select lastUsn from meta")
def setLastUsn(self, usn): def setLastUsn(self, usn) -> None:
self.db.execute("update meta set lastUsn = ?", usn) self.db.execute("update meta set lastUsn = ?", usn)
self.db.commit() self.db.commit()
def syncInfo(self, fname): def syncInfo(self, fname) -> Any:
ret = self.db.first( ret = self.db.first(
"select csum, dirty from media where fname=?", fname) "select csum, dirty from media where fname=?", fname)
return ret or (None, 0) return ret or (None, 0)
def markClean(self, fnames): def markClean(self, fnames) -> None:
for fname in fnames: for fname in fnames:
self.db.execute( self.db.execute(
"update media set dirty=0 where fname=?", fname) "update media set dirty=0 where fname=?", fname)
def syncDelete(self, fname): def syncDelete(self, fname) -> None:
if os.path.exists(fname): if os.path.exists(fname):
os.unlink(fname) os.unlink(fname)
self.db.execute("delete from media where fname=?", fname) self.db.execute("delete from media where fname=?", fname)
def mediaCount(self): def mediaCount(self) -> Any:
return self.db.scalar( return self.db.scalar(
"select count() from media where csum is not null") "select count() from media where csum is not null")
def dirtyCount(self): def dirtyCount(self) -> Any:
return self.db.scalar( return self.db.scalar(
"select count() from media where dirty=1") "select count() from media where dirty=1")
def forceResync(self): def forceResync(self) -> None:
self.db.execute("delete from media") self.db.execute("delete from media")
self.db.execute("update meta set lastUsn=0,dirMod=0") self.db.execute("update meta set lastUsn=0,dirMod=0")
self.db.commit() self.db.commit()
@ -557,7 +560,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Media syncing: zips # Media syncing: zips
########################################################################## ##########################################################################
def mediaChangesZip(self): def mediaChangesZip(self) -> Tuple[bytes, list]:
f = io.BytesIO() f = io.BytesIO()
z = zipfile.ZipFile(f, "w", compression=zipfile.ZIP_DEFLATED) z = zipfile.ZipFile(f, "w", compression=zipfile.ZIP_DEFLATED)
@ -590,7 +593,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
z.close() z.close()
return f.getvalue(), fnames return f.getvalue(), fnames
def addFilesFromZip(self, zipData): def addFilesFromZip(self, zipData) -> int:
"Extract zip data; true if finished." "Extract zip data; true if finished."
f = io.BytesIO(zipData) f = io.BytesIO(zipData)
z = zipfile.ZipFile(f, "r") z = zipfile.ZipFile(f, "r")

View File

@ -10,6 +10,7 @@ from anki.lang import _
from anki.consts import * from anki.consts import *
from anki.hooks import runHook from anki.hooks import runHook
import time import time
from typing import List, Optional, Tuple, Union
# Models # Models
########################################################################## ##########################################################################
@ -75,17 +76,17 @@ class ModelManager:
# Saving/loading registry # Saving/loading registry
############################################################# #############################################################
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self.models = {} self.models = {}
self.changed = False self.changed = False
def load(self, json_): def load(self, json_) -> None:
"Load registry from JSON." "Load registry from JSON."
self.changed = False self.changed = False
self.models = json.loads(json_) self.models = json.loads(json_)
def save(self, m=None, templates=False, updateReqs=True): def save(self, m=None, templates=False, updateReqs=True) -> None:
"Mark M modified if provided, and schedule registry flush." "Mark M modified if provided, and schedule registry flush."
if m and m['id']: if m and m['id']:
m['mod'] = intTime() m['mod'] = intTime()
@ -97,7 +98,7 @@ class ModelManager:
self.changed = True self.changed = True
runHook("newModel") runHook("newModel")
def flush(self): def flush(self) -> None:
"Flush the registry if any models were changed." "Flush the registry if any models were changed."
if self.changed: if self.changed:
self.ensureNotEmpty() self.ensureNotEmpty()
@ -105,7 +106,7 @@ class ModelManager:
json.dumps(self.models)) json.dumps(self.models))
self.changed = False self.changed = False
def ensureNotEmpty(self): def ensureNotEmpty(self) -> Optional[bool]:
if not self.models: if not self.models:
from anki.stdmodels import addBasicModel from anki.stdmodels import addBasicModel
addBasicModel(self.col) addBasicModel(self.col)
@ -114,37 +115,37 @@ class ModelManager:
# Retrieving and creating models # Retrieving and creating models
############################################################# #############################################################
def current(self, forDeck=True): def current(self, forDeck=True) -> Any:
"Get current model." "Get current model."
m = self.get(self.col.decks.current().get('mid')) m = self.get(self.col.decks.current().get('mid'))
if not forDeck or not m: if not forDeck or not m:
m = self.get(self.col.conf['curModel']) m = self.get(self.col.conf['curModel'])
return m or list(self.models.values())[0] return m or list(self.models.values())[0]
def setCurrent(self, m): def setCurrent(self, m) -> None:
self.col.conf['curModel'] = m['id'] self.col.conf['curModel'] = m['id']
self.col.setMod() self.col.setMod()
def get(self, id): def get(self, id) -> Any:
"Get model with ID, or None." "Get model with ID, or None."
id = str(id) id = str(id)
if id in self.models: if id in self.models:
return self.models[id] return self.models[id]
def all(self): def all(self) -> List:
"Get all models." "Get all models."
return list(self.models.values()) return list(self.models.values())
def allNames(self): def allNames(self) -> List:
return [m['name'] for m in self.all()] return [m['name'] for m in self.all()]
def byName(self, name): def byName(self, name) -> Any:
"Get model with NAME." "Get model with NAME."
for m in list(self.models.values()): for m in list(self.models.values()):
if m['name'] == name: if m['name'] == name:
return m return m
def new(self, name): def new(self, name: str) -> Dict[str, Any]:
"Create a new model, save it in the registry, and return it." "Create a new model, save it in the registry, and return it."
# caller should call save() after modifying # caller should call save() after modifying
m = defaultModel.copy() m = defaultModel.copy()
@ -156,7 +157,7 @@ class ModelManager:
m['id'] = None m['id'] = None
return m return m
def rem(self, m): def rem(self, m) -> None:
"Delete model, and all its cards/notes." "Delete model, and all its cards/notes."
self.col.modSchema(check=True) self.col.modSchema(check=True)
current = self.current()['id'] == m['id'] current = self.current()['id'] == m['id']
@ -171,52 +172,52 @@ select id from cards where nid in (select id from notes where mid = ?)""",
if current: if current:
self.setCurrent(list(self.models.values())[0]) self.setCurrent(list(self.models.values())[0])
def add(self, m): def add(self, m) -> None:
self._setID(m) self._setID(m)
self.update(m) self.update(m)
self.setCurrent(m) self.setCurrent(m)
self.save(m) self.save(m)
def ensureNameUnique(self, m): def ensureNameUnique(self, m) -> None:
for mcur in self.all(): for mcur in self.all():
if (mcur['name'] == m['name'] and mcur['id'] != m['id']): if (mcur['name'] == m['name'] and mcur['id'] != m['id']):
m['name'] += "-" + checksum(str(time.time()))[:5] m['name'] += "-" + checksum(str(time.time()))[:5]
break break
def update(self, m): def update(self, m) -> None:
"Add or update an existing model. Used for syncing and merging." "Add or update an existing model. Used for syncing and merging."
self.ensureNameUnique(m) self.ensureNameUnique(m)
self.models[str(m['id'])] = m self.models[str(m['id'])] = m
# mark registry changed, but don't bump mod time # mark registry changed, but don't bump mod time
self.save() self.save()
def _setID(self, m): def _setID(self, m) -> None:
while 1: while 1:
id = str(intTime(1000)) id = str(intTime(1000))
if id not in self.models: if id not in self.models:
break break
m['id'] = id m['id'] = id
def have(self, id): def have(self, id) -> bool:
return str(id) in self.models return str(id) in self.models
def ids(self): def ids(self) -> List[str]:
return list(self.models.keys()) return list(self.models.keys())
# Tools # Tools
################################################## ##################################################
def nids(self, m): def nids(self, m) -> Any:
"Note ids for M." "Note ids for M."
return self.col.db.list( return self.col.db.list(
"select id from notes where mid = ?", m['id']) "select id from notes where mid = ?", m['id'])
def useCount(self, m): def useCount(self, m) -> Any:
"Number of note using M." "Number of note using M."
return self.col.db.scalar( return self.col.db.scalar(
"select count() from notes where mid = ?", m['id']) "select count() from notes where mid = ?", m['id'])
def tmplUseCount(self, m, ord): def tmplUseCount(self, m, ord) -> Any:
return self.col.db.scalar(""" return self.col.db.scalar("""
select count() from cards, notes where cards.nid = notes.id select count() from cards, notes where cards.nid = notes.id
and notes.mid = ? and cards.ord = ?""", m['id'], ord) and notes.mid = ? and cards.ord = ?""", m['id'], ord)
@ -224,7 +225,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# Copying # Copying
################################################## ##################################################
def copy(self, m): def copy(self, m) -> Any:
"Copy, save and return." "Copy, save and return."
m2 = copy.deepcopy(m) m2 = copy.deepcopy(m)
m2['name'] = _("%s copy") % m2['name'] m2['name'] = _("%s copy") % m2['name']
@ -234,30 +235,30 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# Fields # Fields
################################################## ##################################################
def newField(self, name): def newField(self, name) -> Dict[str, Any]:
assert(isinstance(name, str)) assert(isinstance(name, str))
f = defaultField.copy() f = defaultField.copy()
f['name'] = name f['name'] = name
return f return f
def fieldMap(self, m): def fieldMap(self, m) -> Dict[Any, Tuple[Any, Any]]:
"Mapping of field name -> (ord, field)." "Mapping of field name -> (ord, field)."
return dict((f['name'], (f['ord'], f)) for f in m['flds']) return dict((f['name'], (f['ord'], f)) for f in m['flds'])
def fieldNames(self, m): def fieldNames(self, m) -> List:
return [f['name'] for f in m['flds']] return [f['name'] for f in m['flds']]
def sortIdx(self, m): def sortIdx(self, m) -> Any:
return m['sortf'] return m['sortf']
def setSortIdx(self, m, idx): def setSortIdx(self, m, idx) -> None:
assert 0 <= idx < len(m['flds']) assert 0 <= idx < len(m['flds'])
self.col.modSchema(check=True) self.col.modSchema(check=True)
m['sortf'] = idx m['sortf'] = idx
self.col.updateFieldCache(self.nids(m)) self.col.updateFieldCache(self.nids(m))
self.save(m, updateReqs=False) self.save(m, updateReqs=False)
def addField(self, m, field): def addField(self, m, field) -> None:
# only mod schema if model isn't new # only mod schema if model isn't new
if m['id']: if m['id']:
self.col.modSchema(check=True) self.col.modSchema(check=True)
@ -269,7 +270,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
return fields return fields
self._transformFields(m, add) self._transformFields(m, add)
def remField(self, m, field): def remField(self, m, field) -> None:
self.col.modSchema(check=True) self.col.modSchema(check=True)
# save old sort field # save old sort field
sortFldName = m['flds'][m['sortf']]['name'] sortFldName = m['flds'][m['sortf']]['name']
@ -292,7 +293,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# saves # saves
self.renameField(m, field, None) self.renameField(m, field, None)
def moveField(self, m, field, idx): def moveField(self, m, field, idx) -> None:
self.col.modSchema(check=True) self.col.modSchema(check=True)
oldidx = m['flds'].index(field) oldidx = m['flds'].index(field)
if oldidx == idx: if oldidx == idx:
@ -313,7 +314,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
return fields return fields
self._transformFields(m, move) self._transformFields(m, move)
def renameField(self, m, field, newName): def renameField(self, m, field, newName) -> None:
self.col.modSchema(check=True) self.col.modSchema(check=True)
pat = r'{{([^{}]*)([:#^/]|[^:#/^}][^:}]*?:|)%s}}' pat = r'{{([^{}]*)([:#^/]|[^:#/^}][^:}]*?:|)%s}}'
def wrap(txt): def wrap(txt):
@ -331,11 +332,11 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
field['name'] = newName field['name'] = newName
self.save(m) self.save(m)
def _updateFieldOrds(self, m): def _updateFieldOrds(self, m) -> None:
for c, f in enumerate(m['flds']): for c, f in enumerate(m['flds']):
f['ord'] = c f['ord'] = c
def _transformFields(self, m, fn): def _transformFields(self, m, fn) -> None:
# model hasn't been added yet? # model hasn't been added yet?
if not m['id']: if not m['id']:
return return
@ -350,12 +351,12 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# Templates # Templates
################################################## ##################################################
def newTemplate(self, name): def newTemplate(self, name: str) -> Dict[str, Any]:
t = defaultTemplate.copy() t = defaultTemplate.copy()
t['name'] = name t['name'] = name
return t return t
def addTemplate(self, m, template): def addTemplate(self, m, template) -> None:
"Note: should col.genCards() afterwards." "Note: should col.genCards() afterwards."
if m['id']: if m['id']:
self.col.modSchema(check=True) self.col.modSchema(check=True)
@ -363,7 +364,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
self._updateTemplOrds(m) self._updateTemplOrds(m)
self.save(m) self.save(m)
def remTemplate(self, m, template): def remTemplate(self, m, template) -> bool:
"False if removing template would leave orphan notes." "False if removing template would leave orphan notes."
assert len(m['tmpls']) > 1 assert len(m['tmpls']) > 1
# find cards using this template # find cards using this template
@ -393,11 +394,11 @@ update cards set ord = ord - 1, usn = ?, mod = ?
self.save(m) self.save(m)
return True return True
def _updateTemplOrds(self, m): def _updateTemplOrds(self, m) -> None:
for c, t in enumerate(m['tmpls']): for c, t in enumerate(m['tmpls']):
t['ord'] = c t['ord'] = c
def moveTemplate(self, m, template, idx): def moveTemplate(self, m, template, idx) -> None:
oldidx = m['tmpls'].index(template) oldidx = m['tmpls'].index(template)
if oldidx == idx: if oldidx == idx:
return return
@ -416,7 +417,7 @@ update cards set ord = (case %s end),usn=?,mod=? where nid in (
select id from notes where mid = ?)""" % " ".join(map), select id from notes where mid = ?)""" % " ".join(map),
self.col.usn(), intTime(), m['id']) self.col.usn(), intTime(), m['id'])
def _syncTemplates(self, m): def _syncTemplates(self, m) -> None:
rem = self.col.genCards(self.nids(m)) rem = self.col.genCards(self.nids(m))
# Model changing # Model changing
@ -424,7 +425,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# - maps are ord->ord, and there should not be duplicate targets # - maps are ord->ord, and there should not be duplicate targets
# - newModel should be self if model is not changing # - newModel should be self if model is not changing
def change(self, m, nids, newModel, fmap, cmap): def change(self, m, nids, newModel, fmap, cmap) -> None:
self.col.modSchema(check=True) self.col.modSchema(check=True)
assert newModel['id'] == m['id'] or (fmap and cmap) assert newModel['id'] == m['id'] or (fmap and cmap)
if fmap: if fmap:
@ -433,7 +434,7 @@ select id from notes where mid = ?)""" % " ".join(map),
self._changeCards(nids, m, newModel, cmap) self._changeCards(nids, m, newModel, cmap)
self.col.genCards(nids) self.col.genCards(nids)
def _changeNotes(self, nids, newModel, map): def _changeNotes(self, nids, newModel, map) -> None:
d = [] d = []
nfields = len(newModel['flds']) nfields = len(newModel['flds'])
for (nid, flds) in self.col.db.execute( for (nid, flds) in self.col.db.execute(
@ -452,7 +453,7 @@ select id from notes where mid = ?)""" % " ".join(map),
"update notes set flds=:flds,mid=:mid,mod=:m,usn=:u where id = :nid", d) "update notes set flds=:flds,mid=:mid,mod=:m,usn=:u where id = :nid", d)
self.col.updateFieldCache(nids) self.col.updateFieldCache(nids)
def _changeCards(self, nids, oldModel, newModel, map): def _changeCards(self, nids, oldModel, newModel, map) -> None:
d = [] d = []
deleted = [] deleted = []
for (cid, ord) in self.col.db.execute( for (cid, ord) in self.col.db.execute(
@ -482,7 +483,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# Schema hash # Schema hash
########################################################################## ##########################################################################
def scmhash(self, m): def scmhash(self, m) -> str:
"Return a hash of the schema, to see if models are compatible." "Return a hash of the schema, to see if models are compatible."
s = "" s = ""
for f in m['flds']: for f in m['flds']:
@ -494,7 +495,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# Required field/text cache # Required field/text cache
########################################################################## ##########################################################################
def _updateRequired(self, m): def _updateRequired(self, m) -> None:
if m['type'] == MODEL_CLOZE: if m['type'] == MODEL_CLOZE:
# nothing to do # nothing to do
return return
@ -505,7 +506,7 @@ select id from notes where mid = ?)""" % " ".join(map),
req.append([t['ord'], ret[0], ret[1]]) req.append([t['ord'], ret[0], ret[1]])
m['req'] = req m['req'] = req
def _reqForTemplate(self, m, flds, t): def _reqForTemplate(self, m, flds, t) -> Tuple[Union[str, List[int]], ...]:
a = [] a = []
b = [] b = []
for f in flds: for f in flds:
@ -542,7 +543,7 @@ select id from notes where mid = ?)""" % " ".join(map),
req.append(i) req.append(i)
return type, req return type, req
def availOrds(self, m, flds): def availOrds(self, m, flds) -> List:
"Given a joined field string, return available template ordinals." "Given a joined field string, return available template ordinals."
if m['type'] == MODEL_CLOZE: if m['type'] == MODEL_CLOZE:
return self._availClozeOrds(m, flds) return self._availClozeOrds(m, flds)
@ -576,7 +577,7 @@ select id from notes where mid = ?)""" % " ".join(map),
avail.append(ord) avail.append(ord)
return avail return avail
def _availClozeOrds(self, m, flds, allowEmpty=True): def _availClozeOrds(self, m, flds, allowEmpty=True) -> List:
sflds = splitFields(flds) sflds = splitFields(flds)
map = self.fieldMap(m) map = self.fieldMap(m)
ords = set() ords = set()
@ -598,7 +599,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# Sync handling # Sync handling
########################################################################## ##########################################################################
def beforeUpload(self): def beforeUpload(self) -> None:
for m in self.all(): for m in self.all():
m['usn'] = 0 m['usn'] = 0
self.save() self.save()

View File

@ -56,6 +56,7 @@ class MPVTimeoutError(MPVError):
pass pass
from anki.utils import isWin from anki.utils import isWin
from typing import Any
if isWin: if isWin:
# pylint: disable=import-error # pylint: disable=import-error
import win32file, win32pipe, pywintypes, winerror # pytype: disable=import-error import win32file, win32pipe, pywintypes, winerror # pytype: disable=import-error
@ -76,7 +77,7 @@ class MPVBase:
"--keep-open=no", "--keep-open=no",
] ]
def __init__(self, window_id=None, debug=False): def __init__(self, window_id=None, debug=False) -> None:
self.window_id = window_id self.window_id = window_id
self.debug = debug self.debug = debug
@ -87,18 +88,18 @@ class MPVBase:
self._prepare_thread() self._prepare_thread()
self._start_thread() self._start_thread()
def __del__(self): def __del__(self) -> None:
self._stop_thread() self._stop_thread()
self._stop_process() self._stop_process()
self._stop_socket() self._stop_socket()
def _thread_id(self): def _thread_id(self) -> int:
return threading.get_ident() return threading.get_ident()
# #
# Process # Process
# #
def _prepare_process(self): def _prepare_process(self) -> None:
"""Prepare the argument list for the mpv process. """Prepare the argument list for the mpv process.
""" """
self.argv = [self.executable] self.argv = [self.executable]
@ -107,12 +108,12 @@ class MPVBase:
if self.window_id is not None: if self.window_id is not None:
self.argv += ["--wid", str(self.window_id)] self.argv += ["--wid", str(self.window_id)]
def _start_process(self): def _start_process(self) -> None:
"""Start the mpv process. """Start the mpv process.
""" """
self._proc = subprocess.Popen(self.argv, env=self.popenEnv) self._proc = subprocess.Popen(self.argv, env=self.popenEnv)
def _stop_process(self): def _stop_process(self) -> None:
"""Stop the mpv process. """Stop the mpv process.
""" """
if hasattr(self, "_proc"): if hasattr(self, "_proc"):
@ -125,7 +126,7 @@ class MPVBase:
# #
# Socket communication # Socket communication
# #
def _prepare_socket(self): def _prepare_socket(self) -> None:
"""Create a random socket filename which we pass to mpv with the """Create a random socket filename which we pass to mpv with the
--input-unix-socket option. --input-unix-socket option.
""" """
@ -136,7 +137,7 @@ class MPVBase:
os.close(fd) os.close(fd)
os.remove(self._sock_filename) os.remove(self._sock_filename)
def _start_socket(self): def _start_socket(self) -> None:
"""Wait for the mpv process to create the unix socket and finish """Wait for the mpv process to create the unix socket and finish
startup. startup.
""" """
@ -173,7 +174,7 @@ class MPVBase:
else: else:
raise MPVProcessError("unable to start process") raise MPVProcessError("unable to start process")
def _stop_socket(self): def _stop_socket(self) -> None:
"""Clean up the socket. """Clean up the socket.
""" """
if hasattr(self, "_sock"): if hasattr(self, "_sock"):
@ -184,7 +185,7 @@ class MPVBase:
except OSError: except OSError:
pass pass
def _prepare_thread(self): def _prepare_thread(self) -> None:
"""Set up the queues for the communication threads. """Set up the queues for the communication threads.
""" """
self._request_queue = Queue(1) self._request_queue = Queue(1)
@ -192,14 +193,14 @@ class MPVBase:
self._event_queue = Queue() self._event_queue = Queue()
self._stop_event = threading.Event() self._stop_event = threading.Event()
def _start_thread(self): def _start_thread(self) -> None:
"""Start up the communication threads. """Start up the communication threads.
""" """
self._thread = threading.Thread(target=self._reader) self._thread = threading.Thread(target=self._reader)
self._thread.daemon = True self._thread.daemon = True
self._thread.start() self._thread.start()
def _stop_thread(self): def _stop_thread(self) -> None:
"""Stop the communication threads. """Stop the communication threads.
""" """
if hasattr(self, "_stop_event"): if hasattr(self, "_stop_event"):
@ -207,7 +208,7 @@ class MPVBase:
if hasattr(self, "_thread"): if hasattr(self, "_thread"):
self._thread.join() self._thread.join()
def _reader(self): def _reader(self) -> None:
"""Read the incoming json messages from the unix socket that is """Read the incoming json messages from the unix socket that is
connected to the mpv process. Pass them on to the message handler. connected to the mpv process. Pass them on to the message handler.
""" """
@ -249,21 +250,21 @@ class MPVBase:
# #
# Message handling # Message handling
# #
def _compose_message(self, message): def _compose_message(self, message) -> bytes:
"""Return a json representation from a message dictionary. """Return a json representation from a message dictionary.
""" """
# XXX may be strict is too strict ;-) # XXX may be strict is too strict ;-)
data = json.dumps(message) data = json.dumps(message)
return data.encode("utf8", "strict") + b"\n" return data.encode("utf8", "strict") + b"\n"
def _parse_message(self, data): def _parse_message(self, data) -> Any:
"""Return a message dictionary from a json representation. """Return a message dictionary from a json representation.
""" """
# XXX may be strict is too strict ;-) # XXX may be strict is too strict ;-)
data = data.decode("utf8", "strict") data = data.decode("utf8", "strict")
return json.loads(data) return json.loads(data)
def _handle_message(self, message): def _handle_message(self, message) -> None:
"""Handle different types of incoming messages, i.e. responses to """Handle different types of incoming messages, i.e. responses to
commands or asynchronous events. commands or asynchronous events.
""" """
@ -283,7 +284,7 @@ class MPVBase:
else: else:
raise MPVCommunicationError("invalid message %r" % message) raise MPVCommunicationError("invalid message %r" % message)
def _send_message(self, message, timeout=None): def _send_message(self, message, timeout=None) -> None:
"""Send a message/command to the mpv process, message must be a """Send a message/command to the mpv process, message must be a
dictionary of the form {"command": ["arg1", "arg2", ...]}. Responses dictionary of the form {"command": ["arg1", "arg2", ...]}. Responses
from the mpv process must be collected using _get_response(). from the mpv process must be collected using _get_response().
@ -320,7 +321,7 @@ class MPVBase:
raise MPVCommunicationError("broken sender socket") raise MPVCommunicationError("broken sender socket")
data = data[size:] data = data[size:]
def _get_response(self, timeout=None): def _get_response(self, timeout=None) -> Any:
"""Collect the response message to a previous request. If there was an """Collect the response message to a previous request. If there was an
error a MPVCommandError exception is raised, otherwise the command error a MPVCommandError exception is raised, otherwise the command
specific data is returned. specific data is returned.
@ -335,7 +336,7 @@ class MPVBase:
else: else:
return message.get("data") return message.get("data")
def _get_event(self, timeout=None): def _get_event(self, timeout=None) -> Any:
"""Collect a single event message that has been received out-of-band """Collect a single event message that has been received out-of-band
from the mpv process. If a timeout is specified and there have not from the mpv process. If a timeout is specified and there have not
been any events during that period, None is returned. been any events during that period, None is returned.
@ -345,7 +346,7 @@ class MPVBase:
except Empty: except Empty:
return None return None
def _send_request(self, message, timeout=None, _retry=1): def _send_request(self, message, timeout=None, _retry=1) -> Any:
"""Send a command to the mpv process and collect the result. """Send a command to the mpv process and collect the result.
""" """
self.ensure_running() self.ensure_running()
@ -365,12 +366,12 @@ class MPVBase:
# #
# Public API # Public API
# #
def is_running(self): def is_running(self) -> bool:
"""Return True if the mpv process is still active. """Return True if the mpv process is still active.
""" """
return self._proc.poll() is None return self._proc.poll() is None
def ensure_running(self): def ensure_running(self) -> None:
if not self.is_running(): if not self.is_running():
self._stop_thread() self._stop_thread()
self._stop_process() self._stop_process()
@ -382,7 +383,7 @@ class MPVBase:
self._prepare_thread() self._prepare_thread()
self._start_thread() self._start_thread()
def close(self): def close(self) -> None:
"""Shutdown the mpv process and our communication setup. """Shutdown the mpv process and our communication setup.
""" """
if self.is_running(): if self.is_running():
@ -413,7 +414,7 @@ class MPV(MPVBase):
threads to the same MPV instance are synchronized. threads to the same MPV instance are synchronized.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._callbacks = {} self._callbacks = {}
@ -463,7 +464,7 @@ class MPV(MPVBase):
# #
# Event/callback API # Event/callback API
# #
def _event_reader(self): def _event_reader(self) -> None:
"""Collect incoming event messages and call the event handler. """Collect incoming event messages and call the event handler.
""" """
while not self._stop_event.is_set(): while not self._stop_event.is_set():
@ -473,7 +474,7 @@ class MPV(MPVBase):
self._handle_event(message) self._handle_event(message)
def _handle_event(self, message): def _handle_event(self, message) -> None:
"""Lookup and call the callbacks for a particular event message. """Lookup and call the callbacks for a particular event message.
""" """
if message["event"] == "property-change": if message["event"] == "property-change":
@ -487,7 +488,7 @@ class MPV(MPVBase):
else: else:
callback() callback()
def register_callback(self, name, callback): def register_callback(self, name, callback) -> None:
"""Register a function `callback` for the event `name`. """Register a function `callback` for the event `name`.
""" """
try: try:
@ -497,7 +498,7 @@ class MPV(MPVBase):
self._callbacks.setdefault(name, []).append(callback) self._callbacks.setdefault(name, []).append(callback)
def unregister_callback(self, name, callback): def unregister_callback(self, name, callback) -> None:
"""Unregister a previously registered function `callback` for the event """Unregister a previously registered function `callback` for the event
`name`. `name`.
""" """
@ -511,7 +512,7 @@ class MPV(MPVBase):
except ValueError: except ValueError:
raise MPVError("callback %r not registered for event %r" % (callback, name)) raise MPVError("callback %r not registered for event %r" % (callback, name))
def register_property_callback(self, name, callback): def register_property_callback(self, name, callback) -> int:
"""Register a function `callback` for the property-change event on """Register a function `callback` for the property-change event on
property `name`. property `name`.
""" """
@ -533,7 +534,7 @@ class MPV(MPVBase):
self._property_serials[(name, callback)] = serial self._property_serials[(name, callback)] = serial
return serial return serial
def unregister_property_callback(self, name, callback): def unregister_property_callback(self, name, callback) -> None:
"""Unregister a previously registered function `callback` for the """Unregister a previously registered function `callback` for the
property-change event on property `name`. property-change event on property `name`.
""" """
@ -553,17 +554,17 @@ class MPV(MPVBase):
# #
# Public API # Public API
# #
def command(self, *args, timeout=1): def command(self, *args, timeout=1) -> Any:
"""Execute a single command on the mpv process and return the result. """Execute a single command on the mpv process and return the result.
""" """
return self._send_request({"command": list(args)}, timeout=timeout) return self._send_request({"command": list(args)}, timeout=timeout)
def get_property(self, name): def get_property(self, name) -> Any:
"""Return the value of property `name`. """Return the value of property `name`.
""" """
return self.command("get_property", name) return self.command("get_property", name)
def set_property(self, name, value): def set_property(self, name, value) -> Any:
"""Set the value of property `name`. """Set the value of property `name`.
""" """
return self.command("set_property", name, value) return self.command("set_property", name, value)

View File

@ -4,10 +4,11 @@
from anki.utils import fieldChecksum, intTime, \ from anki.utils import fieldChecksum, intTime, \
joinFields, splitFields, stripHTMLMedia, timestampID, guid64 joinFields, splitFields, stripHTMLMedia, timestampID, guid64
from typing import Any, List, Tuple
class Note: class Note:
def __init__(self, col, model=None, id=None): def __init__(self, col, model=None, id=None) -> None:
assert not (model and id) assert not (model and id)
self.col = col self.col = col
self.newlyAdded = False self.newlyAdded = False
@ -26,7 +27,7 @@ class Note:
self._fmap = self.col.models.fieldMap(self._model) self._fmap = self.col.models.fieldMap(self._model)
self.scm = self.col.scm self.scm = self.col.scm
def load(self): def load(self) -> None:
(self.guid, (self.guid,
self.mid, self.mid,
self.mod, self.mod,
@ -43,7 +44,7 @@ from notes where id = ?""", self.id)
self._fmap = self.col.models.fieldMap(self._model) self._fmap = self.col.models.fieldMap(self._model)
self.scm = self.col.scm self.scm = self.col.scm
def flush(self, mod=None): def flush(self, mod=None) -> None:
"If fields or tags have changed, write changes to disk." "If fields or tags have changed, write changes to disk."
assert self.scm == self.col.scm assert self.scm == self.col.scm
self._preFlush() self._preFlush()
@ -66,57 +67,57 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
self.col.tags.register(self.tags) self.col.tags.register(self.tags)
self._postFlush() self._postFlush()
def joinedFields(self): def joinedFields(self) -> str:
return joinFields(self.fields) return joinFields(self.fields)
def cards(self): def cards(self) -> List:
return [self.col.getCard(id) for id in self.col.db.list( return [self.col.getCard(id) for id in self.col.db.list(
"select id from cards where nid = ? order by ord", self.id)] "select id from cards where nid = ? order by ord", self.id)]
def model(self): def model(self) -> Any:
return self._model return self._model
# Dict interface # Dict interface
################################################## ##################################################
def keys(self): def keys(self) -> List:
return list(self._fmap.keys()) return list(self._fmap.keys())
def values(self): def values(self) -> Any:
return self.fields return self.fields
def items(self): def items(self) -> List[Tuple[Any, Any]]:
return [(f['name'], self.fields[ord]) return [(f['name'], self.fields[ord])
for ord, f in sorted(self._fmap.values())] for ord, f in sorted(self._fmap.values())]
def _fieldOrd(self, key): def _fieldOrd(self, key) -> Any:
try: try:
return self._fmap[key][0] return self._fmap[key][0]
except: except:
raise KeyError(key) raise KeyError(key)
def __getitem__(self, key): def __getitem__(self, key) -> Any:
return self.fields[self._fieldOrd(key)] return self.fields[self._fieldOrd(key)]
def __setitem__(self, key, value): def __setitem__(self, key, value) -> None:
self.fields[self._fieldOrd(key)] = value self.fields[self._fieldOrd(key)] = value
def __contains__(self, key): def __contains__(self, key) -> bool:
return key in list(self._fmap.keys()) return key in list(self._fmap.keys())
# Tags # Tags
################################################## ##################################################
def hasTag(self, tag): def hasTag(self, tag) -> Any:
return self.col.tags.inList(tag, self.tags) return self.col.tags.inList(tag, self.tags)
def stringTags(self): def stringTags(self) -> Any:
return self.col.tags.join(self.col.tags.canonify(self.tags)) return self.col.tags.join(self.col.tags.canonify(self.tags))
def setTagsFromStr(self, str): def setTagsFromStr(self, str) -> None:
self.tags = self.col.tags.split(str) self.tags = self.col.tags.split(str)
def delTag(self, tag): def delTag(self, tag) -> None:
rem = [] rem = []
for t in self.tags: for t in self.tags:
if t.lower() == tag.lower(): if t.lower() == tag.lower():
@ -124,14 +125,14 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
for r in rem: for r in rem:
self.tags.remove(r) self.tags.remove(r)
def addTag(self, tag): def addTag(self, tag) -> None:
# duplicates will be stripped on save # duplicates will be stripped on save
self.tags.append(tag) self.tags.append(tag)
# Unique/duplicate check # Unique/duplicate check
################################################## ##################################################
def dupeOrEmpty(self): def dupeOrEmpty(self) -> int:
"1 if first is empty; 2 if first is a duplicate, False otherwise." "1 if first is empty; 2 if first is a duplicate, False otherwise."
val = self.fields[0] val = self.fields[0]
if not val.strip(): if not val.strip():
@ -149,12 +150,12 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
# Flushing cloze notes # Flushing cloze notes
################################################## ##################################################
def _preFlush(self): def _preFlush(self) -> None:
# have we been added yet? # have we been added yet?
self.newlyAdded = not self.col.db.scalar( self.newlyAdded = not self.col.db.scalar(
"select 1 from cards where nid = ?", self.id) "select 1 from cards where nid = ?", self.id)
def _postFlush(self): def _postFlush(self) -> None:
# generate missing cards # generate missing cards
if not self.newlyAdded: if not self.newlyAdded:
rem = self.col.genCards([self.id]) rem = self.col.genCards([self.id])

View File

@ -13,6 +13,9 @@ from anki.utils import ids2str, intTime, fmtTimeSpan
from anki.lang import _ from anki.lang import _
from anki.consts import * from anki.consts import *
from anki.hooks import runHook from anki.hooks import runHook
from typing import Any, List, Optional, Tuple, TypeVar
_T = TypeVar('_T')
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried # queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram # revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
@ -24,7 +27,7 @@ class Scheduler:
_spreadRev = True _spreadRev = True
_burySiblingsOnAnswer = True _burySiblingsOnAnswer = True
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self.queueLimit = 50 self.queueLimit = 50
self.reportLimit = 1000 self.reportLimit = 1000
@ -33,7 +36,7 @@ class Scheduler:
self._haveQueues = False self._haveQueues = False
self._updateCutoff() self._updateCutoff()
def getCard(self): def getCard(self) -> Any:
"Pop the next card from the queue. None if finished." "Pop the next card from the queue. None if finished."
self._checkDay() self._checkDay()
if not self._haveQueues: if not self._haveQueues:
@ -47,14 +50,14 @@ class Scheduler:
card.startTimer() card.startTimer()
return card return card
def reset(self): def reset(self) -> None:
self._updateCutoff() self._updateCutoff()
self._resetLrn() self._resetLrn()
self._resetRev() self._resetRev()
self._resetNew() self._resetNew()
self._haveQueues = True self._haveQueues = True
def answerCard(self, card, ease): def answerCard(self, card, ease) -> None:
self.col.log() self.col.log()
assert 1 <= ease <= 4 assert 1 <= ease <= 4
self.col.markReview(card) self.col.markReview(card)
@ -93,7 +96,7 @@ class Scheduler:
card.usn = self.col.usn() card.usn = self.col.usn()
card.flushSched() card.flushSched()
def counts(self, card=None): def counts(self, card=None) -> tuple:
counts = [self.newCount, self.lrnCount, self.revCount] counts = [self.newCount, self.lrnCount, self.revCount]
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)
@ -103,7 +106,7 @@ class Scheduler:
counts[idx] += 1 counts[idx] += 1
return tuple(counts) return tuple(counts)
def dueForecast(self, days=7): def dueForecast(self, days=7) -> List:
"Return counts over next DAYS. Includes today." "Return counts over next DAYS. Includes today."
daysd = dict(self.col.db.all(""" daysd = dict(self.col.db.all("""
select due, count() from cards select due, count() from cards
@ -121,12 +124,12 @@ order by due""" % self._deckLimit(),
ret = [x[1] for x in sorted(daysd.items())] ret = [x[1] for x in sorted(daysd.items())]
return ret return ret
def countIdx(self, card): def countIdx(self, card) -> Any:
if card.queue == 3: if card.queue == 3:
return 1 return 1
return card.queue return card.queue
def answerButtons(self, card): def answerButtons(self, card) -> int:
if card.odue: if card.odue:
# normal review in dyn deck? # normal review in dyn deck?
if card.odid and card.queue == 2: if card.odid and card.queue == 2:
@ -140,7 +143,7 @@ order by due""" % self._deckLimit(),
else: else:
return 3 return 3
def unburyCards(self): def unburyCards(self) -> None:
"Unbury cards." "Unbury cards."
self.col.conf['lastUnburied'] = self.today self.col.conf['lastUnburied'] = self.today
self.col.log( self.col.log(
@ -148,7 +151,7 @@ order by due""" % self._deckLimit(),
self.col.db.execute( self.col.db.execute(
"update cards set queue=type where queue = -2") "update cards set queue=type where queue = -2")
def unburyCardsForDeck(self): def unburyCardsForDeck(self) -> None:
sids = ids2str(self.col.decks.active()) sids = ids2str(self.col.decks.active())
self.col.log( self.col.log(
self.col.db.list("select id from cards where queue = -2 and did in %s" self.col.db.list("select id from cards where queue = -2 and did in %s"
@ -160,7 +163,7 @@ order by due""" % self._deckLimit(),
# Rev/lrn/time daily stats # Rev/lrn/time daily stats
########################################################################## ##########################################################################
def _updateStats(self, card, type, cnt=1): def _updateStats(self, card, type, cnt=1) -> None:
key = type+"Today" key = type+"Today"
for g in ([self.col.decks.get(card.did)] + for g in ([self.col.decks.get(card.did)] +
self.col.decks.parents(card.did)): self.col.decks.parents(card.did)):
@ -168,7 +171,7 @@ order by due""" % self._deckLimit(),
g[key][1] += cnt g[key][1] += cnt
self.col.decks.save(g) self.col.decks.save(g)
def extendLimits(self, new, rev): def extendLimits(self, new, rev) -> None:
cur = self.col.decks.current() cur = self.col.decks.current()
parents = self.col.decks.parents(cur['id']) parents = self.col.decks.parents(cur['id'])
children = [self.col.decks.get(did) for (name, did) in children = [self.col.decks.get(did) for (name, did) in
@ -179,7 +182,7 @@ order by due""" % self._deckLimit(),
g['revToday'][1] -= rev g['revToday'][1] -= rev
self.col.decks.save(g) self.col.decks.save(g)
def _walkingCount(self, limFn=None, cntFn=None): def _walkingCount(self, limFn=None, cntFn=None) -> Any:
tot = 0 tot = 0
pcounts = {} pcounts = {}
# for each of the active decks # for each of the active decks
@ -213,7 +216,7 @@ order by due""" % self._deckLimit(),
# Deck list # Deck list
########################################################################## ##########################################################################
def deckDueList(self): def deckDueList(self) -> List[list]:
"Returns [deckname, did, rev, lrn, new]" "Returns [deckname, did, rev, lrn, new]"
self._checkDay() self._checkDay()
self.col.decks.checkIntegrity() self.col.decks.checkIntegrity()
@ -247,10 +250,10 @@ order by due""" % self._deckLimit(),
lims[deck['name']] = [nlim, rlim] lims[deck['name']] = [nlim, rlim]
return data return data
def deckDueTree(self): def deckDueTree(self) -> Any:
return self._groupChildren(self.deckDueList()) return self._groupChildren(self.deckDueList())
def _groupChildren(self, grps): def _groupChildren(self, grps) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]:
# first, split the group names into components # first, split the group names into components
for g in grps: for g in grps:
g[0] = g[0].split("::") g[0] = g[0].split("::")
@ -259,7 +262,7 @@ order by due""" % self._deckLimit(),
# then run main function # then run main function
return self._groupChildrenMain(grps) return self._groupChildrenMain(grps)
def _groupChildrenMain(self, grps): def _groupChildrenMain(self, grps) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]:
tree = [] tree = []
# group and recurse # group and recurse
def key(grp): def key(grp):
@ -300,7 +303,7 @@ order by due""" % self._deckLimit(),
# Getting the next card # Getting the next card
########################################################################## ##########################################################################
def _getCard(self): def _getCard(self) -> Any:
"Return the next due card id, or None." "Return the next due card id, or None."
# learning card due? # learning card due?
c = self._getLrnCard() c = self._getLrnCard()
@ -329,19 +332,19 @@ order by due""" % self._deckLimit(),
# New cards # New cards
########################################################################## ##########################################################################
def _resetNewCount(self): def _resetNewCount(self) -> None:
cntFn = lambda did, lim: self.col.db.scalar(""" cntFn = lambda did, lim: self.col.db.scalar("""
select count() from (select 1 from cards where select count() from (select 1 from cards where
did = ? and queue = 0 limit ?)""", did, lim) did = ? and queue = 0 limit ?)""", did, lim)
self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn)
def _resetNew(self): def _resetNew(self) -> None:
self._resetNewCount() self._resetNewCount()
self._newDids = self.col.decks.active()[:] self._newDids = self.col.decks.active()[:]
self._newQueue = [] self._newQueue = []
self._updateNewCardRatio() self._updateNewCardRatio()
def _fillNew(self): def _fillNew(self) -> Any:
if self._newQueue: if self._newQueue:
return True return True
if not self.newCount: if not self.newCount:
@ -365,12 +368,12 @@ did = ? and queue = 0 limit ?)""", did, lim)
self._resetNew() self._resetNew()
return self._fillNew() return self._fillNew()
def _getNewCard(self): def _getNewCard(self) -> Any:
if self._fillNew(): if self._fillNew():
self.newCount -= 1 self.newCount -= 1
return self.col.getCard(self._newQueue.pop()) return self.col.getCard(self._newQueue.pop())
def _updateNewCardRatio(self): def _updateNewCardRatio(self) -> None:
if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE: if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.newCount: if self.newCount:
self.newCardModulus = ( self.newCardModulus = (
@ -381,7 +384,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
return return
self.newCardModulus = 0 self.newCardModulus = 0
def _timeForNewCard(self): def _timeForNewCard(self) -> Optional[int]:
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
if not self.newCount: if not self.newCount:
return False return False
@ -392,7 +395,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
elif self.newCardModulus: elif self.newCardModulus:
return self.reps and self.reps % self.newCardModulus == 0 return self.reps and self.reps % self.newCardModulus == 0
def _deckNewLimit(self, did, fn=None): def _deckNewLimit(self, did, fn=None) -> Any:
if not fn: if not fn:
fn = self._deckNewLimitSingle fn = self._deckNewLimitSingle
sel = self.col.decks.get(did) sel = self.col.decks.get(did)
@ -406,7 +409,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
lim = min(rem, lim) lim = min(rem, lim)
return lim return lim
def _newForDeck(self, did, lim): def _newForDeck(self, did, lim) -> Any:
"New count for a single deck." "New count for a single deck."
if not lim: if not lim:
return 0 return 0
@ -415,14 +418,14 @@ did = ? and queue = 0 limit ?)""", did, lim)
select count() from select count() from
(select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim) (select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim)
def _deckNewLimitSingle(self, g): def _deckNewLimitSingle(self, g) -> Any:
"Limit for deck without parent limits." "Limit for deck without parent limits."
if g['dyn']: if g['dyn']:
return self.reportLimit return self.reportLimit
c = self.col.decks.confForDid(g['id']) c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1]) return max(0, c['new']['perDay'] - g['newToday'][1])
def totalNewForCurrentDeck(self): def totalNewForCurrentDeck(self) -> Any:
return self.col.db.scalar( return self.col.db.scalar(
""" """
select count() from cards where id in ( select count() from cards where id in (
@ -432,7 +435,7 @@ select id from cards where did in %s and queue = 0 limit ?)"""
# Learning queues # Learning queues
########################################################################## ##########################################################################
def _resetLrnCount(self): def _resetLrnCount(self) -> None:
# sub-day # sub-day
self.lrnCount = self.col.db.scalar(""" self.lrnCount = self.col.db.scalar("""
select sum(left/1000) from (select left from cards where select sum(left/1000) from (select left from cards where
@ -445,14 +448,14 @@ select count() from cards where did in %s and queue = 3
and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit),
self.today) self.today)
def _resetLrn(self): def _resetLrn(self) -> None:
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue = [] self._lrnQueue = []
self._lrnDayQueue = [] self._lrnDayQueue = []
self._lrnDids = self.col.decks.active()[:] self._lrnDids = self.col.decks.active()[:]
# sub-day learning # sub-day learning
def _fillLrn(self): def _fillLrn(self) -> Any:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnQueue: if self._lrnQueue:
@ -465,7 +468,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
self._lrnQueue.sort() self._lrnQueue.sort()
return self._lrnQueue return self._lrnQueue
def _getLrnCard(self, collapse=False): def _getLrnCard(self, collapse=False) -> Any:
if self._fillLrn(): if self._fillLrn():
cutoff = time.time() cutoff = time.time()
if collapse: if collapse:
@ -477,7 +480,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
return card return card
# daily learning # daily learning
def _fillLrnDay(self): def _fillLrnDay(self) -> Optional[bool]:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnDayQueue: if self._lrnDayQueue:
@ -501,12 +504,12 @@ did = ? and queue = 3 and due <= ? limit ?""",
# nothing left in the deck; move to next # nothing left in the deck; move to next
self._lrnDids.pop(0) self._lrnDids.pop(0)
def _getLrnDayCard(self): def _getLrnDayCard(self) -> Any:
if self._fillLrnDay(): if self._fillLrnDay():
self.lrnCount -= 1 self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop()) return self.col.getCard(self._lrnDayQueue.pop())
def _answerLrnCard(self, card, ease): def _answerLrnCard(self, card, ease) -> None:
# ease 1=no, 2=yes, 3=remove # ease 1=no, 2=yes, 3=remove
conf = self._lrnConf(card) conf = self._lrnConf(card)
if card.odid and not card.wasNew: if card.odid and not card.wasNew:
@ -568,7 +571,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.queue = 3 card.queue = 3
self._logLrn(card, ease, conf, leaving, type, lastLeft) self._logLrn(card, ease, conf, leaving, type, lastLeft)
def _delayForGrade(self, conf, left): def _delayForGrade(self, conf, left) -> Any:
left = left % 1000 left = left % 1000
try: try:
delay = conf['delays'][-left] delay = conf['delays'][-left]
@ -580,13 +583,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
delay = 1 delay = 1
return delay*60 return delay*60
def _lrnConf(self, card): def _lrnConf(self, card) -> Any:
if card.type == 2: if card.type == 2:
return self._lapseConf(card) return self._lapseConf(card)
else: else:
return self._newConf(card) return self._newConf(card)
def _rescheduleAsRev(self, card, conf, early): def _rescheduleAsRev(self, card, conf, early) -> None:
lapse = card.type == 2 lapse = card.type == 2
if lapse: if lapse:
if self._resched(card): if self._resched(card):
@ -609,7 +612,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.queue = card.type = 0 card.queue = card.type = 0
card.due = self.col.nextID("pos") card.due = self.col.nextID("pos")
def _startingLeft(self, card): def _startingLeft(self, card) -> int:
if card.type == 2: if card.type == 2:
conf = self._lapseConf(card) conf = self._lapseConf(card)
else: else:
@ -618,7 +621,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
tod = self._leftToday(conf['delays'], tot) tod = self._leftToday(conf['delays'], tot)
return tot + tod*1000 return tot + tod*1000
def _leftToday(self, delays, left, now=None): def _leftToday(self, delays, left, now=None) -> int:
"The number of steps that can be completed by the day cutoff." "The number of steps that can be completed by the day cutoff."
if not now: if not now:
now = intTime() now = intTime()
@ -631,7 +634,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
ok = i ok = i
return ok+1 return ok+1
def _graduatingIvl(self, card, conf, early, adj=True): def _graduatingIvl(self, card, conf, early, adj=True) -> Any:
if card.type == 2: if card.type == 2:
# lapsed card being relearnt # lapsed card being relearnt
if card.odid: if card.odid:
@ -649,13 +652,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
else: else:
return ideal return ideal
def _rescheduleNew(self, card, conf, early): def _rescheduleNew(self, card, conf, early) -> None:
"Reschedule a new card that's graduated for the first time." "Reschedule a new card that's graduated for the first time."
card.ivl = self._graduatingIvl(card, conf, early) card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today+card.ivl card.due = self.today+card.ivl
card.factor = conf['initialFactor'] card.factor = conf['initialFactor']
def _logLrn(self, card, ease, conf, leaving, type, lastLeft): def _logLrn(self, card, ease, conf, leaving, type, lastLeft) -> None:
lastIvl = -(self._delayForGrade(conf, lastLeft)) lastIvl = -(self._delayForGrade(conf, lastLeft))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log(): def log():
@ -670,7 +673,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
time.sleep(0.01) time.sleep(0.01)
log() log()
def removeLrn(self, ids=None): def removeLrn(self, ids=None) -> None:
"Remove cards from the learning queues." "Remove cards from the learning queues."
if ids: if ids:
extra = " and id in "+ids2str(ids) extra = " and id in "+ids2str(ids)
@ -689,7 +692,7 @@ where queue in (1,3) and type = 2
self.forgetCards(self.col.db.list( self.forgetCards(self.col.db.list(
"select id from cards where queue in (1,3) %s" % extra)) "select id from cards where queue in (1,3) %s" % extra))
def _lrnForDeck(self, did): def _lrnForDeck(self, did) -> Any:
cnt = self.col.db.scalar( cnt = self.col.db.scalar(
""" """
select sum(left/1000) from select sum(left/1000) from
@ -705,16 +708,16 @@ and due <= ? limit ?)""",
# Reviews # Reviews
########################################################################## ##########################################################################
def _deckRevLimit(self, did): def _deckRevLimit(self, did) -> Any:
return self._deckNewLimit(did, self._deckRevLimitSingle) return self._deckNewLimit(did, self._deckRevLimitSingle)
def _deckRevLimitSingle(self, d): def _deckRevLimitSingle(self, d) -> Any:
if d['dyn']: if d['dyn']:
return self.reportLimit return self.reportLimit
c = self.col.decks.confForDid(d['id']) c = self.col.decks.confForDid(d['id'])
return max(0, c['rev']['perDay'] - d['revToday'][1]) return max(0, c['rev']['perDay'] - d['revToday'][1])
def _revForDeck(self, did, lim): def _revForDeck(self, did, lim) -> Any:
lim = min(lim, self.reportLimit) lim = min(lim, self.reportLimit)
return self.col.db.scalar( return self.col.db.scalar(
""" """
@ -723,7 +726,7 @@ select count() from
and due <= ? limit ?)""", and due <= ? limit ?)""",
did, self.today, lim) did, self.today, lim)
def _resetRevCount(self): def _resetRevCount(self) -> None:
def cntFn(did, lim): def cntFn(did, lim):
return self.col.db.scalar(""" return self.col.db.scalar("""
select count() from (select id from cards where select count() from (select id from cards where
@ -732,12 +735,12 @@ did = ? and queue = 2 and due <= ? limit %d)""" % lim,
self.revCount = self._walkingCount( self.revCount = self._walkingCount(
self._deckRevLimitSingle, cntFn) self._deckRevLimitSingle, cntFn)
def _resetRev(self): def _resetRev(self) -> None:
self._resetRevCount() self._resetRevCount()
self._revQueue = [] self._revQueue = []
self._revDids = self.col.decks.active()[:] self._revDids = self.col.decks.active()[:]
def _fillRev(self): def _fillRev(self) -> Any:
if self._revQueue: if self._revQueue:
return True return True
if not self.revCount: if not self.revCount:
@ -774,12 +777,12 @@ did = ? and queue = 2 and due <= ? limit ?""",
self._resetRev() self._resetRev()
return self._fillRev() return self._fillRev()
def _getRevCard(self): def _getRevCard(self) -> Any:
if self._fillRev(): if self._fillRev():
self.revCount -= 1 self.revCount -= 1
return self.col.getCard(self._revQueue.pop()) return self.col.getCard(self._revQueue.pop())
def totalRevForCurrentDeck(self): def totalRevForCurrentDeck(self) -> Any:
return self.col.db.scalar( return self.col.db.scalar(
""" """
select count() from cards where id in ( select count() from cards where id in (
@ -789,7 +792,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Answering a review card # Answering a review card
########################################################################## ##########################################################################
def _answerRevCard(self, card, ease): def _answerRevCard(self, card, ease) -> None:
delay = 0 delay = 0
if ease == 1: if ease == 1:
delay = self._rescheduleLapse(card) delay = self._rescheduleLapse(card)
@ -797,7 +800,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self._rescheduleRev(card, ease) self._rescheduleRev(card, ease)
self._logRev(card, ease, delay) self._logRev(card, ease, delay)
def _rescheduleLapse(self, card): def _rescheduleLapse(self, card) -> Any:
conf = self._lapseConf(card) conf = self._lapseConf(card)
card.lastIvl = card.ivl card.lastIvl = card.ivl
if self._resched(card): if self._resched(card):
@ -833,10 +836,10 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
card.queue = 3 card.queue = 3
return delay return delay
def _nextLapseIvl(self, card, conf): def _nextLapseIvl(self, card, conf) -> Any:
return max(conf['minInt'], int(card.ivl*conf['mult'])) return max(conf['minInt'], int(card.ivl*conf['mult']))
def _rescheduleRev(self, card, ease): def _rescheduleRev(self, card, ease) -> None:
# update interval # update interval
card.lastIvl = card.ivl card.lastIvl = card.ivl
if self._resched(card): if self._resched(card):
@ -851,7 +854,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
card.odid = 0 card.odid = 0
card.odue = 0 card.odue = 0
def _logRev(self, card, ease, delay): def _logRev(self, card, ease, delay) -> None:
def log(): def log():
self.col.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
@ -868,7 +871,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Interval management # Interval management
########################################################################## ##########################################################################
def _nextRevIvl(self, card, ease): def _nextRevIvl(self, card, ease) -> Any:
"Ideal next interval for CARD, given EASE." "Ideal next interval for CARD, given EASE."
delay = self._daysLate(card) delay = self._daysLate(card)
conf = self._revConf(card) conf = self._revConf(card)
@ -886,11 +889,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# interval capped? # interval capped?
return min(interval, conf['maxIvl']) return min(interval, conf['maxIvl'])
def _fuzzedIvl(self, ivl): def _fuzzedIvl(self, ivl) -> int:
min, max = self._fuzzIvlRange(ivl) min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max) return random.randint(min, max)
def _fuzzIvlRange(self, ivl): def _fuzzIvlRange(self, ivl) -> List:
if ivl < 2: if ivl < 2:
return [1, 1] return [1, 1]
elif ivl == 2: elif ivl == 2:
@ -905,22 +908,22 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
fuzz = max(fuzz, 1) fuzz = max(fuzz, 1)
return [ivl-fuzz, ivl+fuzz] return [ivl-fuzz, ivl+fuzz]
def _constrainedIvl(self, ivl, conf, prev): def _constrainedIvl(self, ivl, conf, prev) -> int:
"Integer interval after interval factor and prev+1 constraints applied." "Integer interval after interval factor and prev+1 constraints applied."
new = ivl * conf.get('ivlFct', 1) new = ivl * conf.get('ivlFct', 1)
return int(max(new, prev+1)) return int(max(new, prev+1))
def _daysLate(self, card): def _daysLate(self, card) -> Any:
"Number of days later than scheduled." "Number of days later than scheduled."
due = card.odue if card.odid else card.due due = card.odue if card.odid else card.due
return max(0, self.today - due) return max(0, self.today - due)
def _updateRevIvl(self, card, ease): def _updateRevIvl(self, card, ease) -> None:
idealIvl = self._nextRevIvl(card, ease) idealIvl = self._nextRevIvl(card, ease)
card.ivl = min(max(self._adjRevIvl(card, idealIvl), card.ivl+1), card.ivl = min(max(self._adjRevIvl(card, idealIvl), card.ivl+1),
self._revConf(card)['maxIvl']) self._revConf(card)['maxIvl'])
def _adjRevIvl(self, card, idealIvl): def _adjRevIvl(self, card, idealIvl) -> int:
if self._spreadRev: if self._spreadRev:
idealIvl = self._fuzzedIvl(idealIvl) idealIvl = self._fuzzedIvl(idealIvl)
return idealIvl return idealIvl
@ -928,7 +931,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Dynamic deck handling # Dynamic deck handling
########################################################################## ##########################################################################
def rebuildDyn(self, did=None): def rebuildDyn(self, did=None) -> Any:
"Rebuild a dynamic deck." "Rebuild a dynamic deck."
did = did or self.col.decks.selected() did = did or self.col.decks.selected()
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
@ -942,7 +945,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self.col.decks.select(did) self.col.decks.select(did)
return ids return ids
def _fillDyn(self, deck): def _fillDyn(self, deck) -> Any:
search, limit, order = deck['terms'][0] search, limit, order = deck['terms'][0]
orderlimit = self._dynOrder(order, limit) orderlimit = self._dynOrder(order, limit)
if search.strip(): if search.strip():
@ -958,7 +961,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self._moveToDyn(deck['id'], ids) self._moveToDyn(deck['id'], ids)
return ids return ids
def emptyDyn(self, did, lim=None): def emptyDyn(self, did, lim=None) -> None:
if not lim: if not lim:
lim = "did = %s" % did lim = "did = %s" % did
self.col.log(self.col.db.list("select id from cards where %s" % lim)) self.col.log(self.col.db.list("select id from cards where %s" % lim))
@ -969,10 +972,10 @@ else type end), type = (case when type = 1 then 0 else type end),
due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim, due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim,
self.col.usn()) self.col.usn())
def remFromDyn(self, cids): def remFromDyn(self, cids) -> None:
self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
def _dynOrder(self, o, l): def _dynOrder(self, o, l) -> str:
if o == DYN_OLDEST: if o == DYN_OLDEST:
t = "(select max(id) from revlog where cid=c.id)" t = "(select max(id) from revlog where cid=c.id)"
elif o == DYN_RANDOM: elif o == DYN_RANDOM:
@ -997,7 +1000,7 @@ due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim,
t = "c.due" t = "c.due"
return t + " limit %d" % l return t + " limit %d" % l
def _moveToDyn(self, did, ids): def _moveToDyn(self, did, ids) -> None:
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
data = [] data = []
t = intTime(); u = self.col.usn() t = intTime(); u = self.col.usn()
@ -1016,7 +1019,7 @@ odid = (case when odid then odid else did end),
odue = (case when odue then odue else due end), odue = (case when odue then odue else due end),
did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data) did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
def _dynIvlBoost(self, card): def _dynIvlBoost(self, card) -> Any:
assert card.odid and card.type == 2 assert card.odid and card.type == 2
assert card.factor assert card.factor
elapsed = card.ivl - (card.odue - self.today) elapsed = card.ivl - (card.odue - self.today)
@ -1028,7 +1031,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Leeches # Leeches
########################################################################## ##########################################################################
def _checkLeech(self, card, conf): def _checkLeech(self, card, conf) -> Optional[bool]:
"Leech handler. True if card was a leech." "Leech handler. True if card was a leech."
lf = conf['leechFails'] lf = conf['leechFails']
if not lf: if not lf:
@ -1057,10 +1060,10 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Tools # Tools
########################################################################## ##########################################################################
def _cardConf(self, card): def _cardConf(self, card) -> Any:
return self.col.decks.confForDid(card.did) return self.col.decks.confForDid(card.did)
def _newConf(self, card): def _newConf(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1080,7 +1083,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
perDay=self.reportLimit perDay=self.reportLimit
) )
def _lapseConf(self, card): def _lapseConf(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1099,7 +1102,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
resched=conf['resched'], resched=conf['resched'],
) )
def _revConf(self, card): def _revConf(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1107,10 +1110,10 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# dynamic deck # dynamic deck
return self.col.decks.confForDid(card.odid)['rev'] return self.col.decks.confForDid(card.odid)['rev']
def _deckLimit(self): def _deckLimit(self) -> str:
return ids2str(self.col.decks.active()) return ids2str(self.col.decks.active())
def _resched(self, card): def _resched(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
if not conf['dyn']: if not conf['dyn']:
return True return True
@ -1119,7 +1122,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Daily cutoff # Daily cutoff
########################################################################## ##########################################################################
def _updateCutoff(self): def _updateCutoff(self) -> None:
oldToday = self.today oldToday = self.today
# days since col created # days since col created
self.today = int((time.time() - self.col.crt) // 86400) self.today = int((time.time() - self.col.crt) // 86400)
@ -1141,7 +1144,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
if unburied < self.today: if unburied < self.today:
self.unburyCards() self.unburyCards()
def _checkDay(self): def _checkDay(self) -> None:
# check if the day has rolled over # check if the day has rolled over
if time.time() > self.dayCutoff: if time.time() > self.dayCutoff:
self.reset() self.reset()
@ -1149,12 +1152,12 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Deck finished state # Deck finished state
########################################################################## ##########################################################################
def finishedMsg(self): def finishedMsg(self) -> str:
return ("<b>"+_( return ("<b>"+_(
"Congratulations! You have finished this deck for now.")+ "Congratulations! You have finished this deck for now.")+
"</b><br><br>" + self._nextDueMsg()) "</b><br><br>" + self._nextDueMsg())
def _nextDueMsg(self): def _nextDueMsg(self) -> str:
line = [] line = []
# the new line replacements are so we don't break translations # the new line replacements are so we don't break translations
# in a point release # in a point release
@ -1181,20 +1184,20 @@ Some related or buried cards were delayed until a later session.""")+now)
To study outside of the normal schedule, click the Custom Study button below.""")) To study outside of the normal schedule, click the Custom Study button below."""))
return "<p>".join(line) return "<p>".join(line)
def revDue(self): def revDue(self) -> Any:
"True if there are any rev cards due." "True if there are any rev cards due."
return self.col.db.scalar( return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 2 " ("select 1 from cards where did in %s and queue = 2 "
"and due <= ? limit 1") % self._deckLimit(), "and due <= ? limit 1") % self._deckLimit(),
self.today) self.today)
def newDue(self): def newDue(self) -> Any:
"True if there are any new cards due." "True if there are any new cards due."
return self.col.db.scalar( return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 0 " ("select 1 from cards where did in %s and queue = 0 "
"limit 1") % self._deckLimit()) "limit 1") % self._deckLimit())
def haveBuried(self): def haveBuried(self) -> bool:
sdids = ids2str(self.col.decks.active()) sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar( cnt = self.col.db.scalar(
"select 1 from cards where queue = -2 and did in %s limit 1" % sdids) "select 1 from cards where queue = -2 and did in %s limit 1" % sdids)
@ -1203,7 +1206,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# Next time reports # Next time reports
########################################################################## ##########################################################################
def nextIvlStr(self, card, ease, short=False): def nextIvlStr(self, card, ease, short=False) -> Any:
"Return the next interval for CARD as a string." "Return the next interval for CARD as a string."
ivl = self.nextIvl(card, ease) ivl = self.nextIvl(card, ease)
if not ivl: if not ivl:
@ -1213,7 +1216,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
s = "<"+s s = "<"+s
return s return s
def nextIvl(self, card, ease): def nextIvl(self, card, ease) -> Any:
"Return the next interval for CARD, in seconds." "Return the next interval for CARD, in seconds."
if card.queue in (0,1,3): if card.queue in (0,1,3):
return self._nextLrnIvl(card, ease) return self._nextLrnIvl(card, ease)
@ -1228,7 +1231,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
return self._nextRevIvl(card, ease)*86400 return self._nextRevIvl(card, ease)*86400
# this isn't easily extracted from the learn code # this isn't easily extracted from the learn code
def _nextLrnIvl(self, card, ease): def _nextLrnIvl(self, card, ease) -> Any:
if card.queue == 0: if card.queue == 0:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
conf = self._lrnConf(card) conf = self._lrnConf(card)
@ -1253,7 +1256,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# Suspending # Suspending
########################################################################## ##########################################################################
def suspendCards(self, ids): def suspendCards(self, ids) -> None:
"Suspend cards." "Suspend cards."
self.col.log(ids) self.col.log(ids)
self.remFromDyn(ids) self.remFromDyn(ids)
@ -1262,7 +1265,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
"update cards set queue=-1,mod=?,usn=? where id in "+ "update cards set queue=-1,mod=?,usn=? where id in "+
ids2str(ids), intTime(), self.col.usn()) ids2str(ids), intTime(), self.col.usn())
def unsuspendCards(self, ids): def unsuspendCards(self, ids) -> None:
"Unsuspend cards." "Unsuspend cards."
self.col.log(ids) self.col.log(ids)
self.col.db.execute( self.col.db.execute(
@ -1270,7 +1273,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
"where queue = -1 and id in "+ ids2str(ids), "where queue = -1 and id in "+ ids2str(ids),
intTime(), self.col.usn()) intTime(), self.col.usn())
def buryCards(self, cids): def buryCards(self, cids) -> None:
self.col.log(cids) self.col.log(cids)
self.remFromDyn(cids) self.remFromDyn(cids)
self.removeLrn(cids) self.removeLrn(cids)
@ -1278,7 +1281,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids), update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
intTime(), self.col.usn()) intTime(), self.col.usn())
def buryNote(self, nid): def buryNote(self, nid) -> None:
"Bury all cards for note until next session." "Bury all cards for note until next session."
cids = self.col.db.list( cids = self.col.db.list(
"select id from cards where nid = ? and queue >= 0", nid) "select id from cards where nid = ? and queue >= 0", nid)
@ -1287,7 +1290,7 @@ update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
# Sibling spacing # Sibling spacing
########################################################################## ##########################################################################
def _burySiblings(self, card): def _burySiblings(self, card) -> None:
toBury = [] toBury = []
nconf = self._newConf(card) nconf = self._newConf(card)
buryNew = nconf.get("bury", True) buryNew = nconf.get("bury", True)
@ -1324,7 +1327,7 @@ and (queue=0 or (queue=2 and due<=?))""",
# Resetting # Resetting
########################################################################## ##########################################################################
def forgetCards(self, ids): def forgetCards(self, ids) -> None:
"Put cards at the end of the new queue." "Put cards at the end of the new queue."
self.remFromDyn(ids) self.remFromDyn(ids)
self.col.db.execute( self.col.db.execute(
@ -1336,7 +1339,7 @@ and (queue=0 or (queue=2 and due<=?))""",
self.sortCards(ids, start=pmax+1) self.sortCards(ids, start=pmax+1)
self.col.log(ids) self.col.log(ids)
def reschedCards(self, ids, imin, imax): def reschedCards(self, ids, imin, imax) -> None:
"Put cards in review queue with a new interval in days (min, max)." "Put cards in review queue with a new interval in days (min, max)."
d = [] d = []
t = self.today t = self.today
@ -1352,7 +1355,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
d) d)
self.col.log(ids) self.col.log(ids)
def resetCards(self, ids): def resetCards(self, ids) -> None:
"Completely reset cards for export." "Completely reset cards for export."
sids = ids2str(ids) sids = ids2str(ids)
# we want to avoid resetting due number of existing new cards on export # we want to avoid resetting due number of existing new cards on export
@ -1371,7 +1374,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
# Repositioning new cards # Repositioning new cards
########################################################################## ##########################################################################
def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False) -> None:
scids = ids2str(cids) scids = ids2str(cids)
now = intTime() now = intTime()
nids = [] nids = []
@ -1411,15 +1414,15 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.col.db.executemany( self.col.db.executemany(
"update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d)
def randomizeCards(self, did): def randomizeCards(self, did) -> None:
cids = self.col.db.list("select id from cards where did = ?", did) cids = self.col.db.list("select id from cards where did = ?", did)
self.sortCards(cids, shuffle=True) self.sortCards(cids, shuffle=True)
def orderCards(self, did): def orderCards(self, did) -> None:
cids = self.col.db.list("select id from cards where did = ? order by id", did) cids = self.col.db.list("select id from cards where did = ? order by id", did)
self.sortCards(cids) self.sortCards(cids)
def resortConf(self, conf): def resortConf(self, conf) -> None:
for did in self.col.decks.didsForConf(conf): for did in self.col.decks.didsForConf(conf):
if conf['new']['order'] == 0: if conf['new']['order'] == 0:
self.randomizeCards(did) self.randomizeCards(did)
@ -1427,7 +1430,7 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.orderCards(did) self.orderCards(did)
# for post-import # for post-import
def maybeRandomizeDeck(self, did=None): def maybeRandomizeDeck(self, did=None) -> None:
if not did: if not did:
did = self.col.decks.selected() did = self.col.decks.selected()
conf = self.col.decks.confForDid(did) conf = self.col.decks.confForDid(did)

View File

@ -13,6 +13,7 @@ from anki.utils import ids2str, intTime, fmtTimeSpan
from anki.lang import _ from anki.lang import _
from anki.consts import * from anki.consts import *
from anki.hooks import runHook from anki.hooks import runHook
from typing import Any, List, Optional, Tuple
# card types: 0=new, 1=lrn, 2=rev, 3=relrn # card types: 0=new, 1=lrn, 2=rev, 3=relrn
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, # queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
@ -26,7 +27,7 @@ class Scheduler:
haveCustomStudy = True haveCustomStudy = True
_burySiblingsOnAnswer = True _burySiblingsOnAnswer = True
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self.queueLimit = 50 self.queueLimit = 50
self.reportLimit = 1000 self.reportLimit = 1000
@ -37,7 +38,7 @@ class Scheduler:
self._lrnCutoff = 0 self._lrnCutoff = 0
self._updateCutoff() self._updateCutoff()
def getCard(self): def getCard(self) -> Any:
"Pop the next card from the queue. None if finished." "Pop the next card from the queue. None if finished."
self._checkDay() self._checkDay()
if not self._haveQueues: if not self._haveQueues:
@ -51,14 +52,14 @@ class Scheduler:
card.startTimer() card.startTimer()
return card return card
def reset(self): def reset(self) -> None:
self._updateCutoff() self._updateCutoff()
self._resetLrn() self._resetLrn()
self._resetRev() self._resetRev()
self._resetNew() self._resetNew()
self._haveQueues = True self._haveQueues = True
def answerCard(self, card, ease): def answerCard(self, card, ease) -> None:
self.col.log() self.col.log()
assert 1 <= ease <= 4 assert 1 <= ease <= 4
assert 0 <= card.queue <= 4 assert 0 <= card.queue <= 4
@ -73,7 +74,7 @@ class Scheduler:
card.usn = self.col.usn() card.usn = self.col.usn()
card.flushSched() card.flushSched()
def _answerCard(self, card, ease): def _answerCard(self, card, ease) -> None:
if self._previewingCard(card): if self._previewingCard(card):
self._answerCardPreview(card, ease) self._answerCardPreview(card, ease)
return return
@ -103,7 +104,7 @@ class Scheduler:
if card.odue: if card.odue:
card.odue = 0 card.odue = 0
def _answerCardPreview(self, card, ease): def _answerCardPreview(self, card, ease) -> None:
assert 1 <= ease <= 2 assert 1 <= ease <= 2
if ease == 1: if ease == 1:
@ -116,14 +117,14 @@ class Scheduler:
self._restorePreviewCard(card) self._restorePreviewCard(card)
self._removeFromFiltered(card) self._removeFromFiltered(card)
def counts(self, card=None): def counts(self, card=None) -> tuple:
counts = [self.newCount, self.lrnCount, self.revCount] counts = [self.newCount, self.lrnCount, self.revCount]
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)
counts[idx] += 1 counts[idx] += 1
return tuple(counts) return tuple(counts)
def dueForecast(self, days=7): def dueForecast(self, days=7) -> List:
"Return counts over next DAYS. Includes today." "Return counts over next DAYS. Includes today."
daysd = dict(self.col.db.all(""" daysd = dict(self.col.db.all("""
select due, count() from cards select due, count() from cards
@ -141,12 +142,12 @@ order by due""" % self._deckLimit(),
ret = [x[1] for x in sorted(daysd.items())] ret = [x[1] for x in sorted(daysd.items())]
return ret return ret
def countIdx(self, card): def countIdx(self, card) -> Any:
if card.queue in (3,4): if card.queue in (3,4):
return 1 return 1
return card.queue return card.queue
def answerButtons(self, card): def answerButtons(self, card) -> int:
conf = self._cardConf(card) conf = self._cardConf(card)
if card.odid and not conf['resched']: if card.odid and not conf['resched']:
return 2 return 2
@ -155,7 +156,7 @@ order by due""" % self._deckLimit(),
# Rev/lrn/time daily stats # Rev/lrn/time daily stats
########################################################################## ##########################################################################
def _updateStats(self, card, type, cnt=1): def _updateStats(self, card, type, cnt=1) -> None:
key = type+"Today" key = type+"Today"
for g in ([self.col.decks.get(card.did)] + for g in ([self.col.decks.get(card.did)] +
self.col.decks.parents(card.did)): self.col.decks.parents(card.did)):
@ -163,7 +164,7 @@ order by due""" % self._deckLimit(),
g[key][1] += cnt g[key][1] += cnt
self.col.decks.save(g) self.col.decks.save(g)
def extendLimits(self, new, rev): def extendLimits(self, new, rev) -> None:
cur = self.col.decks.current() cur = self.col.decks.current()
parents = self.col.decks.parents(cur['id']) parents = self.col.decks.parents(cur['id'])
children = [self.col.decks.get(did) for (name, did) in children = [self.col.decks.get(did) for (name, did) in
@ -174,7 +175,7 @@ order by due""" % self._deckLimit(),
g['revToday'][1] -= rev g['revToday'][1] -= rev
self.col.decks.save(g) self.col.decks.save(g)
def _walkingCount(self, limFn=None, cntFn=None): def _walkingCount(self, limFn=None, cntFn=None) -> Any:
tot = 0 tot = 0
pcounts = {} pcounts = {}
# for each of the active decks # for each of the active decks
@ -208,7 +209,7 @@ order by due""" % self._deckLimit(),
# Deck list # Deck list
########################################################################## ##########################################################################
def deckDueList(self): def deckDueList(self) -> List[list]:
"Returns [deckname, did, rev, lrn, new]" "Returns [deckname, did, rev, lrn, new]"
self._checkDay() self._checkDay()
self.col.decks.checkIntegrity() self.col.decks.checkIntegrity()
@ -245,10 +246,10 @@ order by due""" % self._deckLimit(),
lims[deck['name']] = [nlim, rlim] lims[deck['name']] = [nlim, rlim]
return data return data
def deckDueTree(self): def deckDueTree(self) -> Any:
return self._groupChildren(self.deckDueList()) return self._groupChildren(self.deckDueList())
def _groupChildren(self, grps): def _groupChildren(self, grps) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]:
# first, split the group names into components # first, split the group names into components
for g in grps: for g in grps:
g[0] = g[0].split("::") g[0] = g[0].split("::")
@ -257,7 +258,7 @@ order by due""" % self._deckLimit(),
# then run main function # then run main function
return self._groupChildrenMain(grps) return self._groupChildrenMain(grps)
def _groupChildrenMain(self, grps): def _groupChildrenMain(self, grps) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]:
tree = [] tree = []
# group and recurse # group and recurse
def key(grp): def key(grp):
@ -296,7 +297,7 @@ order by due""" % self._deckLimit(),
# Getting the next card # Getting the next card
########################################################################## ##########################################################################
def _getCard(self): def _getCard(self) -> Any:
"Return the next due card id, or None." "Return the next due card id, or None."
# learning card due? # learning card due?
c = self._getLrnCard() c = self._getLrnCard()
@ -338,19 +339,19 @@ order by due""" % self._deckLimit(),
# New cards # New cards
########################################################################## ##########################################################################
def _resetNewCount(self): def _resetNewCount(self) -> None:
cntFn = lambda did, lim: self.col.db.scalar(""" cntFn = lambda did, lim: self.col.db.scalar("""
select count() from (select 1 from cards where select count() from (select 1 from cards where
did = ? and queue = 0 limit ?)""", did, lim) did = ? and queue = 0 limit ?)""", did, lim)
self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn)
def _resetNew(self): def _resetNew(self) -> None:
self._resetNewCount() self._resetNewCount()
self._newDids = self.col.decks.active()[:] self._newDids = self.col.decks.active()[:]
self._newQueue = [] self._newQueue = []
self._updateNewCardRatio() self._updateNewCardRatio()
def _fillNew(self): def _fillNew(self) -> Any:
if self._newQueue: if self._newQueue:
return True return True
if not self.newCount: if not self.newCount:
@ -374,12 +375,12 @@ did = ? and queue = 0 limit ?)""", did, lim)
self._resetNew() self._resetNew()
return self._fillNew() return self._fillNew()
def _getNewCard(self): def _getNewCard(self) -> Any:
if self._fillNew(): if self._fillNew():
self.newCount -= 1 self.newCount -= 1
return self.col.getCard(self._newQueue.pop()) return self.col.getCard(self._newQueue.pop())
def _updateNewCardRatio(self): def _updateNewCardRatio(self) -> None:
if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE: if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.newCount: if self.newCount:
self.newCardModulus = ( self.newCardModulus = (
@ -390,7 +391,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
return return
self.newCardModulus = 0 self.newCardModulus = 0
def _timeForNewCard(self): def _timeForNewCard(self) -> Optional[int]:
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
if not self.newCount: if not self.newCount:
return False return False
@ -401,7 +402,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
elif self.newCardModulus: elif self.newCardModulus:
return self.reps and self.reps % self.newCardModulus == 0 return self.reps and self.reps % self.newCardModulus == 0
def _deckNewLimit(self, did, fn=None): def _deckNewLimit(self, did, fn=None) -> Any:
if not fn: if not fn:
fn = self._deckNewLimitSingle fn = self._deckNewLimitSingle
sel = self.col.decks.get(did) sel = self.col.decks.get(did)
@ -415,7 +416,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
lim = min(rem, lim) lim = min(rem, lim)
return lim return lim
def _newForDeck(self, did, lim): def _newForDeck(self, did, lim) -> Any:
"New count for a single deck." "New count for a single deck."
if not lim: if not lim:
return 0 return 0
@ -424,14 +425,14 @@ did = ? and queue = 0 limit ?)""", did, lim)
select count() from select count() from
(select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim) (select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim)
def _deckNewLimitSingle(self, g): def _deckNewLimitSingle(self, g) -> Any:
"Limit for deck without parent limits." "Limit for deck without parent limits."
if g['dyn']: if g['dyn']:
return self.dynReportLimit return self.dynReportLimit
c = self.col.decks.confForDid(g['id']) c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1]) return max(0, c['new']['perDay'] - g['newToday'][1])
def totalNewForCurrentDeck(self): def totalNewForCurrentDeck(self) -> Any:
return self.col.db.scalar( return self.col.db.scalar(
""" """
select count() from cards where id in ( select count() from cards where id in (
@ -442,18 +443,18 @@ select id from cards where did in %s and queue = 0 limit ?)"""
########################################################################## ##########################################################################
# scan for any newly due learning cards every minute # scan for any newly due learning cards every minute
def _updateLrnCutoff(self, force): def _updateLrnCutoff(self, force) -> bool:
nextCutoff = intTime() + self.col.conf['collapseTime'] nextCutoff = intTime() + self.col.conf['collapseTime']
if nextCutoff - self._lrnCutoff > 60 or force: if nextCutoff - self._lrnCutoff > 60 or force:
self._lrnCutoff = nextCutoff self._lrnCutoff = nextCutoff
return True return True
return False return False
def _maybeResetLrn(self, force): def _maybeResetLrn(self, force) -> None:
if self._updateLrnCutoff(force): if self._updateLrnCutoff(force):
self._resetLrn() self._resetLrn()
def _resetLrnCount(self): def _resetLrnCount(self) -> None:
# sub-day # sub-day
self.lrnCount = self.col.db.scalar(""" self.lrnCount = self.col.db.scalar("""
select count() from cards where did in %s and queue = 1 select count() from cards where did in %s and queue = 1
@ -470,7 +471,7 @@ and due <= ?""" % (self._deckLimit()),
select count() from cards where did in %s and queue = 4 select count() from cards where did in %s and queue = 4
""" % (self._deckLimit())) """ % (self._deckLimit()))
def _resetLrn(self): def _resetLrn(self) -> None:
self._updateLrnCutoff(force=True) self._updateLrnCutoff(force=True)
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue = [] self._lrnQueue = []
@ -478,7 +479,7 @@ select count() from cards where did in %s and queue = 4
self._lrnDids = self.col.decks.active()[:] self._lrnDids = self.col.decks.active()[:]
# sub-day learning # sub-day learning
def _fillLrn(self): def _fillLrn(self) -> Any:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnQueue: if self._lrnQueue:
@ -492,7 +493,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=cutoff)
self._lrnQueue.sort() self._lrnQueue.sort()
return self._lrnQueue return self._lrnQueue
def _getLrnCard(self, collapse=False): def _getLrnCard(self, collapse=False) -> Any:
self._maybeResetLrn(force=collapse and self.lrnCount == 0) self._maybeResetLrn(force=collapse and self.lrnCount == 0)
if self._fillLrn(): if self._fillLrn():
cutoff = time.time() cutoff = time.time()
@ -505,7 +506,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=cutoff)
return card return card
# daily learning # daily learning
def _fillLrnDay(self): def _fillLrnDay(self) -> Optional[bool]:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnDayQueue: if self._lrnDayQueue:
@ -529,12 +530,12 @@ did = ? and queue = 3 and due <= ? limit ?""",
# nothing left in the deck; move to next # nothing left in the deck; move to next
self._lrnDids.pop(0) self._lrnDids.pop(0)
def _getLrnDayCard(self): def _getLrnDayCard(self) -> Any:
if self._fillLrnDay(): if self._fillLrnDay():
self.lrnCount -= 1 self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop()) return self.col.getCard(self._lrnDayQueue.pop())
def _answerLrnCard(self, card, ease): def _answerLrnCard(self, card, ease) -> None:
conf = self._lrnConf(card) conf = self._lrnConf(card)
if card.type in (2,3): if card.type in (2,3):
type = 2 type = 2
@ -565,11 +566,11 @@ did = ? and queue = 3 and due <= ? limit ?""",
self._logLrn(card, ease, conf, leaving, type, lastLeft) self._logLrn(card, ease, conf, leaving, type, lastLeft)
def _updateRevIvlOnFail(self, card, conf): def _updateRevIvlOnFail(self, card, conf) -> None:
card.lastIvl = card.ivl card.lastIvl = card.ivl
card.ivl = self._lapseIvl(card, conf) card.ivl = self._lapseIvl(card, conf)
def _moveToFirstStep(self, card, conf): def _moveToFirstStep(self, card, conf) -> Any:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
# relearning card? # relearning card?
@ -578,18 +579,18 @@ did = ? and queue = 3 and due <= ? limit ?""",
return self._rescheduleLrnCard(card, conf) return self._rescheduleLrnCard(card, conf)
def _moveToNextStep(self, card, conf): def _moveToNextStep(self, card, conf) -> None:
# decrement real left count and recalculate left today # decrement real left count and recalculate left today
left = (card.left % 1000) - 1 left = (card.left % 1000) - 1
card.left = self._leftToday(conf['delays'], left)*1000 + left card.left = self._leftToday(conf['delays'], left)*1000 + left
self._rescheduleLrnCard(card, conf) self._rescheduleLrnCard(card, conf)
def _repeatStep(self, card, conf): def _repeatStep(self, card, conf) -> None:
delay = self._delayForRepeatingGrade(conf, card.left) delay = self._delayForRepeatingGrade(conf, card.left)
self._rescheduleLrnCard(card, conf, delay=delay) self._rescheduleLrnCard(card, conf, delay=delay)
def _rescheduleLrnCard(self, card, conf, delay=None): def _rescheduleLrnCard(self, card, conf, delay=None) -> Any:
# normal delay for the current step? # normal delay for the current step?
if delay is None: if delay is None:
delay = self._delayForGrade(conf, card.left) delay = self._delayForGrade(conf, card.left)
@ -619,7 +620,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.queue = 3 card.queue = 3
return delay return delay
def _delayForGrade(self, conf, left): def _delayForGrade(self, conf, left) -> Any:
left = left % 1000 left = left % 1000
try: try:
delay = conf['delays'][-left] delay = conf['delays'][-left]
@ -631,7 +632,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
delay = 1 delay = 1
return delay*60 return delay*60
def _delayForRepeatingGrade(self, conf, left): def _delayForRepeatingGrade(self, conf, left) -> Any:
# halfway between last and next # halfway between last and next
delay1 = self._delayForGrade(conf, left) delay1 = self._delayForGrade(conf, left)
if len(conf['delays']) > 1: if len(conf['delays']) > 1:
@ -641,13 +642,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
avg = (delay1+max(delay1, delay2))//2 avg = (delay1+max(delay1, delay2))//2
return avg return avg
def _lrnConf(self, card): def _lrnConf(self, card) -> Any:
if card.type in (2, 3): if card.type in (2, 3):
return self._lapseConf(card) return self._lapseConf(card)
else: else:
return self._newConf(card) return self._newConf(card)
def _rescheduleAsRev(self, card, conf, early): def _rescheduleAsRev(self, card, conf, early) -> None:
lapse = card.type in (2,3) lapse = card.type in (2,3)
if lapse: if lapse:
@ -659,14 +660,14 @@ did = ? and queue = 3 and due <= ? limit ?""",
if card.odid: if card.odid:
self._removeFromFiltered(card) self._removeFromFiltered(card)
def _rescheduleGraduatingLapse(self, card, early=False): def _rescheduleGraduatingLapse(self, card, early=False) -> None:
if early: if early:
card.ivl += 1 card.ivl += 1
card.due = self.today+card.ivl card.due = self.today+card.ivl
card.queue = 2 card.queue = 2
card.type = 2 card.type = 2
def _startingLeft(self, card): def _startingLeft(self, card) -> int:
if card.type == 3: if card.type == 3:
conf = self._lapseConf(card) conf = self._lapseConf(card)
else: else:
@ -675,7 +676,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
tod = self._leftToday(conf['delays'], tot) tod = self._leftToday(conf['delays'], tot)
return tot + tod*1000 return tot + tod*1000
def _leftToday(self, delays, left, now=None): def _leftToday(self, delays, left, now=None) -> int:
"The number of steps that can be completed by the day cutoff." "The number of steps that can be completed by the day cutoff."
if not now: if not now:
now = intTime() now = intTime()
@ -688,7 +689,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
ok = i ok = i
return ok+1 return ok+1
def _graduatingIvl(self, card, conf, early, fuzz=True): def _graduatingIvl(self, card, conf, early, fuzz=True) -> Any:
if card.type in (2,3): if card.type in (2,3):
bonus = early and 1 or 0 bonus = early and 1 or 0
return card.ivl + bonus return card.ivl + bonus
@ -702,14 +703,14 @@ did = ? and queue = 3 and due <= ? limit ?""",
ideal = self._fuzzedIvl(ideal) ideal = self._fuzzedIvl(ideal)
return ideal return ideal
def _rescheduleNew(self, card, conf, early): def _rescheduleNew(self, card, conf, early) -> None:
"Reschedule a new card that's graduated for the first time." "Reschedule a new card that's graduated for the first time."
card.ivl = self._graduatingIvl(card, conf, early) card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today+card.ivl card.due = self.today+card.ivl
card.factor = conf['initialFactor'] card.factor = conf['initialFactor']
card.type = card.queue = 2 card.type = card.queue = 2
def _logLrn(self, card, ease, conf, leaving, type, lastLeft): def _logLrn(self, card, ease, conf, leaving, type, lastLeft) -> None:
lastIvl = -(self._delayForGrade(conf, lastLeft)) lastIvl = -(self._delayForGrade(conf, lastLeft))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log(): def log():
@ -724,7 +725,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
time.sleep(0.01) time.sleep(0.01)
log() log()
def _lrnForDeck(self, did): def _lrnForDeck(self, did) -> Any:
cnt = self.col.db.scalar( cnt = self.col.db.scalar(
""" """
select count() from select count() from
@ -740,11 +741,11 @@ and due <= ? limit ?)""",
# Reviews # Reviews
########################################################################## ##########################################################################
def _currentRevLimit(self): def _currentRevLimit(self) -> Any:
d = self.col.decks.get(self.col.decks.selected(), default=False) d = self.col.decks.get(self.col.decks.selected(), default=False)
return self._deckRevLimitSingle(d) return self._deckRevLimitSingle(d)
def _deckRevLimitSingle(self, d, parentLimit=None): def _deckRevLimitSingle(self, d, parentLimit=None) -> Any:
# invalid deck selected? # invalid deck selected?
if not d: if not d:
return 0 return 0
@ -765,7 +766,7 @@ and due <= ? limit ?)""",
lim = min(lim, self._deckRevLimitSingle(parent, parentLimit=lim)) lim = min(lim, self._deckRevLimitSingle(parent, parentLimit=lim))
return lim return lim
def _revForDeck(self, did, lim, childMap): def _revForDeck(self, did, lim, childMap) -> Any:
dids = [did] + self.col.decks.childDids(did, childMap) dids = [did] + self.col.decks.childDids(did, childMap)
lim = min(lim, self.reportLimit) lim = min(lim, self.reportLimit)
return self.col.db.scalar( return self.col.db.scalar(
@ -775,18 +776,18 @@ select count() from
and due <= ? limit ?)""" % ids2str(dids), and due <= ? limit ?)""" % ids2str(dids),
self.today, lim) self.today, lim)
def _resetRevCount(self): def _resetRevCount(self) -> None:
lim = self._currentRevLimit() lim = self._currentRevLimit()
self.revCount = self.col.db.scalar(""" self.revCount = self.col.db.scalar("""
select count() from (select id from cards where select count() from (select id from cards where
did in %s and queue = 2 and due <= ? limit %d)""" % ( did in %s and queue = 2 and due <= ? limit %d)""" % (
ids2str(self.col.decks.active()), lim), self.today) ids2str(self.col.decks.active()), lim), self.today)
def _resetRev(self): def _resetRev(self) -> None:
self._resetRevCount() self._resetRevCount()
self._revQueue = [] self._revQueue = []
def _fillRev(self): def _fillRev(self) -> Any:
if self._revQueue: if self._revQueue:
return True return True
if not self.revCount: if not self.revCount:
@ -813,12 +814,12 @@ limit ?""" % (ids2str(self.col.decks.active())),
self._resetRev() self._resetRev()
return self._fillRev() return self._fillRev()
def _getRevCard(self): def _getRevCard(self) -> Any:
if self._fillRev(): if self._fillRev():
self.revCount -= 1 self.revCount -= 1
return self.col.getCard(self._revQueue.pop()) return self.col.getCard(self._revQueue.pop())
def totalRevForCurrentDeck(self): def totalRevForCurrentDeck(self) -> Any:
return self.col.db.scalar( return self.col.db.scalar(
""" """
select count() from cards where id in ( select count() from cards where id in (
@ -828,7 +829,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Answering a review card # Answering a review card
########################################################################## ##########################################################################
def _answerRevCard(self, card, ease): def _answerRevCard(self, card, ease) -> None:
delay = 0 delay = 0
early = card.odid and (card.odue > self.today) early = card.odid and (card.odue > self.today)
type = early and 3 or 1 type = early and 3 or 1
@ -840,7 +841,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self._logRev(card, ease, delay, type) self._logRev(card, ease, delay, type)
def _rescheduleLapse(self, card): def _rescheduleLapse(self, card) -> Any:
conf = self._lapseConf(card) conf = self._lapseConf(card)
card.lapses += 1 card.lapses += 1
@ -862,11 +863,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
return delay return delay
def _lapseIvl(self, card, conf): def _lapseIvl(self, card, conf) -> Any:
ivl = max(1, conf['minInt'], int(card.ivl*conf['mult'])) ivl = max(1, conf['minInt'], int(card.ivl*conf['mult']))
return ivl return ivl
def _rescheduleRev(self, card, ease, early): def _rescheduleRev(self, card, ease, early) -> None:
# update interval # update interval
card.lastIvl = card.ivl card.lastIvl = card.ivl
if early: if early:
@ -881,7 +882,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# card leaves filtered deck # card leaves filtered deck
self._removeFromFiltered(card) self._removeFromFiltered(card)
def _logRev(self, card, ease, delay, type): def _logRev(self, card, ease, delay, type) -> None:
def log(): def log():
self.col.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
@ -898,7 +899,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Interval management # Interval management
########################################################################## ##########################################################################
def _nextRevIvl(self, card, ease, fuzz): def _nextRevIvl(self, card, ease, fuzz) -> int:
"Next review interval for CARD, given EASE." "Next review interval for CARD, given EASE."
delay = self._daysLate(card) delay = self._daysLate(card)
conf = self._revConf(card) conf = self._revConf(card)
@ -920,11 +921,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
(card.ivl + delay) * fct * conf['ease4'], conf, ivl3, fuzz) (card.ivl + delay) * fct * conf['ease4'], conf, ivl3, fuzz)
return ivl4 return ivl4
def _fuzzedIvl(self, ivl): def _fuzzedIvl(self, ivl) -> int:
min, max = self._fuzzIvlRange(ivl) min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max) return random.randint(min, max)
def _fuzzIvlRange(self, ivl): def _fuzzIvlRange(self, ivl) -> List:
if ivl < 2: if ivl < 2:
return [1, 1] return [1, 1]
elif ivl == 2: elif ivl == 2:
@ -939,7 +940,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
fuzz = max(fuzz, 1) fuzz = max(fuzz, 1)
return [ivl-fuzz, ivl+fuzz] return [ivl-fuzz, ivl+fuzz]
def _constrainedIvl(self, ivl, conf, prev, fuzz): def _constrainedIvl(self, ivl, conf, prev, fuzz) -> int:
ivl = int(ivl * conf.get('ivlFct', 1)) ivl = int(ivl * conf.get('ivlFct', 1))
if fuzz: if fuzz:
ivl = self._fuzzedIvl(ivl) ivl = self._fuzzedIvl(ivl)
@ -947,19 +948,19 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
ivl = min(ivl, conf['maxIvl']) ivl = min(ivl, conf['maxIvl'])
return int(ivl) return int(ivl)
def _daysLate(self, card): def _daysLate(self, card) -> Any:
"Number of days later than scheduled." "Number of days later than scheduled."
due = card.odue if card.odid else card.due due = card.odue if card.odid else card.due
return max(0, self.today - due) return max(0, self.today - due)
def _updateRevIvl(self, card, ease): def _updateRevIvl(self, card, ease) -> None:
card.ivl = self._nextRevIvl(card, ease, fuzz=True) card.ivl = self._nextRevIvl(card, ease, fuzz=True)
def _updateEarlyRevIvl(self, card, ease): def _updateEarlyRevIvl(self, card, ease) -> None:
card.ivl = self._earlyReviewIvl(card, ease) card.ivl = self._earlyReviewIvl(card, ease)
# next interval for card when answered early+correctly # next interval for card when answered early+correctly
def _earlyReviewIvl(self, card, ease): def _earlyReviewIvl(self, card, ease) -> int:
assert card.odid and card.type == 2 assert card.odid and card.type == 2
assert card.factor assert card.factor
assert ease > 1 assert ease > 1
@ -997,7 +998,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Dynamic deck handling # Dynamic deck handling
########################################################################## ##########################################################################
def rebuildDyn(self, did=None): def rebuildDyn(self, did=None) -> Optional[int]:
"Rebuild a dynamic deck." "Rebuild a dynamic deck."
did = did or self.col.decks.selected() did = did or self.col.decks.selected()
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
@ -1011,7 +1012,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self.col.decks.select(did) self.col.decks.select(did)
return cnt return cnt
def _fillDyn(self, deck): def _fillDyn(self, deck) -> int:
start = -100000 start = -100000
total = 0 total = 0
for search, limit, order in deck['terms']: for search, limit, order in deck['terms']:
@ -1029,7 +1030,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
total += len(ids) total += len(ids)
return total return total
def emptyDyn(self, did, lim=None): def emptyDyn(self, did, lim=None) -> None:
if not lim: if not lim:
lim = "did = %s" % did lim = "did = %s" % did
self.col.log(self.col.db.list("select id from cards where %s" % lim)) self.col.log(self.col.db.list("select id from cards where %s" % lim))
@ -1040,10 +1041,10 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
self._restoreQueueSnippet, lim), self._restoreQueueSnippet, lim),
self.col.usn()) self.col.usn())
def remFromDyn(self, cids): def remFromDyn(self, cids) -> None:
self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
def _dynOrder(self, o, l): def _dynOrder(self, o, l) -> str:
if o == DYN_OLDEST: if o == DYN_OLDEST:
t = "(select max(id) from revlog where cid=c.id)" t = "(select max(id) from revlog where cid=c.id)"
elif o == DYN_RANDOM: elif o == DYN_RANDOM:
@ -1065,7 +1066,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
t = "c.due, c.ord" t = "c.due, c.ord"
return t + " limit %d" % l return t + " limit %d" % l
def _moveToDyn(self, did, ids, start=-100000): def _moveToDyn(self, did, ids, start=-100000) -> None:
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
data = [] data = []
u = self.col.usn() u = self.col.usn()
@ -1089,13 +1090,13 @@ where id = ?
""" % queue """ % queue
self.col.db.executemany(query, data) self.col.db.executemany(query, data)
def _removeFromFiltered(self, card): def _removeFromFiltered(self, card) -> None:
if card.odid: if card.odid:
card.did = card.odid card.did = card.odid
card.odue = 0 card.odue = 0
card.odid = 0 card.odid = 0
def _restorePreviewCard(self, card): def _restorePreviewCard(self, card) -> None:
assert card.odid assert card.odid
card.due = card.odue card.due = card.odue
@ -1113,7 +1114,7 @@ where id = ?
# Leeches # Leeches
########################################################################## ##########################################################################
def _checkLeech(self, card, conf): def _checkLeech(self, card, conf) -> Optional[bool]:
"Leech handler. True if card was a leech." "Leech handler. True if card was a leech."
lf = conf['leechFails'] lf = conf['leechFails']
if not lf: if not lf:
@ -1136,10 +1137,10 @@ where id = ?
# Tools # Tools
########################################################################## ##########################################################################
def _cardConf(self, card): def _cardConf(self, card) -> Any:
return self.col.decks.confForDid(card.did) return self.col.decks.confForDid(card.did)
def _newConf(self, card): def _newConf(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1158,7 +1159,7 @@ where id = ?
perDay=self.reportLimit perDay=self.reportLimit
) )
def _lapseConf(self, card): def _lapseConf(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1176,7 +1177,7 @@ where id = ?
resched=conf['resched'], resched=conf['resched'],
) )
def _revConf(self, card): def _revConf(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1184,20 +1185,20 @@ where id = ?
# dynamic deck # dynamic deck
return self.col.decks.confForDid(card.odid)['rev'] return self.col.decks.confForDid(card.odid)['rev']
def _deckLimit(self): def _deckLimit(self) -> str:
return ids2str(self.col.decks.active()) return ids2str(self.col.decks.active())
def _previewingCard(self, card): def _previewingCard(self, card) -> Any:
conf = self._cardConf(card) conf = self._cardConf(card)
return conf['dyn'] and not conf['resched'] return conf['dyn'] and not conf['resched']
def _previewDelay(self, card): def _previewDelay(self, card) -> Any:
return self._cardConf(card).get("previewDelay", 10)*60 return self._cardConf(card).get("previewDelay", 10)*60
# Daily cutoff # Daily cutoff
########################################################################## ##########################################################################
def _updateCutoff(self): def _updateCutoff(self) -> None:
oldToday = self.today oldToday = self.today
# days since col created # days since col created
self.today = self._daysSinceCreation() self.today = self._daysSinceCreation()
@ -1220,12 +1221,12 @@ where id = ?
self.unburyCards() self.unburyCards()
self.col.conf['lastUnburied'] = self.today self.col.conf['lastUnburied'] = self.today
def _checkDay(self): def _checkDay(self) -> None:
# check if the day has rolled over # check if the day has rolled over
if time.time() > self.dayCutoff: if time.time() > self.dayCutoff:
self.reset() self.reset()
def _dayCutoff(self): def _dayCutoff(self) -> int:
rolloverTime = self.col.conf.get("rollover", 4) rolloverTime = self.col.conf.get("rollover", 4)
if rolloverTime < 0: if rolloverTime < 0:
rolloverTime = 24+rolloverTime rolloverTime = 24+rolloverTime
@ -1237,7 +1238,7 @@ where id = ?
stamp = int(time.mktime(date.timetuple())) stamp = int(time.mktime(date.timetuple()))
return stamp return stamp
def _daysSinceCreation(self): def _daysSinceCreation(self) -> int:
startDate = datetime.datetime.fromtimestamp(self.col.crt) startDate = datetime.datetime.fromtimestamp(self.col.crt)
startDate = startDate.replace(hour=self.col.conf.get("rollover", 4), startDate = startDate.replace(hour=self.col.conf.get("rollover", 4),
minute=0, second=0, microsecond=0) minute=0, second=0, microsecond=0)
@ -1246,12 +1247,12 @@ where id = ?
# Deck finished state # Deck finished state
########################################################################## ##########################################################################
def finishedMsg(self): def finishedMsg(self) -> str:
return ("<b>"+_( return ("<b>"+_(
"Congratulations! You have finished this deck for now.")+ "Congratulations! You have finished this deck for now.")+
"</b><br><br>" + self._nextDueMsg()) "</b><br><br>" + self._nextDueMsg())
def _nextDueMsg(self): def _nextDueMsg(self) -> str:
line = [] line = []
# the new line replacements are so we don't break translations # the new line replacements are so we don't break translations
# in a point release # in a point release
@ -1278,38 +1279,38 @@ Some related or buried cards were delayed until a later session.""")+now)
To study outside of the normal schedule, click the Custom Study button below.""")) To study outside of the normal schedule, click the Custom Study button below."""))
return "<p>".join(line) return "<p>".join(line)
def revDue(self): def revDue(self) -> Any:
"True if there are any rev cards due." "True if there are any rev cards due."
return self.col.db.scalar( return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 2 " ("select 1 from cards where did in %s and queue = 2 "
"and due <= ? limit 1") % self._deckLimit(), "and due <= ? limit 1") % self._deckLimit(),
self.today) self.today)
def newDue(self): def newDue(self) -> Any:
"True if there are any new cards due." "True if there are any new cards due."
return self.col.db.scalar( return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 0 " ("select 1 from cards where did in %s and queue = 0 "
"limit 1") % self._deckLimit()) "limit 1") % self._deckLimit())
def haveBuriedSiblings(self): def haveBuriedSiblings(self) -> bool:
sdids = ids2str(self.col.decks.active()) sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar( cnt = self.col.db.scalar(
"select 1 from cards where queue = -2 and did in %s limit 1" % sdids) "select 1 from cards where queue = -2 and did in %s limit 1" % sdids)
return not not cnt return not not cnt
def haveManuallyBuried(self): def haveManuallyBuried(self) -> bool:
sdids = ids2str(self.col.decks.active()) sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar( cnt = self.col.db.scalar(
"select 1 from cards where queue = -3 and did in %s limit 1" % sdids) "select 1 from cards where queue = -3 and did in %s limit 1" % sdids)
return not not cnt return not not cnt
def haveBuried(self): def haveBuried(self) -> bool:
return self.haveManuallyBuried() or self.haveBuriedSiblings() return self.haveManuallyBuried() or self.haveBuriedSiblings()
# Next time reports # Next time reports
########################################################################## ##########################################################################
def nextIvlStr(self, card, ease, short=False): def nextIvlStr(self, card, ease, short=False) -> Any:
"Return the next interval for CARD as a string." "Return the next interval for CARD as a string."
ivl = self.nextIvl(card, ease) ivl = self.nextIvl(card, ease)
if not ivl: if not ivl:
@ -1319,7 +1320,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
s = "<"+s s = "<"+s
return s return s
def nextIvl(self, card, ease): def nextIvl(self, card, ease) -> Any:
"Return the next interval for CARD, in seconds." "Return the next interval for CARD, in seconds."
# preview mode? # preview mode?
if self._previewingCard(card): if self._previewingCard(card):
@ -1345,7 +1346,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
return self._nextRevIvl(card, ease, fuzz=False)*86400 return self._nextRevIvl(card, ease, fuzz=False)*86400
# this isn't easily extracted from the learn code # this isn't easily extracted from the learn code
def _nextLrnIvl(self, card, ease): def _nextLrnIvl(self, card, ease) -> Any:
if card.queue == 0: if card.queue == 0:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
conf = self._lrnConf(card) conf = self._lrnConf(card)
@ -1377,14 +1378,14 @@ else
end) end)
""" """
def suspendCards(self, ids): def suspendCards(self, ids) -> None:
"Suspend cards." "Suspend cards."
self.col.log(ids) self.col.log(ids)
self.col.db.execute( self.col.db.execute(
"update cards set queue=-1,mod=?,usn=? where id in "+ "update cards set queue=-1,mod=?,usn=? where id in "+
ids2str(ids), intTime(), self.col.usn()) ids2str(ids), intTime(), self.col.usn())
def unsuspendCards(self, ids): def unsuspendCards(self, ids) -> None:
"Unsuspend cards." "Unsuspend cards."
self.col.log(ids) self.col.log(ids)
self.col.db.execute( self.col.db.execute(
@ -1392,27 +1393,27 @@ end)
"where queue = -1 and id in %s") % (self._restoreQueueSnippet, ids2str(ids)), "where queue = -1 and id in %s") % (self._restoreQueueSnippet, ids2str(ids)),
intTime(), self.col.usn()) intTime(), self.col.usn())
def buryCards(self, cids, manual=True): def buryCards(self, cids, manual=True) -> None:
queue = manual and -3 or -2 queue = manual and -3 or -2
self.col.log(cids) self.col.log(cids)
self.col.db.execute(""" self.col.db.execute("""
update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids), update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids),
queue, intTime(), self.col.usn()) queue, intTime(), self.col.usn())
def buryNote(self, nid): def buryNote(self, nid) -> None:
"Bury all cards for note until next session." "Bury all cards for note until next session."
cids = self.col.db.list( cids = self.col.db.list(
"select id from cards where nid = ? and queue >= 0", nid) "select id from cards where nid = ? and queue >= 0", nid)
self.buryCards(cids) self.buryCards(cids)
def unburyCards(self): def unburyCards(self) -> None:
"Unbury all buried cards in all decks." "Unbury all buried cards in all decks."
self.col.log( self.col.log(
self.col.db.list("select id from cards where queue in (-2, -3)")) self.col.db.list("select id from cards where queue in (-2, -3)"))
self.col.db.execute( self.col.db.execute(
"update cards set %s where queue in (-2, -3)" % self._restoreQueueSnippet) "update cards set %s where queue in (-2, -3)" % self._restoreQueueSnippet)
def unburyCardsForDeck(self, type="all"): def unburyCardsForDeck(self, type="all") -> None:
if type == "all": if type == "all":
queue = "queue in (-2, -3)" queue = "queue in (-2, -3)"
elif type == "manual": elif type == "manual":
@ -1433,7 +1434,7 @@ update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids),
# Sibling spacing # Sibling spacing
########################################################################## ##########################################################################
def _burySiblings(self, card): def _burySiblings(self, card) -> None:
toBury = [] toBury = []
nconf = self._newConf(card) nconf = self._newConf(card)
buryNew = nconf.get("bury", True) buryNew = nconf.get("bury", True)
@ -1467,7 +1468,7 @@ and (queue=0 or (queue=2 and due<=?))""",
# Resetting # Resetting
########################################################################## ##########################################################################
def forgetCards(self, ids): def forgetCards(self, ids) -> None:
"Put cards at the end of the new queue." "Put cards at the end of the new queue."
self.remFromDyn(ids) self.remFromDyn(ids)
self.col.db.execute( self.col.db.execute(
@ -1479,7 +1480,7 @@ and (queue=0 or (queue=2 and due<=?))""",
self.sortCards(ids, start=pmax+1) self.sortCards(ids, start=pmax+1)
self.col.log(ids) self.col.log(ids)
def reschedCards(self, ids, imin, imax): def reschedCards(self, ids, imin, imax) -> None:
"Put cards in review queue with a new interval in days (min, max)." "Put cards in review queue with a new interval in days (min, max)."
d = [] d = []
t = self.today t = self.today
@ -1495,7 +1496,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
d) d)
self.col.log(ids) self.col.log(ids)
def resetCards(self, ids): def resetCards(self, ids) -> None:
"Completely reset cards for export." "Completely reset cards for export."
sids = ids2str(ids) sids = ids2str(ids)
# we want to avoid resetting due number of existing new cards on export # we want to avoid resetting due number of existing new cards on export
@ -1514,7 +1515,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
# Repositioning new cards # Repositioning new cards
########################################################################## ##########################################################################
def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False) -> None:
scids = ids2str(cids) scids = ids2str(cids)
now = intTime() now = intTime()
nids = [] nids = []
@ -1554,15 +1555,15 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.col.db.executemany( self.col.db.executemany(
"update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d)
def randomizeCards(self, did): def randomizeCards(self, did) -> None:
cids = self.col.db.list("select id from cards where did = ?", did) cids = self.col.db.list("select id from cards where did = ?", did)
self.sortCards(cids, shuffle=True) self.sortCards(cids, shuffle=True)
def orderCards(self, did): def orderCards(self, did) -> None:
cids = self.col.db.list("select id from cards where did = ? order by id", did) cids = self.col.db.list("select id from cards where did = ? order by id", did)
self.sortCards(cids) self.sortCards(cids)
def resortConf(self, conf): def resortConf(self, conf) -> None:
for did in self.col.decks.didsForConf(conf): for did in self.col.decks.didsForConf(conf):
if conf['new']['order'] == 0: if conf['new']['order'] == 0:
self.randomizeCards(did) self.randomizeCards(did)
@ -1570,7 +1571,7 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.orderCards(did) self.orderCards(did)
# for post-import # for post-import
def maybeRandomizeDeck(self, did=None): def maybeRandomizeDeck(self, did=None) -> None:
if not did: if not did:
did = self.col.decks.selected() did = self.col.decks.selected()
conf = self.col.decks.confForDid(did) conf = self.col.decks.confForDid(did)
@ -1581,7 +1582,7 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
# Changing scheduler versions # Changing scheduler versions
########################################################################## ##########################################################################
def _emptyAllFiltered(self): def _emptyAllFiltered(self) -> None:
self.col.db.execute(""" self.col.db.execute("""
update cards set did = odid, queue = (case update cards set did = odid, queue = (case
when type = 1 then 0 when type = 1 then 0
@ -1593,7 +1594,7 @@ else type end),
due = odue, odue = 0, odid = 0, usn = ? where odid != 0""", due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
self.col.usn()) self.col.usn())
def _removeAllFromLearning(self, schedVer=2): def _removeAllFromLearning(self, schedVer=2) -> None:
# remove review cards from relearning # remove review cards from relearning
if schedVer == 1: if schedVer == 1:
self.col.db.execute(""" self.col.db.execute("""
@ -1612,7 +1613,7 @@ due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
"select id from cards where queue in (1,3)")) "select id from cards where queue in (1,3)"))
# v1 doesn't support buried/suspended (re)learning cards # v1 doesn't support buried/suspended (re)learning cards
def _resetSuspendedLearning(self): def _resetSuspendedLearning(self) -> None:
self.col.db.execute(""" self.col.db.execute("""
update cards set type = (case update cards set type = (case
when type = 1 then 0 when type = 1 then 0
@ -1624,15 +1625,15 @@ mod = %d, usn = %d
where queue < 0""" % (intTime(), self.col.usn())) where queue < 0""" % (intTime(), self.col.usn()))
# no 'manually buried' queue in v1 # no 'manually buried' queue in v1
def _moveManuallyBuried(self): def _moveManuallyBuried(self) -> None:
self.col.db.execute("update cards set queue=-2,mod=%d where queue=-3" % intTime()) self.col.db.execute("update cards set queue=-2,mod=%d where queue=-3" % intTime())
# adding 'hard' in v2 scheduler means old ease entries need shifting # adding 'hard' in v2 scheduler means old ease entries need shifting
# up or down # up or down
def _remapLearningAnswers(self, sql): def _remapLearningAnswers(self, sql) -> None:
self.col.db.execute("update revlog set %s and type in (0,2)" % sql) self.col.db.execute("update revlog set %s and type in (0,2)" % sql)
def moveToV1(self): def moveToV1(self) -> None:
self._emptyAllFiltered() self._emptyAllFiltered()
self._removeAllFromLearning() self._removeAllFromLearning()
@ -1640,7 +1641,7 @@ where queue < 0""" % (intTime(), self.col.usn()))
self._resetSuspendedLearning() self._resetSuspendedLearning()
self._remapLearningAnswers("ease=ease-1 where ease in (3,4)") self._remapLearningAnswers("ease=ease-1 where ease in (3,4)")
def moveToV2(self): def moveToV2(self) -> None:
self._emptyAllFiltered() self._emptyAllFiltered()
self._removeAllFromLearning(schedVer=1) self._removeAllFromLearning(schedVer=1)
self._remapLearningAnswers("ease=ease+1 where ease in (2,3)") self._remapLearningAnswers("ease=ease+1 where ease in (2,3)")

View File

@ -5,7 +5,9 @@
import html import html
import re, sys, threading, time, subprocess, os, atexit import re, sys, threading, time, subprocess, os, atexit
import random import random
from typing import List from typing import List, Tuple, Dict, Any
from typing import Callable, NoReturn, Optional
from anki.hooks import addHook, runHook from anki.hooks import addHook, runHook
from anki.utils import tmpdir, isWin, isMac, isLin from anki.utils import tmpdir, isWin, isMac, isLin
from anki.lang import _ from anki.lang import _
@ -15,19 +17,19 @@ from anki.lang import _
_soundReg = r"\[sound:(.*?)\]" _soundReg = r"\[sound:(.*?)\]"
def playFromText(text): def playFromText(text) -> None:
for match in allSounds(text): for match in allSounds(text):
# filename is html encoded # filename is html encoded
match = html.unescape(match) match = html.unescape(match)
play(match) play(match)
def allSounds(text): def allSounds(text) -> List:
return re.findall(_soundReg, text) return re.findall(_soundReg, text)
def stripSounds(text): def stripSounds(text) -> str:
return re.sub(_soundReg, "", text) return re.sub(_soundReg, "", text)
def hasSound(text): def hasSound(text) -> bool:
return re.search(_soundReg, text) is not None return re.search(_soundReg, text) is not None
# Packaged commands # Packaged commands
@ -35,7 +37,7 @@ def hasSound(text):
# return modified command array that points to bundled command, and return # return modified command array that points to bundled command, and return
# required environment # required environment
def _packagedCmd(cmd): def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
cmd = cmd[:] cmd = cmd[:]
env = os.environ.copy() env = os.environ.copy()
if "LD_LIBRARY_PATH" in env: if "LD_LIBRARY_PATH" in env:
@ -76,7 +78,7 @@ if sys.platform == "win32":
else: else:
si = None si = None
def retryWait(proc): def retryWait(proc) -> Any:
# osx throws interrupted system call errors frequently # osx throws interrupted system call errors frequently
while 1: while 1:
try: try:
@ -89,6 +91,10 @@ def retryWait(proc):
from anki.mpv import MPV, MPVBase from anki.mpv import MPV, MPVBase
_player: Optional[Callable[[Any], Any]]
_queueEraser: Optional[Callable[[], Any]]
_soundReg: str
mpvPath, mpvEnv = _packagedCmd(["mpv"]) mpvPath, mpvEnv = _packagedCmd(["mpv"])
class MpvManager(MPV): class MpvManager(MPV):
@ -101,28 +107,28 @@ class MpvManager(MPV):
"--input-media-keys=no", "--input-media-keys=no",
] ]
def __init__(self): def __init__(self) -> None:
super().__init__(window_id=None, debug=False) super().__init__(window_id=None, debug=False)
def queueFile(self, file): def queueFile(self, file) -> None:
runHook("mpvWillPlay", file) runHook("mpvWillPlay", file)
path = os.path.join(os.getcwd(), file) path = os.path.join(os.getcwd(), file)
self.command("loadfile", path, "append-play") self.command("loadfile", path, "append-play")
def clearQueue(self): def clearQueue(self) -> None:
self.command("stop") self.command("stop")
def togglePause(self): def togglePause(self) -> None:
self.set_property("pause", not self.get_property("pause")) self.set_property("pause", not self.get_property("pause"))
def seekRelative(self, secs): def seekRelative(self, secs) -> None:
self.command("seek", secs, "relative") self.command("seek", secs, "relative")
def on_idle(self): def on_idle(self) -> None:
runHook("mpvIdleHook") runHook("mpvIdleHook")
def setMpvConfigBase(base): def setMpvConfigBase(base) -> None:
mpvConfPath = os.path.join(base, "mpv.conf") mpvConfPath = os.path.join(base, "mpv.conf")
MpvManager.default_argv += [ MpvManager.default_argv += [
"--no-config", "--no-config",
@ -131,14 +137,14 @@ def setMpvConfigBase(base):
mpvManager = None mpvManager = None
def setupMPV(): def setupMPV() -> None:
global mpvManager, _player, _queueEraser global mpvManager, _player, _queueEraser
mpvManager = MpvManager() mpvManager = MpvManager()
_player = mpvManager.queueFile _player = mpvManager.queueFile
_queueEraser = mpvManager.clearQueue _queueEraser = mpvManager.clearQueue
atexit.register(cleanupMPV) atexit.register(cleanupMPV)
def cleanupMPV(): def cleanupMPV() -> None:
global mpvManager, _player, _queueEraser global mpvManager, _player, _queueEraser
if mpvManager: if mpvManager:
mpvManager.close() mpvManager.close()
@ -151,7 +157,7 @@ def cleanupMPV():
# if anki crashes, an old mplayer instance may be left lying around, # if anki crashes, an old mplayer instance may be left lying around,
# which prevents renaming or deleting the profile # which prevents renaming or deleting the profile
def cleanupOldMplayerProcesses(): def cleanupOldMplayerProcesses() -> None:
# pylint: disable=import-error # pylint: disable=import-error
import psutil # pytype: disable=import-error import psutil # pytype: disable=import-error
@ -189,7 +195,7 @@ class MplayerMonitor(threading.Thread):
mplayer = None mplayer = None
deadPlayers: List[subprocess.Popen] = [] deadPlayers: List[subprocess.Popen] = []
def run(self): def run(self) -> NoReturn:
global mplayerClear global mplayerClear
self.mplayer = None self.mplayer = None
self.deadPlayers = [] self.deadPlayers = []
@ -244,7 +250,7 @@ class MplayerMonitor(threading.Thread):
return True return True
self.deadPlayers = [pl for pl in self.deadPlayers if clean(pl)] self.deadPlayers = [pl for pl in self.deadPlayers if clean(pl)]
def kill(self): def kill(self) -> None:
if not self.mplayer: if not self.mplayer:
return return
try: try:
@ -255,7 +261,7 @@ class MplayerMonitor(threading.Thread):
pass pass
self.mplayer = None self.mplayer = None
def startProcess(self): def startProcess(self) -> subprocess.Popen:
try: try:
cmd = mplayerCmd + ["-slave", "-idle"] cmd = mplayerCmd + ["-slave", "-idle"]
cmd, env = _packagedCmd(cmd) cmd, env = _packagedCmd(cmd)
@ -267,7 +273,7 @@ class MplayerMonitor(threading.Thread):
mplayerEvt.clear() mplayerEvt.clear()
raise Exception("Did you install mplayer?") raise Exception("Did you install mplayer?")
def queueMplayer(path): def queueMplayer(path) -> None:
ensureMplayerThreads() ensureMplayerThreads()
if isWin and os.path.exists(path): if isWin and os.path.exists(path):
# mplayer on windows doesn't like the encoding, so we create a # mplayer on windows doesn't like the encoding, so we create a
@ -284,13 +290,13 @@ def queueMplayer(path):
mplayerQueue.append(path) mplayerQueue.append(path)
mplayerEvt.set() mplayerEvt.set()
def clearMplayerQueue(): def clearMplayerQueue() -> None:
global mplayerClear, mplayerQueue global mplayerClear, mplayerQueue
mplayerQueue = [] mplayerQueue = []
mplayerClear = True mplayerClear = True
mplayerEvt.set() mplayerEvt.set()
def ensureMplayerThreads(): def ensureMplayerThreads() -> None:
global mplayerManager global mplayerManager
if not mplayerManager: if not mplayerManager:
mplayerManager = MplayerMonitor() mplayerManager = MplayerMonitor()
@ -302,7 +308,7 @@ def ensureMplayerThreads():
# clean up mplayer on exit # clean up mplayer on exit
atexit.register(stopMplayer) atexit.register(stopMplayer)
def stopMplayer(*args): def stopMplayer(*args) -> None:
if not mplayerManager: if not mplayerManager:
return return
mplayerManager.kill() mplayerManager.kill()
@ -326,7 +332,7 @@ except:
class _Recorder: class _Recorder:
def postprocess(self, encode=True): def postprocess(self, encode=True) -> None:
self.encode = encode self.encode = encode
for c in processingChain: for c in processingChain:
#print c #print c
@ -344,18 +350,18 @@ class _Recorder:
"Error running %s") % "Error running %s") %
" ".join(cmd)) " ".join(cmd))
def cleanup(self): def cleanup(self) -> None:
if os.path.exists(processingSrc): if os.path.exists(processingSrc):
os.unlink(processingSrc) os.unlink(processingSrc)
class PyAudioThreadedRecorder(threading.Thread): class PyAudioThreadedRecorder(threading.Thread):
def __init__(self, startupDelay): def __init__(self, startupDelay) -> None:
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.startupDelay = startupDelay self.startupDelay = startupDelay
self.finish = False self.finish = False
def run(self): def run(self) -> Any:
chunk = 1024 chunk = 1024
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
@ -421,10 +427,10 @@ if not pyaudio:
_player = queueMplayer _player = queueMplayer
_queueEraser = clearMplayerQueue _queueEraser = clearMplayerQueue
def play(path): def play(path) -> None:
_player(path) _player(path)
def clearAudioQueue(): def clearAudioQueue() -> None:
_queueEraser() _queueEraser()
Recorder = PyAudioRecorder Recorder = PyAudioRecorder

View File

@ -8,6 +8,7 @@ import json
from anki.utils import fmtTimeSpan, ids2str from anki.utils import fmtTimeSpan, ids2str
from anki.lang import _, ngettext from anki.lang import _, ngettext
from typing import Any, List, Tuple, Optional
# Card stats # Card stats
@ -15,12 +16,12 @@ from anki.lang import _, ngettext
class CardStats: class CardStats:
def __init__(self, col, card): def __init__(self, col, card) -> None:
self.col = col self.col = col
self.card = card self.card = card
self.txt = "" self.txt = ""
def report(self): def report(self) -> str:
c = self.card c = self.card
# pylint: disable=unnecessary-lambda # pylint: disable=unnecessary-lambda
fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs) fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs)
@ -65,24 +66,24 @@ class CardStats:
self.txt += "</table>" self.txt += "</table>"
return self.txt return self.txt
def addLine(self, k, v): def addLine(self, k, v) -> None:
self.txt += self.makeLine(k, v) self.txt += self.makeLine(k, v)
def makeLine(self, k, v): def makeLine(self, k, v) -> str:
txt = "<tr><td align=left style='padding-right: 3px;'>" txt = "<tr><td align=left style='padding-right: 3px;'>"
txt += "<b>%s</b></td><td>%s</td></tr>" % (k, v) txt += "<b>%s</b></td><td>%s</td></tr>" % (k, v)
return txt return txt
def date(self, tm): def date(self, tm) -> str:
return time.strftime("%Y-%m-%d", time.localtime(tm)) return time.strftime("%Y-%m-%d", time.localtime(tm))
def time(self, tm): def time(self, tm) -> str:
str = "" s = ""
if tm >= 60: if tm >= 60:
str = fmtTimeSpan((tm/60)*60, short=True, point=-1, unit=1) s = fmtTimeSpan((tm/60)*60, short=True, point=-1, unit=1)
if tm%60 != 0 or not str: if tm%60 != 0 or not s:
str += fmtTimeSpan(tm%60, point=2 if not str else -1, short=True) s += fmtTimeSpan(tm%60, point=2 if not s else -1, short=True)
return str return s
# Collection stats # Collection stats
########################################################################## ##########################################################################
@ -101,7 +102,7 @@ colSusp = "#ff0"
class CollectionStats: class CollectionStats:
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self._stats = None self._stats = None
self.type = 0 self.type = 0
@ -110,7 +111,7 @@ class CollectionStats:
self.wholeCollection = False self.wholeCollection = False
# assumes jquery & plot are available in document # assumes jquery & plot are available in document
def report(self, type=0): def report(self, type=0) -> str:
# 0=days, 1=weeks, 2=months # 0=days, 1=weeks, 2=months
self.type = type self.type = type
from .statsbg import bg from .statsbg import bg
@ -126,7 +127,7 @@ class CollectionStats:
txt += self._section(self.footer()) txt += self._section(self.footer())
return "<center>%s</center>" % txt return "<center>%s</center>" % txt
def _section(self, txt): def _section(self, txt) -> str:
return "<div class=section>%s</div>" % txt return "<div class=section>%s</div>" % txt
css = """ css = """
@ -143,7 +144,7 @@ body {background-image: url(data:image/png;base64,%s); }
# Today stats # Today stats
###################################################################### ######################################################################
def todayStats(self): def todayStats(self) -> str:
b = self._title(_("Today")) b = self._title(_("Today"))
# studied today # studied today
lim = self._revlogLimit() lim = self._revlogLimit()
@ -199,13 +200,13 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
# Due and cumulative due # Due and cumulative due
###################################################################### ######################################################################
def get_start_end_chunk(self, by='review'): def get_start_end_chunk(self, by='review') -> Tuple[int, Optional[int], int]:
start = 0 start = 0
if self.type == 0: if self.type == 0:
end, chunk = 31, 1 end, chunk = 31, 1
elif self.type == 1: elif self.type == 1:
end, chunk = 52, 7 end, chunk = 52, 7
elif self.type == 2: else: # self.type == 2:
end = None end = None
if self._deckAge(by) <= 100: if self._deckAge(by) <= 100:
chunk = 1 chunk = 1
@ -215,7 +216,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
chunk = 31 chunk = 31
return start, end, chunk return start, end, chunk
def dueGraph(self): def dueGraph(self) -> str:
start, end, chunk = self.get_start_end_chunk() start, end, chunk = self.get_start_end_chunk()
d = self._due(start, end, chunk) d = self._due(start, end, chunk)
yng = [] yng = []
@ -251,7 +252,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
txt += self._dueInfo(tot, len(totd)*chunk) txt += self._dueInfo(tot, len(totd)*chunk)
return txt return txt
def _dueInfo(self, tot, num): def _dueInfo(self, tot, num) -> str:
i = [] i = []
self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot) self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot)
self._line(i, _("Average"), self._avgDay( self._line(i, _("Average"), self._avgDay(
@ -263,7 +264,7 @@ and due = ?""" % self._limit(), self.col.sched.today+1)
self._line(i, _("Due tomorrow"), tomorrow) self._line(i, _("Due tomorrow"), tomorrow)
return self._lineTbl(i) return self._lineTbl(i)
def _due(self, start=None, end=None, chunk=1): def _due(self, start=None, end=None, chunk=1) -> Any:
lim = "" lim = ""
if start is not None: if start is not None:
lim += " and due-:today >= %d" % start lim += " and due-:today >= %d" % start
@ -283,7 +284,7 @@ group by day order by day""" % (self._limit(), lim),
# Added, reps and time spent # Added, reps and time spent
###################################################################### ######################################################################
def introductionGraph(self): def introductionGraph(self) -> str:
start, days, chunk = self.get_start_end_chunk() start, days, chunk = self.get_start_end_chunk()
data = self._added(days, chunk) data = self._added(days, chunk)
if not data: if not data:
@ -315,7 +316,7 @@ group by day order by day""" % (self._limit(), lim),
return txt return txt
def repsGraphs(self): def repsGraphs(self) -> str:
start, days, chunk = self.get_start_end_chunk() start, days, chunk = self.get_start_end_chunk()
data = self._done(days, chunk) data = self._done(days, chunk)
if not data: if not data:
@ -363,7 +364,7 @@ group by day order by day""" % (self._limit(), lim),
txt2 += rep txt2 += rep
return self._section(txt1) + self._section(txt2) return self._section(txt1) + self._section(txt2)
def _ansInfo(self, totd, studied, first, unit, convHours=False, total=None): def _ansInfo(self, totd, studied, first, unit, convHours=False, total=None) -> Tuple[str, int]:
assert(totd) assert(totd)
tot = totd[-1][1] tot = totd[-1][1]
period = self._periodDays() period = self._periodDays()
@ -404,7 +405,7 @@ group by day order by day""" % (self._limit(), lim),
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=text)) _("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=text))
return self._lineTbl(i), int(tot) return self._lineTbl(i), int(tot)
def _splitRepData(self, data, spec): def _splitRepData(self, data, spec) -> Tuple[List[dict], List[Tuple[Any, Any]]]:
sep = {} sep = {}
totcnt = {} totcnt = {}
totd = {} totd = {}
@ -433,7 +434,7 @@ group by day order by day""" % (self._limit(), lim),
bars={'show': False}, lines=dict(show=True), stack=-n)) bars={'show': False}, lines=dict(show=True), stack=-n))
return (ret, alltot) return (ret, alltot)
def _added(self, num=7, chunk=1): def _added(self, num=7, chunk=1) -> Any:
lims = [] lims = []
if num is not None: if num is not None:
lims.append("id > %d" % ( lims.append("id > %d" % (
@ -454,7 +455,7 @@ count(id)
from cards %s from cards %s
group by day order by day""" % lim, cut=self.col.sched.dayCutoff,tf=tf, chunk=chunk) group by day order by day""" % lim, cut=self.col.sched.dayCutoff,tf=tf, chunk=chunk)
def _done(self, num=7, chunk=1): def _done(self, num=7, chunk=1) -> Any:
lims = [] lims = []
if num is not None: if num is not None:
lims.append("id > %d" % ( lims.append("id > %d" % (
@ -490,7 +491,7 @@ group by day order by day""" % lim,
tf=tf, tf=tf,
chunk=chunk) chunk=chunk)
def _daysStudied(self): def _daysStudied(self) -> Any:
lims = [] lims = []
num = self._periodDays() num = self._periodDays()
if num: if num:
@ -516,7 +517,7 @@ group by day order by day)""" % lim,
# Intervals # Intervals
###################################################################### ######################################################################
def ivlGraph(self): def ivlGraph(self) -> str:
(ivls, all, avg, max_), chunk = self._ivls() (ivls, all, avg, max_), chunk = self._ivls()
tot = 0 tot = 0
totd = [] totd = []
@ -545,7 +546,7 @@ group by day order by day)""" % lim,
self._line(i, _("Longest interval"), fmtTimeSpan(max_*86400)) self._line(i, _("Longest interval"), fmtTimeSpan(max_*86400))
return txt + self._lineTbl(i) return txt + self._lineTbl(i)
def _ivls(self): def _ivls(self) -> Tuple[list, int]:
start, end, chunk = self.get_start_end_chunk() start, end, chunk = self.get_start_end_chunk()
lim = "and grp <= %d" % end if end else "" lim = "and grp <= %d" % end if end else ""
data = [self.col.db.all(""" data = [self.col.db.all("""
@ -560,7 +561,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2""" %
# Eases # Eases
###################################################################### ######################################################################
def easeGraph(self): def easeGraph(self) -> str:
# 3 + 4 + 4 + spaces on sides and middle = 15 # 3 + 4 + 4 + spaces on sides and middle = 15
# yng starts at 1+3+1 = 5 # yng starts at 1+3+1 = 5
# mtr starts at 5+4+1 = 10 # mtr starts at 5+4+1 = 10
@ -591,7 +592,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2""" %
txt += self._easeInfo(eases) txt += self._easeInfo(eases)
return txt return txt
def _easeInfo(self, eases): def _easeInfo(self, eases) -> str:
types = {0: [0, 0], 1: [0, 0], 2: [0,0]} types = {0: [0, 0], 1: [0, 0], 2: [0,0]}
for (type, ease, cnt) in eases: for (type, ease, cnt) in eases:
if ease == 1: if ease == 1:
@ -614,7 +615,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2""" %
"</td><td align=center>".join(i) + "</td><td align=center>".join(i) +
"</td></tr></table></center>") "</td></tr></table></center>")
def _eases(self): def _eases(self) -> Any:
lims = [] lims = []
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
@ -643,7 +644,7 @@ order by thetype, ease""" % (ease4repl, lim))
# Hourly retention # Hourly retention
###################################################################### ######################################################################
def hourGraph(self): def hourGraph(self) -> str:
data = self._hourRet() data = self._hourRet()
if not data: if not data:
return "" return ""
@ -690,7 +691,7 @@ order by thetype, ease""" % (ease4repl, lim))
txt += _("Hours with less than 30 reviews are not shown.") txt += _("Hours with less than 30 reviews are not shown.")
return txt return txt
def _hourRet(self): def _hourRet(self) -> Any:
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
lim = " and " + lim lim = " and " + lim
@ -715,7 +716,7 @@ group by hour having count() > 30 order by hour""" % lim,
# Cards # Cards
###################################################################### ######################################################################
def cardGraph(self): def cardGraph(self) -> str:
# graph data # graph data
div = self._cards() div = self._cards()
d = [] d = []
@ -749,7 +750,7 @@ when you answer "good" on a review.''')
info) info)
return txt return txt
def _line(self, i, a, b, bold=True): def _line(self, i, a, b, bold=True) -> None:
#T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews". #T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews".
colon = _(":") colon = _(":")
if bold: if bold:
@ -757,10 +758,10 @@ when you answer "good" on a review.''')
else: else:
i.append(("<tr><td width=200 align=right>%s%s</td><td>%s</td></tr>") % (a,colon,b)) i.append(("<tr><td width=200 align=right>%s%s</td><td>%s</td></tr>") % (a,colon,b))
def _lineTbl(self, i): def _lineTbl(self, i) -> str:
return "<table width=400>" + "".join(i) + "</table>" return "<table width=400>" + "".join(i) + "</table>"
def _factors(self): def _factors(self) -> Any:
return self.col.db.first(""" return self.col.db.first("""
select select
min(factor) / 10.0, min(factor) / 10.0,
@ -768,7 +769,7 @@ avg(factor) / 10.0,
max(factor) / 10.0 max(factor) / 10.0
from cards where did in %s and queue = 2""" % self._limit()) from cards where did in %s and queue = 2""" % self._limit())
def _cards(self): def _cards(self) -> Any:
return self.col.db.first(""" return self.col.db.first("""
select select
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
@ -780,7 +781,7 @@ from cards where did in %s""" % self._limit())
# Footer # Footer
###################################################################### ######################################################################
def footer(self): def footer(self) -> str:
b = "<br><br><font size=1>" b = "<br><br><font size=1>"
b += _("Generated on %s") % time.asctime(time.localtime(time.time())) b += _("Generated on %s") % time.asctime(time.localtime(time.time()))
b += "<br>" b += "<br>"
@ -801,7 +802,7 @@ from cards where did in %s""" % self._limit())
###################################################################### ######################################################################
def _graph(self, id, data, conf=None, def _graph(self, id, data, conf=None,
type="bars", xunit=1, ylabel=_("Cards"), ylabel2=""): type="bars", xunit=1, ylabel=_("Cards"), ylabel2="") -> str:
if conf is None: if conf is None:
conf = {} conf = {}
# display settings # display settings
@ -902,21 +903,21 @@ $(function () {
ylab=ylabel, ylab2=ylabel2, ylab=ylabel, ylab2=ylabel2,
data=json.dumps(data), conf=json.dumps(conf))) data=json.dumps(data), conf=json.dumps(conf)))
def _limit(self): def _limit(self) -> Any:
if self.wholeCollection: if self.wholeCollection:
return ids2str([d['id'] for d in self.col.decks.all()]) return ids2str([d['id'] for d in self.col.decks.all()])
return self.col.sched._deckLimit() return self.col.sched._deckLimit()
def _revlogLimit(self): def _revlogLimit(self) -> str:
if self.wholeCollection: if self.wholeCollection:
return "" return ""
return ("cid in (select id from cards where did in %s)" % return ("cid in (select id from cards where did in %s)" %
ids2str(self.col.decks.active())) ids2str(self.col.decks.active()))
def _title(self, title, subtitle=""): def _title(self, title, subtitle="") -> str:
return '<h1>%s</h1>%s' % (title, subtitle) return '<h1>%s</h1>%s' % (title, subtitle)
def _deckAge(self, by): def _deckAge(self, by) -> int:
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
lim = " where " + lim lim = " where " + lim
@ -932,13 +933,13 @@ $(function () {
1, int(1+((self.col.sched.dayCutoff - (t/1000)) / 86400))) 1, int(1+((self.col.sched.dayCutoff - (t/1000)) / 86400)))
return period return period
def _periodDays(self): def _periodDays(self) -> Optional[int]:
start, end, chunk = self.get_start_end_chunk() start, end, chunk = self.get_start_end_chunk()
if end is None: if end is None:
return None return None
return end * chunk return end * chunk
def _avgDay(self, tot, num, unit): def _avgDay(self, tot, num, unit) -> str:
vals = [] vals = []
try: try:
vals.append(_("%(a)0.1f %(b)s/day") % dict(a=tot/float(num), b=unit)) vals.append(_("%(a)0.1f %(b)s/day") % dict(a=tot/float(num), b=unit))

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Dict, Any
from anki.lang import _ from anki.lang import _
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
@ -10,7 +11,7 @@ models = []
# Basic # Basic
########################################################################## ##########################################################################
def _newBasicModel(col, name=None): def _newBasicModel(col, name=None) -> Dict[str, Any]:
mm = col.models mm = col.models
m = mm.new(name or _("Basic")) m = mm.new(name or _("Basic"))
fm = mm.newField(_("Front")) fm = mm.newField(_("Front"))
@ -23,7 +24,7 @@ def _newBasicModel(col, name=None):
mm.addTemplate(m, t) mm.addTemplate(m, t)
return m return m
def addBasicModel(col): def addBasicModel(col) -> Dict[str, Any]:
m = _newBasicModel(col) m = _newBasicModel(col)
col.models.add(m) col.models.add(m)
return m return m
@ -33,7 +34,7 @@ models.append((lambda: _("Basic"), addBasicModel))
# Basic w/ typing # Basic w/ typing
########################################################################## ##########################################################################
def addBasicTypingModel(col): def addBasicTypingModel(col) -> Dict[str, Any]:
mm = col.models mm = col.models
m = _newBasicModel(col, _("Basic (type in the answer)")) m = _newBasicModel(col, _("Basic (type in the answer)"))
t = m['tmpls'][0] t = m['tmpls'][0]
@ -47,7 +48,7 @@ models.append((lambda: _("Basic (type in the answer)"), addBasicTypingModel))
# Forward & Reverse # Forward & Reverse
########################################################################## ##########################################################################
def _newForwardReverse(col, name=None): def _newForwardReverse(col, name=None) -> Dict[str, Any]:
mm = col.models mm = col.models
m = _newBasicModel(col, name or _("Basic (and reversed card)")) m = _newBasicModel(col, name or _("Basic (and reversed card)"))
t = mm.newTemplate(_("Card 2")) t = mm.newTemplate(_("Card 2"))
@ -56,7 +57,7 @@ def _newForwardReverse(col, name=None):
mm.addTemplate(m, t) mm.addTemplate(m, t)
return m return m
def addForwardReverse(col): def addForwardReverse(col) -> Dict[str, Any]:
m = _newForwardReverse(col) m = _newForwardReverse(col)
col.models.add(m) col.models.add(m)
return m return m
@ -66,7 +67,7 @@ models.append((lambda: _("Basic (and reversed card)"), addForwardReverse))
# Forward & Optional Reverse # Forward & Optional Reverse
########################################################################## ##########################################################################
def addForwardOptionalReverse(col): def addForwardOptionalReverse(col) -> Dict[str, Any]:
mm = col.models mm = col.models
m = _newForwardReverse(col, _("Basic (optional reversed card)")) m = _newForwardReverse(col, _("Basic (optional reversed card)"))
av = _("Add Reverse") av = _("Add Reverse")
@ -83,7 +84,7 @@ models.append((lambda: _("Basic (optional reversed card)"),
# Cloze # Cloze
########################################################################## ##########################################################################
def addClozeModel(col): def addClozeModel(col) -> Dict[str, Any]:
mm = col.models mm = col.models
m = mm.new(_("Cloze")) m = mm.new(_("Cloze"))
m['type'] = MODEL_CLOZE m['type'] = MODEL_CLOZE

View File

@ -14,8 +14,11 @@ from anki.collection import _Collection
from anki.consts import * from anki.consts import *
from anki.stdmodels import addBasicModel, addClozeModel, addForwardReverse, \ from anki.stdmodels import addBasicModel, addClozeModel, addForwardReverse, \
addForwardOptionalReverse, addBasicTypingModel addForwardOptionalReverse, addBasicTypingModel
from typing import Any, Dict, List, Optional, Tuple, Type, Union
def Collection(path, lock=True, server=False, log=False): _Collection: Type[_Collection]
def Collection(path, lock=True, server=False, log=False) -> _Collection:
"Open a new or existing collection. Path must be unicode." "Open a new or existing collection. Path must be unicode."
assert path.endswith(".anki2") assert path.endswith(".anki2")
path = os.path.abspath(path) path = os.path.abspath(path)
@ -54,7 +57,7 @@ def Collection(path, lock=True, server=False, log=False):
col.lock() col.lock()
return col return col
def _upgradeSchema(db): def _upgradeSchema(db) -> Any:
ver = db.scalar("select ver from col") ver = db.scalar("select ver from col")
if ver == SCHEMA_VERSION: if ver == SCHEMA_VERSION:
return ver return ver
@ -83,7 +86,7 @@ id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data from notes2""")
_updateIndices(db) _updateIndices(db)
return ver return ver
def _upgrade(col, ver): def _upgrade(col, ver) -> None:
if ver < 3: if ver < 3:
# new deck properties # new deck properties
for d in col.decks.all(): for d in col.decks.all():
@ -184,7 +187,7 @@ update cards set left = left + left*1000 where queue = 1""")
col.models.save(m) col.models.save(m)
col.db.execute("update col set ver = 11") col.db.execute("update col set ver = 11")
def _upgradeClozeModel(col, m): def _upgradeClozeModel(col, m) -> None:
m['type'] = MODEL_CLOZE m['type'] = MODEL_CLOZE
# convert first template # convert first template
t = m['tmpls'][0] t = m['tmpls'][0]
@ -205,7 +208,7 @@ def _upgradeClozeModel(col, m):
# Creating a new collection # Creating a new collection
###################################################################### ######################################################################
def _createDB(db): def _createDB(db) -> int:
db.execute("pragma page_size = 4096") db.execute("pragma page_size = 4096")
db.execute("pragma legacy_file_format = 0") db.execute("pragma legacy_file_format = 0")
db.execute("vacuum") db.execute("vacuum")
@ -214,7 +217,7 @@ def _createDB(db):
db.execute("analyze") db.execute("analyze")
return SCHEMA_VERSION return SCHEMA_VERSION
def _addSchema(db, setColConf=True): def _addSchema(db, setColConf=True) -> None:
db.executescript(""" db.executescript("""
create table if not exists col ( create table if not exists col (
id integer primary key, id integer primary key,
@ -291,7 +294,7 @@ values(1,0,0,%(s)s,%(v)s,0,0,0,'','{}','','','{}');
if setColConf: if setColConf:
_addColVars(db, *_getColVars(db)) _addColVars(db, *_getColVars(db))
def _getColVars(db): def _getColVars(db) -> Tuple[Any, Any, Dict[str, Optional[Union[int, str, List[int]]]]]:
import anki.collection import anki.collection
import anki.decks import anki.decks
g = copy.deepcopy(anki.decks.defaultDeck) g = copy.deepcopy(anki.decks.defaultDeck)
@ -303,14 +306,14 @@ def _getColVars(db):
gc['id'] = 1 gc['id'] = 1
return g, gc, anki.collection.defaultConf.copy() return g, gc, anki.collection.defaultConf.copy()
def _addColVars(db, g, gc, c): def _addColVars(db, g, gc, c) -> None:
db.execute(""" db.execute("""
update col set conf = ?, decks = ?, dconf = ?""", update col set conf = ?, decks = ?, dconf = ?""",
json.dumps(c), json.dumps(c),
json.dumps({'1': g}), json.dumps({'1': g}),
json.dumps({'1': gc})) json.dumps({'1': gc}))
def _updateIndices(db): def _updateIndices(db) -> None:
"Add indices to the DB." "Add indices to the DB."
db.executescript(""" db.executescript("""
-- syncing -- syncing

View File

@ -16,6 +16,7 @@ from anki.utils import versionWithBuild
from .hooks import runHook from .hooks import runHook
import anki import anki
from .lang import ngettext from .lang import ngettext
from typing import Any, Dict, List, Optional, Tuple, Union
# syncing vars # syncing vars
HTTP_TIMEOUT = 90 HTTP_TIMEOUT = 90
@ -30,7 +31,7 @@ class UnexpectedSchemaChange(Exception):
class Syncer: class Syncer:
def __init__(self, col, server=None): def __init__(self, col, server=None) -> None:
self.col = col self.col = col
self.server = server self.server = server
@ -39,7 +40,7 @@ class Syncer:
self.maxUsn = 0 self.maxUsn = 0
self.tablesLeft = [] self.tablesLeft = []
def sync(self): def sync(self) -> str:
"Returns 'noChanges', 'fullSync', 'success', etc" "Returns 'noChanges', 'fullSync', 'success', etc"
self.syncMsg = "" self.syncMsg = ""
self.uname = "" self.uname = ""
@ -138,14 +139,14 @@ class Syncer:
self.finish(mod) self.finish(mod)
return "success" return "success"
def _forceFullSync(self): def _forceFullSync(self) -> str:
# roll back and force full sync # roll back and force full sync
self.col.rollback() self.col.rollback()
self.col.modSchema(False) self.col.modSchema(False)
self.col.save() self.col.save()
return "sanityCheckFailed" return "sanityCheckFailed"
def _gravesChunk(self, graves): def _gravesChunk(self, graves: Dict) -> Tuple[Dict, Optional[Dict]]:
lim = 250 lim = 250
chunk = dict(notes=[], cards=[], decks=[]) chunk = dict(notes=[], cards=[], decks=[])
for cat in "notes", "cards", "decks": for cat in "notes", "cards", "decks":
@ -159,7 +160,7 @@ class Syncer:
return chunk, graves return chunk, graves
return chunk, None return chunk, None
def meta(self): def meta(self) -> dict:
return dict( return dict(
mod=self.col.mod, mod=self.col.mod,
scm=self.col.scm, scm=self.col.scm,
@ -170,7 +171,7 @@ class Syncer:
cont=True cont=True
) )
def changes(self): def changes(self) -> dict:
"Bundle up small objects." "Bundle up small objects."
d = dict(models=self.getModels(), d = dict(models=self.getModels(),
decks=self.getDecks(), decks=self.getDecks(),
@ -180,7 +181,7 @@ class Syncer:
d['crt'] = self.col.crt d['crt'] = self.col.crt
return d return d
def mergeChanges(self, lchg, rchg): def mergeChanges(self, lchg, rchg) -> None:
# then the other objects # then the other objects
self.mergeModels(rchg['models']) self.mergeModels(rchg['models'])
self.mergeDecks(rchg['decks']) self.mergeDecks(rchg['decks'])
@ -192,7 +193,7 @@ class Syncer:
self.col.crt = rchg['crt'] self.col.crt = rchg['crt']
self.prepareToChunk() self.prepareToChunk()
def sanityCheck(self): def sanityCheck(self) -> Union[list, str]:
if not self.col.basicCheck(): if not self.col.basicCheck():
return "failed basic check" return "failed basic check"
for t in "cards", "notes", "revlog", "graves": for t in "cards", "notes", "revlog", "graves":
@ -226,10 +227,10 @@ class Syncer:
len(self.col.decks.allConf()), len(self.col.decks.allConf()),
] ]
def usnLim(self): def usnLim(self) -> str:
return "usn = -1" return "usn = -1"
def finish(self, mod=None): def finish(self, mod: int) -> int:
self.col.ls = mod self.col.ls = mod
self.col._usn = self.maxUsn + 1 self.col._usn = self.maxUsn + 1
# ensure we save the mod time even if no changes made # ensure we save the mod time even if no changes made
@ -240,11 +241,11 @@ class Syncer:
# Chunked syncing # Chunked syncing
########################################################################## ##########################################################################
def prepareToChunk(self): def prepareToChunk(self) -> None:
self.tablesLeft = ["revlog", "cards", "notes"] self.tablesLeft = ["revlog", "cards", "notes"]
self.cursor = None self.cursor = None
def cursorForTable(self, table): def cursorForTable(self, table) -> Any:
lim = self.usnLim() lim = self.usnLim()
x = self.col.db.execute x = self.col.db.execute
d = (self.maxUsn, lim) d = (self.maxUsn, lim)
@ -261,7 +262,7 @@ lapses, left, odue, odid, flags, data from cards where %s""" % d)
select id, guid, mid, mod, %d, tags, flds, '', '', flags, data select id, guid, mid, mod, %d, tags, flds, '', '', flags, data
from notes where %s""" % d) from notes where %s""" % d)
def chunk(self): def chunk(self) -> dict:
buf = dict(done=False) buf = dict(done=False)
lim = 250 lim = 250
while self.tablesLeft and lim: while self.tablesLeft and lim:
@ -284,7 +285,7 @@ from notes where %s""" % d)
buf['done'] = True buf['done'] = True
return buf return buf
def applyChunk(self, chunk): def applyChunk(self, chunk) -> None:
if "revlog" in chunk: if "revlog" in chunk:
self.mergeRevlog(chunk['revlog']) self.mergeRevlog(chunk['revlog'])
if "cards" in chunk: if "cards" in chunk:
@ -295,7 +296,7 @@ from notes where %s""" % d)
# Deletions # Deletions
########################################################################## ##########################################################################
def removed(self): def removed(self) -> dict:
cards = [] cards = []
notes = [] notes = []
decks = [] decks = []
@ -316,7 +317,7 @@ from notes where %s""" % d)
return dict(cards=cards, notes=notes, decks=decks) return dict(cards=cards, notes=notes, decks=decks)
def remove(self, graves): def remove(self, graves) -> None:
# pretend to be the server so we don't set usn = -1 # pretend to be the server so we don't set usn = -1
self.col.server = True self.col.server = True
@ -333,14 +334,14 @@ from notes where %s""" % d)
# Models # Models
########################################################################## ##########################################################################
def getModels(self): def getModels(self) -> List:
mods = [m for m in self.col.models.all() if m['usn'] == -1] mods = [m for m in self.col.models.all() if m['usn'] == -1]
for m in mods: for m in mods:
m['usn'] = self.maxUsn m['usn'] = self.maxUsn
self.col.models.save() self.col.models.save()
return mods return mods
def mergeModels(self, rchg): def mergeModels(self, rchg) -> None:
for r in rchg: for r in rchg:
l = self.col.models.get(r['id']) l = self.col.models.get(r['id'])
# if missing locally or server is newer, update # if missing locally or server is newer, update
@ -358,7 +359,7 @@ from notes where %s""" % d)
# Decks # Decks
########################################################################## ##########################################################################
def getDecks(self): def getDecks(self) -> List[list]:
decks = [g for g in self.col.decks.all() if g['usn'] == -1] decks = [g for g in self.col.decks.all() if g['usn'] == -1]
for g in decks: for g in decks:
g['usn'] = self.maxUsn g['usn'] = self.maxUsn
@ -368,7 +369,7 @@ from notes where %s""" % d)
self.col.decks.save() self.col.decks.save()
return [decks, dconf] return [decks, dconf]
def mergeDecks(self, rchg): def mergeDecks(self, rchg) -> None:
for r in rchg[0]: for r in rchg[0]:
l = self.col.decks.get(r['id'], False) l = self.col.decks.get(r['id'], False)
# work around mod time being stored as string # work around mod time being stored as string
@ -390,7 +391,7 @@ from notes where %s""" % d)
# Tags # Tags
########################################################################## ##########################################################################
def getTags(self): def getTags(self) -> List:
tags = [] tags = []
for t, usn in self.col.tags.allItems(): for t, usn in self.col.tags.allItems():
if usn == -1: if usn == -1:
@ -399,18 +400,18 @@ from notes where %s""" % d)
self.col.tags.save() self.col.tags.save()
return tags return tags
def mergeTags(self, tags): def mergeTags(self, tags) -> None:
self.col.tags.register(tags, usn=self.maxUsn) self.col.tags.register(tags, usn=self.maxUsn)
# Cards/notes/revlog # Cards/notes/revlog
########################################################################## ##########################################################################
def mergeRevlog(self, logs): def mergeRevlog(self, logs) -> None:
self.col.db.executemany( self.col.db.executemany(
"insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)", "insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)",
logs) logs)
def newerRows(self, data, table, modIdx): def newerRows(self, data, table, modIdx) -> List:
ids = (r[0] for r in data) ids = (r[0] for r in data)
lmods = {} lmods = {}
for id, mod in self.col.db.execute( for id, mod in self.col.db.execute(
@ -424,13 +425,13 @@ from notes where %s""" % d)
self.col.log(table, data) self.col.log(table, data)
return update return update
def mergeCards(self, cards): def mergeCards(self, cards) -> None:
self.col.db.executemany( self.col.db.executemany(
"insert or replace into cards values " "insert or replace into cards values "
"(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
self.newerRows(cards, "cards", 4)) self.newerRows(cards, "cards", 4))
def mergeNotes(self, notes): def mergeNotes(self, notes) -> None:
rows = self.newerRows(notes, "notes", 3) rows = self.newerRows(notes, "notes", 3)
self.col.db.executemany( self.col.db.executemany(
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
@ -440,10 +441,10 @@ from notes where %s""" % d)
# Col config # Col config
########################################################################## ##########################################################################
def getConf(self): def getConf(self) -> Any:
return self.col.conf return self.col.conf
def mergeConf(self, conf): def mergeConf(self, conf) -> None:
self.col.conf = conf self.col.conf = conf
# Wrapper for requests that tracks upload/download progress # Wrapper for requests that tracks upload/download progress
@ -454,22 +455,22 @@ class AnkiRequestsClient:
verify = True verify = True
timeout = 60 timeout = 60
def __init__(self): def __init__(self) -> None:
self.session = requests.Session() self.session = requests.Session()
def post(self, url, data, headers): def post(self, url, data, headers) -> Any:
data = _MonitoringFile(data) # pytype: disable=wrong-arg-types data = _MonitoringFile(data) # pytype: disable=wrong-arg-types
headers['User-Agent'] = self._agentName() headers['User-Agent'] = self._agentName()
return self.session.post( return self.session.post(
url, data=data, headers=headers, stream=True, timeout=self.timeout, verify=self.verify) # pytype: disable=wrong-arg-types url, data=data, headers=headers, stream=True, timeout=self.timeout, verify=self.verify) # pytype: disable=wrong-arg-types
def get(self, url, headers=None): def get(self, url, headers=None) -> requests.models.Response:
if headers is None: if headers is None:
headers = {} headers = {}
headers['User-Agent'] = self._agentName() headers['User-Agent'] = self._agentName()
return self.session.get(url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify) return self.session.get(url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify)
def streamContent(self, resp): def streamContent(self, resp) -> bytes:
resp.raise_for_status() resp.raise_for_status()
buf = io.BytesIO() buf = io.BytesIO()
@ -478,7 +479,7 @@ class AnkiRequestsClient:
buf.write(chunk) buf.write(chunk)
return buf.getvalue() return buf.getvalue()
def _agentName(self): def _agentName(self) -> str:
from anki import version from anki import version
return "Anki {}".format(version) return "Anki {}".format(version)
@ -490,7 +491,7 @@ if os.environ.get("ANKI_NOVERIFYSSL"):
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
class _MonitoringFile(io.BufferedReader): class _MonitoringFile(io.BufferedReader):
def read(self, size=-1): def read(self, size=-1) -> bytes:
data = io.BufferedReader.read(self, HTTP_BUF_SIZE) data = io.BufferedReader.read(self, HTTP_BUF_SIZE)
runHook("httpSend", len(data)) runHook("httpSend", len(data))
return data return data
@ -500,7 +501,7 @@ class _MonitoringFile(io.BufferedReader):
class HttpSyncer: class HttpSyncer:
def __init__(self, hkey=None, client=None, hostNum=None): def __init__(self, hkey=None, client=None, hostNum=None) -> None:
self.hkey = hkey self.hkey = hkey
self.skey = checksum(str(random.random()))[:8] self.skey = checksum(str(random.random()))[:8]
self.client = client or AnkiRequestsClient() self.client = client or AnkiRequestsClient()
@ -508,14 +509,14 @@ class HttpSyncer:
self.hostNum = hostNum self.hostNum = hostNum
self.prefix = "sync/" self.prefix = "sync/"
def syncURL(self): def syncURL(self) -> str:
if devMode: if devMode:
url = "https://l1sync.ankiweb.net/" url = "https://l1sync.ankiweb.net/"
else: else:
url = SYNC_BASE % (self.hostNum or "") url = SYNC_BASE % (self.hostNum or "")
return url + self.prefix return url + self.prefix
def assertOk(self, resp): def assertOk(self, resp) -> None:
# not using raise_for_status() as aqt expects this error msg # not using raise_for_status() as aqt expects this error msg
if resp.status_code != 200: if resp.status_code != 200:
raise Exception("Unknown response code: %s" % resp.status_code) raise Exception("Unknown response code: %s" % resp.status_code)
@ -526,7 +527,7 @@ class HttpSyncer:
# costly. We could send it as a raw post, but more HTTP clients seem to # costly. We could send it as a raw post, but more HTTP clients seem to
# support file uploading, so this is the more compatible choice. # support file uploading, so this is the more compatible choice.
def _buildPostData(self, fobj, comp): def _buildPostData(self, fobj, comp) -> Tuple[Dict[str, str], io.BytesIO]:
BOUNDARY=b"Anki-sync-boundary" BOUNDARY=b"Anki-sync-boundary"
bdry = b"--"+BOUNDARY bdry = b"--"+BOUNDARY
buf = io.BytesIO() buf = io.BytesIO()
@ -573,7 +574,7 @@ Content-Type: application/octet-stream\r\n\r\n""")
return headers, buf return headers, buf
def req(self, method, fobj=None, comp=6, badAuthRaises=True): def req(self, method, fobj=None, comp=6, badAuthRaises=True) -> Any:
headers, body = self._buildPostData(fobj, comp) headers, body = self._buildPostData(fobj, comp)
r = self.client.post(self.syncURL()+method, data=body, headers=headers) r = self.client.post(self.syncURL()+method, data=body, headers=headers)
@ -589,10 +590,10 @@ Content-Type: application/octet-stream\r\n\r\n""")
class RemoteServer(HttpSyncer): class RemoteServer(HttpSyncer):
def __init__(self, hkey, hostNum): def __init__(self, hkey, hostNum) -> None:
HttpSyncer.__init__(self, hkey, hostNum=hostNum) HttpSyncer.__init__(self, hkey, hostNum=hostNum)
def hostKey(self, user, pw): def hostKey(self, user, pw) -> Any:
"Returns hkey or none if user/pw incorrect." "Returns hkey or none if user/pw incorrect."
self.postVars = dict() self.postVars = dict()
ret = self.req( ret = self.req(
@ -604,7 +605,7 @@ class RemoteServer(HttpSyncer):
self.hkey = json.loads(ret.decode("utf8"))['key'] self.hkey = json.loads(ret.decode("utf8"))['key']
return self.hkey return self.hkey
def meta(self): def meta(self) -> Any:
self.postVars = dict( self.postVars = dict(
k=self.hkey, k=self.hkey,
s=self.skey, s=self.skey,
@ -618,31 +619,31 @@ class RemoteServer(HttpSyncer):
return return
return json.loads(ret.decode("utf8")) return json.loads(ret.decode("utf8"))
def applyGraves(self, **kw): def applyGraves(self, **kw) -> Any:
return self._run("applyGraves", kw) return self._run("applyGraves", kw)
def applyChanges(self, **kw): def applyChanges(self, **kw) -> Any:
return self._run("applyChanges", kw) return self._run("applyChanges", kw)
def start(self, **kw): def start(self, **kw) -> Any:
return self._run("start", kw) return self._run("start", kw)
def chunk(self, **kw): def chunk(self, **kw) -> Any:
return self._run("chunk", kw) return self._run("chunk", kw)
def applyChunk(self, **kw): def applyChunk(self, **kw) -> Any:
return self._run("applyChunk", kw) return self._run("applyChunk", kw)
def sanityCheck2(self, **kw): def sanityCheck2(self, **kw) -> Any:
return self._run("sanityCheck2", kw) return self._run("sanityCheck2", kw)
def finish(self, **kw): def finish(self, **kw) -> Any:
return self._run("finish", kw) return self._run("finish", kw)
def abort(self, **kw): def abort(self, **kw) -> Any:
return self._run("abort", kw) return self._run("abort", kw)
def _run(self, cmd, data): def _run(self, cmd, data) -> Any:
return json.loads( return json.loads(
self.req(cmd, io.BytesIO(json.dumps(data).encode("utf8"))).decode("utf8")) self.req(cmd, io.BytesIO(json.dumps(data).encode("utf8"))).decode("utf8"))
@ -651,7 +652,7 @@ class RemoteServer(HttpSyncer):
class FullSyncer(HttpSyncer): class FullSyncer(HttpSyncer):
def __init__(self, col, hkey, client, hostNum): def __init__(self, col, hkey, client, hostNum) -> None:
HttpSyncer.__init__(self, hkey, client, hostNum=hostNum) HttpSyncer.__init__(self, hkey, client, hostNum=hostNum)
self.postVars = dict( self.postVars = dict(
k=self.hkey, k=self.hkey,
@ -659,7 +660,7 @@ class FullSyncer(HttpSyncer):
) )
self.col = col self.col = col
def download(self): def download(self) -> Optional[str]:
runHook("sync", "download") runHook("sync", "download")
localNotEmpty = self.col.db.scalar("select 1 from cards") localNotEmpty = self.col.db.scalar("select 1 from cards")
self.col.close() self.col.close()
@ -683,7 +684,7 @@ class FullSyncer(HttpSyncer):
os.rename(tpath, self.col.path) os.rename(tpath, self.col.path)
self.col = None self.col = None
def upload(self): def upload(self) -> bool:
"True if upload successful." "True if upload successful."
runHook("sync", "upload") runHook("sync", "upload")
# make sure it's ok before we try to upload # make sure it's ok before we try to upload
@ -709,12 +710,12 @@ class FullSyncer(HttpSyncer):
class MediaSyncer: class MediaSyncer:
def __init__(self, col, server=None): def __init__(self, col, server=None) -> None:
self.col = col self.col = col
self.server = server self.server = server
self.downloadCount = 0 self.downloadCount = 0
def sync(self): def sync(self) -> Any:
# check if there have been any changes # check if there have been any changes
runHook("sync", "findMedia") runHook("sync", "findMedia")
self.col.log("findChanges") self.col.log("findChanges")
@ -824,7 +825,7 @@ class MediaSyncer:
self.col.media.forceResync() self.col.media.forceResync()
return ret return ret
def _downloadFiles(self, fnames): def _downloadFiles(self, fnames) -> None:
self.col.log("%d files to fetch"%len(fnames)) self.col.log("%d files to fetch"%len(fnames))
while fnames: while fnames:
top = fnames[0:SYNC_ZIP_COUNT] top = fnames[0:SYNC_ZIP_COUNT]
@ -845,12 +846,12 @@ class MediaSyncer:
class RemoteMediaServer(HttpSyncer): class RemoteMediaServer(HttpSyncer):
def __init__(self, col, hkey, client, hostNum): def __init__(self, col, hkey, client, hostNum) -> None:
self.col = col self.col = col
HttpSyncer.__init__(self, hkey, client, hostNum=hostNum) HttpSyncer.__init__(self, hkey, client, hostNum=hostNum)
self.prefix = "msync/" self.prefix = "msync/"
def begin(self): def begin(self) -> Any:
self.postVars = dict( self.postVars = dict(
k=self.hkey, k=self.hkey,
v="ankidesktop,%s,%s"%(anki.version, platDesc()) v="ankidesktop,%s,%s"%(anki.version, platDesc())
@ -861,7 +862,7 @@ class RemoteMediaServer(HttpSyncer):
return ret return ret
# args: lastUsn # args: lastUsn
def mediaChanges(self, **kw): def mediaChanges(self, **kw) -> Any:
self.postVars = dict( self.postVars = dict(
sk=self.skey, sk=self.skey,
) )
@ -869,20 +870,20 @@ class RemoteMediaServer(HttpSyncer):
self.req("mediaChanges", io.BytesIO(json.dumps(kw).encode("utf8")))) self.req("mediaChanges", io.BytesIO(json.dumps(kw).encode("utf8"))))
# args: files # args: files
def downloadFiles(self, **kw): def downloadFiles(self, **kw) -> Any:
return self.req("downloadFiles", io.BytesIO(json.dumps(kw).encode("utf8"))) return self.req("downloadFiles", io.BytesIO(json.dumps(kw).encode("utf8")))
def uploadChanges(self, zip): def uploadChanges(self, zip) -> Any:
# no compression, as we compress the zip file instead # no compression, as we compress the zip file instead
return self._dataOnly( return self._dataOnly(
self.req("uploadChanges", io.BytesIO(zip), comp=0)) self.req("uploadChanges", io.BytesIO(zip), comp=0))
# args: local # args: local
def mediaSanity(self, **kw): def mediaSanity(self, **kw) -> Any:
return self._dataOnly( return self._dataOnly(
self.req("mediaSanity", io.BytesIO(json.dumps(kw).encode("utf8")))) self.req("mediaSanity", io.BytesIO(json.dumps(kw).encode("utf8"))))
def _dataOnly(self, resp): def _dataOnly(self, resp) -> Any:
resp = json.loads(resp.decode("utf8")) resp = json.loads(resp.decode("utf8"))
if resp['err']: if resp['err']:
self.col.log("error returned:%s"%resp['err']) self.col.log("error returned:%s"%resp['err'])
@ -890,7 +891,7 @@ class RemoteMediaServer(HttpSyncer):
return resp['data'] return resp['data']
# only for unit tests # only for unit tests
def mediatest(self, cmd): def mediatest(self, cmd) -> Any:
self.postVars = dict( self.postVars = dict(
k=self.hkey, k=self.hkey,
) )

View File

@ -14,21 +14,22 @@ import json
from anki.utils import intTime, ids2str from anki.utils import intTime, ids2str
from anki.hooks import runHook from anki.hooks import runHook
import re import re
from typing import Any, List, Tuple
class TagManager: class TagManager:
# Registry save/load # Registry save/load
############################################################# #############################################################
def __init__(self, col): def __init__(self, col) -> None:
self.col = col self.col = col
self.tags = {} self.tags = {}
def load(self, json_): def load(self, json_) -> None:
self.tags = json.loads(json_) self.tags = json.loads(json_)
self.changed = False self.changed = False
def flush(self): def flush(self) -> None:
if self.changed: if self.changed:
self.col.db.execute("update col set tags=?", self.col.db.execute("update col set tags=?",
json.dumps(self.tags)) json.dumps(self.tags))
@ -37,7 +38,7 @@ class TagManager:
# Registering and fetching tags # Registering and fetching tags
############################################################# #############################################################
def register(self, tags, usn=None): def register(self, tags, usn=None) -> None:
"Given a list of tags, add any missing ones to tag registry." "Given a list of tags, add any missing ones to tag registry."
found = False found = False
for t in tags: for t in tags:
@ -48,10 +49,10 @@ class TagManager:
if found: if found:
runHook("newTag") runHook("newTag")
def all(self): def all(self) -> List:
return list(self.tags.keys()) return list(self.tags.keys())
def registerNotes(self, nids=None): def registerNotes(self, nids=None) -> None:
"Add any missing tags from notes to the tags list." "Add any missing tags from notes to the tags list."
# when called without an argument, the old list is cleared first. # when called without an argument, the old list is cleared first.
if nids: if nids:
@ -63,13 +64,13 @@ class TagManager:
self.register(set(self.split( self.register(set(self.split(
" ".join(self.col.db.list("select distinct tags from notes"+lim))))) " ".join(self.col.db.list("select distinct tags from notes"+lim)))))
def allItems(self): def allItems(self) -> List[Tuple[Any, Any]]:
return list(self.tags.items()) return list(self.tags.items())
def save(self): def save(self) -> None:
self.changed = True self.changed = True
def byDeck(self, did, children=False): def byDeck(self, did, children=False) -> List:
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id" basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
if not children: if not children:
query = basequery + " AND c.did=?" query = basequery + " AND c.did=?"
@ -85,7 +86,7 @@ class TagManager:
# Bulk addition/removal from notes # Bulk addition/removal from notes
############################################################# #############################################################
def bulkAdd(self, ids, tags, add=True): def bulkAdd(self, ids, tags, add=True) -> None:
"Add tags in bulk. TAGS is space-separated." "Add tags in bulk. TAGS is space-separated."
newTags = self.split(tags) newTags = self.split(tags)
if not newTags: if not newTags:
@ -117,23 +118,23 @@ class TagManager:
"update notes set tags=:t,mod=:n,usn=:u where id = :id", "update notes set tags=:t,mod=:n,usn=:u where id = :id",
[fix(row) for row in res]) [fix(row) for row in res])
def bulkRem(self, ids, tags): def bulkRem(self, ids, tags) -> None:
self.bulkAdd(ids, tags, False) self.bulkAdd(ids, tags, False)
# String-based utilities # String-based utilities
########################################################################## ##########################################################################
def split(self, tags): def split(self, tags) -> List:
"Parse a string and return a list of tags." "Parse a string and return a list of tags."
return [t for t in tags.replace('\u3000', ' ').split(" ") if t] return [t for t in tags.replace('\u3000', ' ').split(" ") if t]
def join(self, tags): def join(self, tags) -> str:
"Join tags into a single string, with leading and trailing spaces." "Join tags into a single string, with leading and trailing spaces."
if not tags: if not tags:
return "" return ""
return " %s " % " ".join(tags) return " %s " % " ".join(tags)
def addToStr(self, addtags, tags): def addToStr(self, addtags, tags) -> str:
"Add tags if they don't exist, and canonify." "Add tags if they don't exist, and canonify."
currentTags = self.split(tags) currentTags = self.split(tags)
for tag in self.split(addtags): for tag in self.split(addtags):
@ -141,7 +142,7 @@ class TagManager:
currentTags.append(tag) currentTags.append(tag)
return self.join(self.canonify(currentTags)) return self.join(self.canonify(currentTags))
def remFromStr(self, deltags, tags): def remFromStr(self, deltags, tags) -> str:
"Delete tags if they exist." "Delete tags if they exist."
def wildcard(pat, str): def wildcard(pat, str):
pat = re.escape(pat).replace('\\*', '.*') pat = re.escape(pat).replace('\\*', '.*')
@ -161,7 +162,7 @@ class TagManager:
# List-based utilities # List-based utilities
########################################################################## ##########################################################################
def canonify(self, tagList): def canonify(self, tagList) -> List:
"Strip duplicates, adjust case to match existing tags, and sort." "Strip duplicates, adjust case to match existing tags, and sort."
strippedTags = [] strippedTags = []
for t in tagList: for t in tagList:
@ -172,14 +173,14 @@ class TagManager:
strippedTags.append(s) strippedTags.append(s)
return sorted(set(strippedTags)) return sorted(set(strippedTags))
def inList(self, tag, tags): def inList(self, tag, tags) -> bool:
"True if TAG is in TAGS. Ignore case." "True if TAG is in TAGS. Ignore case."
return tag.lower() in [t.lower() for t in tags] return tag.lower() in [t.lower() for t in tags]
# Sync handling # Sync handling
########################################################################## ##########################################################################
def beforeUpload(self): def beforeUpload(self) -> None:
for k in list(self.tags.keys()): for k in list(self.tags.keys()):
self.tags[k] = 0 self.tags[k] = 0
self.save() self.save()

View File

@ -1,8 +1,9 @@
from .template import Template from .template import Template
from . import furigana; furigana.install() from . import furigana; furigana.install()
from . import hint; hint.install() from . import hint; hint.install()
from typing import Any
def render(template, context=None, **kwargs): def render(template, context=None, **kwargs) -> Any:
context = context and context.copy() or {} context = context and context.copy() or {}
context.update(kwargs) context.update(kwargs)
return Template(template, context).render() return Template(template, context).render()

View File

@ -5,11 +5,12 @@
import re import re
from anki.hooks import addHook from anki.hooks import addHook
from typing import Any, Callable
r = r' ?([^ >]+?)\[(.+?)\]' r = r' ?([^ >]+?)\[(.+?)\]'
ruby = r'<ruby><rb>\1</rb><rt>\2</rt></ruby>' ruby = r'<ruby><rb>\1</rb><rt>\2</rt></ruby>'
def noSound(repl): def noSound(repl) -> Callable[[Any], Any]:
def func(match): def func(match):
if match.group(2).startswith("sound:"): if match.group(2).startswith("sound:"):
# return without modification # return without modification
@ -18,19 +19,19 @@ def noSound(repl):
return re.sub(r, repl, match.group(0)) return re.sub(r, repl, match.group(0))
return func return func
def _munge(s): def _munge(s) -> Any:
return s.replace("&nbsp;", " ") return s.replace("&nbsp;", " ")
def kanji(txt, *args): def kanji(txt, *args) -> str:
return re.sub(r, noSound(r'\1'), _munge(txt)) return re.sub(r, noSound(r'\1'), _munge(txt))
def kana(txt, *args): def kana(txt, *args) -> str:
return re.sub(r, noSound(r'\2'), _munge(txt)) return re.sub(r, noSound(r'\2'), _munge(txt))
def furigana(txt, *args): def furigana(txt, *args) -> str:
return re.sub(r, noSound(ruby), _munge(txt)) return re.sub(r, noSound(ruby), _munge(txt))
def install(): def install() -> None:
addHook('fmod_kanji', kanji) addHook('fmod_kanji', kanji)
addHook('fmod_kana', kana) addHook('fmod_kana', kana)
addHook('fmod_furigana', furigana) addHook('fmod_furigana', furigana)

View File

@ -5,7 +5,7 @@
from anki.hooks import addHook from anki.hooks import addHook
from anki.lang import _ from anki.lang import _
def hint(txt, extra, context, tag, fullname): def hint(txt, extra, context, tag, fullname) -> str:
if not txt.strip(): if not txt.strip():
return "" return ""
# random id # random id
@ -16,5 +16,5 @@ onclick="this.style.display='none';document.getElementById('%s').style.display='
%s</a><div id="%s" class=hint style="display: none">%s</div> %s</a><div id="%s" class=hint style="display: none">%s</div>
""" % (domid, _("Show %s") % tag, domid, txt) """ % (domid, _("Show %s") % tag, domid, txt)
def install(): def install() -> None:
addHook('fmod_hint', hint) addHook('fmod_hint', hint)

View File

@ -1,11 +1,12 @@
import re import re
from anki.utils import stripHTML, stripHTMLMedia from anki.utils import stripHTML, stripHTMLMedia
from anki.hooks import runFilter from anki.hooks import runFilter
from typing import Any, Callable, NoReturn, Optional
clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}" clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}"
modifiers = {} modifiers = {}
def modifier(symbol): def modifier(symbol) -> Callable[[Any], Any]:
"""Decorator for associating a function with a Mustache tag modifier. """Decorator for associating a function with a Mustache tag modifier.
@modifier('P') @modifier('P')
@ -20,7 +21,7 @@ def modifier(symbol):
return set_modifier return set_modifier
def get_or_attr(obj, name, default=None): def get_or_attr(obj, name, default=None) -> Any:
try: try:
return obj[name] return obj[name]
except KeyError: except KeyError:
@ -45,12 +46,12 @@ class Template:
# Closing tag delimiter # Closing tag delimiter
ctag = '}}' ctag = '}}'
def __init__(self, template, context=None): def __init__(self, template, context=None) -> None:
self.template = template self.template = template
self.context = context or {} self.context = context or {}
self.compile_regexps() self.compile_regexps()
def render(self, template=None, context=None, encoding=None): def render(self, template=None, context=None, encoding=None) -> str:
"""Turns a Mustache template into something wonderful.""" """Turns a Mustache template into something wonderful."""
template = template or self.template template = template or self.template
context = context or self.context context = context or self.context
@ -61,7 +62,7 @@ class Template:
result = result.encode(encoding) result = result.encode(encoding)
return result return result
def compile_regexps(self): def compile_regexps(self) -> None:
"""Compiles our section and tag regular expressions.""" """Compiles our section and tag regular expressions."""
tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) } tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) }
@ -71,7 +72,7 @@ class Template:
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
self.tag_re = re.compile(tag % tags) self.tag_re = re.compile(tag % tags)
def render_sections(self, template, context): def render_sections(self, template, context) -> NoReturn:
"""Expands sections.""" """Expands sections."""
while 1: while 1:
match = self.section_re.search(template) match = self.section_re.search(template)
@ -104,7 +105,7 @@ class Template:
return template return template
def render_tags(self, template, context): def render_tags(self, template, context) -> str:
"""Renders all the tags in a template for a context.""" """Renders all the tags in a template for a context."""
repCount = 0 repCount = 0
while 1: while 1:
@ -130,16 +131,16 @@ class Template:
# {{{ functions just like {{ in anki # {{{ functions just like {{ in anki
@modifier('{') @modifier('{')
def render_tag(self, tag_name, context): def render_tag(self, tag_name, context) -> Any:
return self.render_unescaped(tag_name, context) return self.render_unescaped(tag_name, context)
@modifier('!') @modifier('!')
def render_comment(self, tag_name=None, context=None): def render_comment(self, tag_name=None, context=None) -> str:
"""Rendering a comment always returns nothing.""" """Rendering a comment always returns nothing."""
return '' return ''
@modifier(None) @modifier(None)
def render_unescaped(self, tag_name=None, context=None): def render_unescaped(self, tag_name=None, context=None) -> Any:
"""Render a tag without escaping it.""" """Render a tag without escaping it."""
txt = get_or_attr(context, tag_name) txt = get_or_attr(context, tag_name)
if txt is not None: if txt is not None:
@ -192,7 +193,7 @@ class Template:
return '{unknown field %s}' % tag_name return '{unknown field %s}' % tag_name
return txt return txt
def clozeText(self, txt, ord, type): def clozeText(self, txt, ord, type) -> str:
reg = clozeReg reg = clozeReg
if not re.search(reg%ord, txt): if not re.search(reg%ord, txt):
return "" return ""
@ -215,7 +216,7 @@ class Template:
return re.sub(reg%r"\d+", "\\2", txt) return re.sub(reg%r"\d+", "\\2", txt)
# look for clozes wrapped in mathjax, and change {{cx to {{Cx # look for clozes wrapped in mathjax, and change {{cx to {{Cx
def _removeFormattingFromMathjax(self, txt, ord): def _removeFormattingFromMathjax(self, txt, ord) -> str:
opening = ["\\(", "\\["] opening = ["\\(", "\\["]
closing = ["\\)", "\\]"] closing = ["\\)", "\\]"]
# flags in middle of expression deprecated # flags in middle of expression deprecated
@ -237,7 +238,7 @@ class Template:
return txt return txt
@modifier('=') @modifier('=')
def render_delimiter(self, tag_name=None, context=None): def render_delimiter(self, tag_name=None, context=None) -> Optional[str]:
"""Changes the Mustache delimiter.""" """Changes the Mustache delimiter."""
try: try:
self.otag, self.ctag = tag_name.split(' ') self.otag, self.ctag = tag_name.split(' ')

View File

@ -1,6 +1,7 @@
from .template import Template from .template import Template
import os.path import os.path
import re import re
from typing import Any
class View: class View:
# Path where this view's template(s) live # Path where this view's template(s) live
@ -24,7 +25,7 @@ class View:
# do any decoding of the template. # do any decoding of the template.
template_encoding = None template_encoding = None
def __init__(self, template=None, context=None, **kwargs): def __init__(self, template=None, context=None, **kwargs) -> None:
self.template = template self.template = template
self.context = context or {} self.context = context or {}
@ -36,7 +37,7 @@ class View:
if kwargs: if kwargs:
self.context.update(kwargs) self.context.update(kwargs)
def inherit_settings(self, view): def inherit_settings(self, view) -> None:
"""Given another View, copies its settings.""" """Given another View, copies its settings."""
if view.template_path: if view.template_path:
self.template_path = view.template_path self.template_path = view.template_path
@ -44,7 +45,7 @@ class View:
if view.template_name: if view.template_name:
self.template_name = view.template_name self.template_name = view.template_name
def load_template(self): def load_template(self) -> Any:
if self.template: if self.template:
return self.template return self.template
@ -65,7 +66,7 @@ class View:
raise IOError('"%s" not found in "%s"' % (name, ':'.join(self.template_path),)) raise IOError('"%s" not found in "%s"' % (name, ':'.join(self.template_path),))
def _load_template(self): def _load_template(self) -> str:
f = open(self.template_file, 'r') f = open(self.template_file, 'r')
try: try:
template = f.read() template = f.read()
@ -75,7 +76,7 @@ class View:
f.close() f.close()
return template return template
def get_template_name(self, name=None): def get_template_name(self, name=None) -> Any:
"""TemplatePartial => template_partial """TemplatePartial => template_partial
Takes a string but defaults to using the current class' name or Takes a string but defaults to using the current class' name or
the `template_name` attribute the `template_name` attribute
@ -91,16 +92,16 @@ class View:
return re.sub('[A-Z]', repl, name)[1:] return re.sub('[A-Z]', repl, name)[1:]
def __contains__(self, needle): def __contains__(self, needle) -> bool:
return needle in self.context or hasattr(self, needle) return needle in self.context or hasattr(self, needle)
def __getitem__(self, attr): def __getitem__(self, attr) -> Any:
val = self.get(attr, None) val = self.get(attr, None)
if not val: if not val:
raise KeyError("No such key.") raise KeyError("No such key.")
return val return val
def get(self, attr, default): def get(self, attr, default) -> Any:
attr = self.context.get(attr, getattr(self, attr, default)) attr = self.context.get(attr, getattr(self, attr, default))
if hasattr(attr, '__call__'): if hasattr(attr, '__call__'):
@ -108,9 +109,9 @@ class View:
else: else:
return attr return attr
def render(self, encoding=None): def render(self, encoding=None) -> str:
template = self.load_template() template = self.load_template()
return Template(template, self).render(encoding=encoding) return Template(template, self).render(encoding=encoding)
def __str__(self): def __str__(self) -> str:
return self.render() return self.render()

View File

@ -22,11 +22,14 @@ from anki.lang import _, ngettext
# some add-ons expect json to be in the utils module # some add-ons expect json to be in the utils module
import json # pylint: disable=unused-import import json # pylint: disable=unused-import
from typing import Any, Optional, Tuple
_tmpdir: Optional[str]
# Time handling # Time handling
############################################################################## ##############################################################################
def intTime(scale=1): def intTime(scale=1) -> int:
"The time in integer seconds. Pass scale=1000 to get milliseconds." "The time in integer seconds. Pass scale=1000 to get milliseconds."
return int(time.time()*scale) return int(time.time()*scale)
@ -48,7 +51,7 @@ inTimeTable = {
"seconds": lambda n: ngettext("in %s second", "in %s seconds", n), "seconds": lambda n: ngettext("in %s second", "in %s seconds", n),
} }
def shortTimeFmt(type): def shortTimeFmt(type) -> Any:
return { return {
#T: year is an abbreviation for year. %s is a number of years #T: year is an abbreviation for year. %s is a number of years
"years": _("%sy"), "years": _("%sy"),
@ -64,7 +67,7 @@ def shortTimeFmt(type):
"seconds": _("%ss"), "seconds": _("%ss"),
}[type] }[type]
def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99) -> str:
"Return a string representing a time span (eg '2 days')." "Return a string representing a time span (eg '2 days')."
(type, point) = optimalPeriod(time, point, unit) (type, point) = optimalPeriod(time, point, unit)
time = convertSecondsTo(time, type) time = convertSecondsTo(time, type)
@ -80,7 +83,7 @@ def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99):
timestr = "%%%(a)d.%(b)df" % {'a': pad, 'b': point} timestr = "%%%(a)d.%(b)df" % {'a': pad, 'b': point}
return locale.format_string(fmt % timestr, time) return locale.format_string(fmt % timestr, time)
def optimalPeriod(time, point, unit): def optimalPeriod(time, point, unit) -> Tuple[str, Any]:
if abs(time) < 60 or unit < 1: if abs(time) < 60 or unit < 1:
type = "seconds" type = "seconds"
point -= 1 point -= 1
@ -98,7 +101,7 @@ def optimalPeriod(time, point, unit):
point += 1 point += 1
return (type, max(point, 0)) return (type, max(point, 0))
def convertSecondsTo(seconds, type): def convertSecondsTo(seconds, type) -> Any:
if type == "seconds": if type == "seconds":
return seconds return seconds
elif type == "minutes": elif type == "minutes":
@ -113,7 +116,7 @@ def convertSecondsTo(seconds, type):
return seconds / 31536000 return seconds / 31536000
assert False assert False
def _pluralCount(time, point): def _pluralCount(time, point) -> int:
if point: if point:
return 2 return 2
return math.floor(time) return math.floor(time)
@ -121,12 +124,12 @@ def _pluralCount(time, point):
# Locale # Locale
############################################################################## ##############################################################################
def fmtPercentage(float_value, point=1): def fmtPercentage(float_value, point=1) -> str:
"Return float with percentage sign" "Return float with percentage sign"
fmt = '%' + "0.%(b)df" % {'b': point} fmt = '%' + "0.%(b)df" % {'b': point}
return locale.format_string(fmt, float_value) + "%" return locale.format_string(fmt, float_value) + "%"
def fmtFloat(float_value, point=1): def fmtFloat(float_value, point=1) -> str:
"Return a string with decimal separator according to current locale" "Return a string with decimal separator according to current locale"
fmt = '%' + "0.%(b)df" % {'b': point} fmt = '%' + "0.%(b)df" % {'b': point}
return locale.format_string(fmt, float_value) return locale.format_string(fmt, float_value)
@ -140,7 +143,7 @@ reTag = re.compile("(?s)<.*?>")
reEnts = re.compile(r"&#?\w+;") reEnts = re.compile(r"&#?\w+;")
reMedia = re.compile("(?i)<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>") reMedia = re.compile("(?i)<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>")
def stripHTML(s): def stripHTML(s) -> str:
s = reComment.sub("", s) s = reComment.sub("", s)
s = reStyle.sub("", s) s = reStyle.sub("", s)
s = reScript.sub("", s) s = reScript.sub("", s)
@ -148,12 +151,12 @@ def stripHTML(s):
s = entsToTxt(s) s = entsToTxt(s)
return s return s
def stripHTMLMedia(s): def stripHTMLMedia(s) -> Any:
"Strip HTML but keep media filenames" "Strip HTML but keep media filenames"
s = reMedia.sub(" \\1 ", s) s = reMedia.sub(" \\1 ", s)
return stripHTML(s) return stripHTML(s)
def minimizeHTML(s): def minimizeHTML(s) -> str:
"Correct Qt's verbose bold/underline/etc." "Correct Qt's verbose bold/underline/etc."
s = re.sub('<span style="font-weight:600;">(.*?)</span>', '<b>\\1</b>', s = re.sub('<span style="font-weight:600;">(.*?)</span>', '<b>\\1</b>',
s) s)
@ -163,7 +166,7 @@ def minimizeHTML(s):
'<u>\\1</u>', s) '<u>\\1</u>', s)
return s return s
def htmlToTextLine(s): def htmlToTextLine(s) -> Any:
s = s.replace("<br>", " ") s = s.replace("<br>", " ")
s = s.replace("<br />", " ") s = s.replace("<br />", " ")
s = s.replace("<div>", " ") s = s.replace("<div>", " ")
@ -174,7 +177,7 @@ def htmlToTextLine(s):
s = s.strip() s = s.strip()
return s return s
def entsToTxt(html): def entsToTxt(html) -> str:
# entitydefs defines nbsp as \xa0 instead of a standard space, so we # entitydefs defines nbsp as \xa0 instead of a standard space, so we
# replace it first # replace it first
html = html.replace("&nbsp;", " ") html = html.replace("&nbsp;", " ")
@ -198,7 +201,7 @@ def entsToTxt(html):
return text # leave as is return text # leave as is
return reEnts.sub(fixup, html) return reEnts.sub(fixup, html)
def bodyClass(col, card): def bodyClass(col, card) -> str:
bodyclass = "card card%d" % (card.ord+1) bodyclass = "card card%d" % (card.ord+1)
if col.conf.get("nightMode"): if col.conf.get("nightMode"):
bodyclass += " nightMode" bodyclass += " nightMode"
@ -207,17 +210,17 @@ def bodyClass(col, card):
# IDs # IDs
############################################################################## ##############################################################################
def hexifyID(id): def hexifyID(id) -> str:
return "%x" % int(id) return "%x" % int(id)
def dehexifyID(id): def dehexifyID(id) -> int:
return int(id, 16) return int(id, 16)
def ids2str(ids): def ids2str(ids) -> str:
"""Given a list of integers, return a string '(int1,int2,...)'.""" """Given a list of integers, return a string '(int1,int2,...)'."""
return "(%s)" % ",".join(str(i) for i in ids) return "(%s)" % ",".join(str(i) for i in ids)
def timestampID(db, table): def timestampID(db, table) -> int:
"Return a non-conflicting timestamp for table." "Return a non-conflicting timestamp for table."
# be careful not to create multiple objects without flushing them, or they # be careful not to create multiple objects without flushing them, or they
# may share an ID. # may share an ID.
@ -226,7 +229,7 @@ def timestampID(db, table):
t += 1 t += 1
return t return t
def maxID(db): def maxID(db) -> Any:
"Return the first safe ID to use." "Return the first safe ID to use."
now = intTime(1000) now = intTime(1000)
for tbl in "cards", "notes": for tbl in "cards", "notes":
@ -234,7 +237,7 @@ def maxID(db):
return now + 1 return now + 1
# used in ankiweb # used in ankiweb
def base62(num, extra=""): def base62(num, extra="") -> str:
s = string; table = s.ascii_letters + s.digits + extra s = string; table = s.ascii_letters + s.digits + extra
buf = "" buf = ""
while num: while num:
@ -243,19 +246,19 @@ def base62(num, extra=""):
return buf return buf
_base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~" _base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
def base91(num): def base91(num) -> str:
# all printable characters minus quotes, backslash and separators # all printable characters minus quotes, backslash and separators
return base62(num, _base91_extra_chars) return base62(num, _base91_extra_chars)
def guid64(): def guid64() -> Any:
"Return a base91-encoded 64bit random number." "Return a base91-encoded 64bit random number."
return base91(random.randint(0, 2**64-1)) return base91(random.randint(0, 2**64-1))
# increment a guid by one, for note type conflicts # increment a guid by one, for note type conflicts
def incGuid(guid): def incGuid(guid) -> str:
return _incGuid(guid[::-1])[::-1] return _incGuid(guid[::-1])[::-1]
def _incGuid(guid): def _incGuid(guid) -> str:
s = string; table = s.ascii_letters + s.digits + _base91_extra_chars s = string; table = s.ascii_letters + s.digits + _base91_extra_chars
idx = table.index(guid[0]) idx = table.index(guid[0])
if idx + 1 == len(table): if idx + 1 == len(table):
@ -268,21 +271,21 @@ def _incGuid(guid):
# Fields # Fields
############################################################################## ##############################################################################
def joinFields(list): def joinFields(list) -> str:
return "\x1f".join(list) return "\x1f".join(list)
def splitFields(string): def splitFields(string) -> Any:
return string.split("\x1f") return string.split("\x1f")
# Checksums # Checksums
############################################################################## ##############################################################################
def checksum(data): def checksum(data) -> str:
if isinstance(data, str): if isinstance(data, str):
data = data.encode("utf-8") data = data.encode("utf-8")
return sha1(data).hexdigest() return sha1(data).hexdigest()
def fieldChecksum(data): def fieldChecksum(data) -> int:
# 32 bit unsigned number from first 8 digits of sha1 hash # 32 bit unsigned number from first 8 digits of sha1 hash
return int(checksum(stripHTMLMedia(data).encode("utf-8"))[:8], 16) return int(checksum(stripHTMLMedia(data).encode("utf-8"))[:8], 16)
@ -291,7 +294,7 @@ def fieldChecksum(data):
_tmpdir = None _tmpdir = None
def tmpdir(): def tmpdir() -> Any:
"A reusable temp folder which we clean out on each program invocation." "A reusable temp folder which we clean out on each program invocation."
global _tmpdir global _tmpdir
if not _tmpdir: if not _tmpdir:
@ -305,12 +308,12 @@ def tmpdir():
os.mkdir(_tmpdir) os.mkdir(_tmpdir)
return _tmpdir return _tmpdir
def tmpfile(prefix="", suffix=""): def tmpfile(prefix="", suffix="") -> Any:
(fd, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix) (fd, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix)
os.close(fd) os.close(fd)
return name return name
def namedtmp(name, rm=True): def namedtmp(name, rm=True) -> Any:
"Return tmpdir+name. Deletes any existing file." "Return tmpdir+name. Deletes any existing file."
path = os.path.join(tmpdir(), name) path = os.path.join(tmpdir(), name)
if rm: if rm:
@ -330,7 +333,7 @@ def noBundledLibs():
if oldlpath is not None: if oldlpath is not None:
os.environ["LD_LIBRARY_PATH"] = oldlpath os.environ["LD_LIBRARY_PATH"] = oldlpath
def call(argv, wait=True, **kwargs): def call(argv, wait=True, **kwargs) -> int:
"Execute a command. If WAIT, return exit code." "Execute a command. If WAIT, return exit code."
# ensure we don't open a separate window for forking process on windows # ensure we don't open a separate window for forking process on windows
if isWin: if isWin:
@ -372,7 +375,7 @@ devMode = os.getenv("ANKIDEV", "")
invalidFilenameChars = ":*?\"<>|" invalidFilenameChars = ":*?\"<>|"
def invalidFilename(str, dirsep=True): def invalidFilename(str, dirsep=True) -> Optional[str]:
for c in invalidFilenameChars: for c in invalidFilenameChars:
if c in str: if c in str:
return c return c
@ -383,7 +386,7 @@ def invalidFilename(str, dirsep=True):
elif str.strip().startswith("."): elif str.strip().startswith("."):
return "." return "."
def platDesc(): def platDesc() -> str:
# we may get an interrupted system call, so try this in a loop # we may get an interrupted system call, so try this in a loop
n = 0 n = 0
theos = "unknown" theos = "unknown"
@ -410,9 +413,9 @@ def platDesc():
############################################################################## ##############################################################################
class TimedLog: class TimedLog:
def __init__(self): def __init__(self) -> None:
self._last = time.time() self._last = time.time()
def log(self, s): def log(self, s) -> None:
path, num, fn, y = traceback.extract_stack(limit=2)[0] path, num, fn, y = traceback.extract_stack(limit=2)[0]
sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s)) sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s))
self._last = time.time() self._last = time.time()
@ -420,7 +423,7 @@ class TimedLog:
# Version # Version
############################################################################## ##############################################################################
def versionWithBuild(): def versionWithBuild() -> str:
from anki import version from anki import version
try: try:
from anki.buildhash import build # type: ignore from anki.buildhash import build # type: ignore

View File

@ -13,6 +13,7 @@ from threading import Thread
from typing import Optional from typing import Optional
from send2trash import send2trash from send2trash import send2trash
from anki.collection import _Collection from anki.collection import _Collection
from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import * from aqt.qt import *
from anki.storage import Collection from anki.storage import Collection
from anki.utils import isWin, isMac, intTime, splitFields, ids2str, \ from anki.utils import isWin, isMac, intTime, splitFields, ids2str, \
@ -33,7 +34,7 @@ from aqt.qt import sip
from anki.lang import _, ngettext from anki.lang import _, ngettext
class AnkiQt(QMainWindow): class AnkiQt(QMainWindow):
def __init__(self, app: QApplication, profileManager, opts, args): def __init__(self, app: QApplication, profileManager: ProfileManagerType, opts, args):
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.state = "startup" self.state = "startup"
self.opts = opts self.opts = opts