", "\n", latex)
latex = stripHTML(latex)
return latex
-def _buildImg(col, latex, fname, model):
+def _buildImg(col, latex, fname, model) -> Any:
# add header/footer
latex = (model["latexPre"] + "\n" +
latex + "\n" +
@@ -129,7 +130,7 @@ package in the LaTeX header instead.""") % bad
os.chdir(oldcwd)
log.close()
-def _errMsg(type, texpath):
+def _errMsg(type, texpath) -> Any:
msg = (_("Error executing %s.") % type) + "
"
msg += (_("Generated file: %s") % texpath) + "
"
try:
diff --git a/anki/media.py b/anki/media.py
index 42654b448..9fcaf674e 100644
--- a/anki/media.py
+++ b/anki/media.py
@@ -17,6 +17,9 @@ from anki.db import DB, DBError
from anki.consts import *
from anki.latex import mungeQA
from anki.lang import _
+from typing import Any, List, Optional, Tuple, TypeVar, Union
+
+_T0 = TypeVar('_T0')
class MediaManager:
@@ -29,7 +32,7 @@ class MediaManager:
]
regexps = soundRegexps + imgRegexps
- def __init__(self, col, server):
+ def __init__(self, col, server) -> None:
self.col = col
if server:
self._dir = None
@@ -50,7 +53,7 @@ class MediaManager:
# change database
self.connect()
- def connect(self):
+ def connect(self) -> None:
if self.col.server:
return
path = self.dir()+".db2"
@@ -61,7 +64,7 @@ class MediaManager:
self._initDB()
self.maybeUpgrade()
- def _initDB(self):
+ def _initDB(self) -> None:
self.db.executescript("""
create table media (
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);
""")
- def maybeUpgrade(self):
+ def maybeUpgrade(self) -> None:
oldpath = self.dir()+".db"
if os.path.exists(oldpath):
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.rename("../collection.media.db", npath)
- def close(self):
+ def close(self) -> None:
if self.col.server:
return
self.db.close()
@@ -115,16 +118,16 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# may have been deleted
pass
- def _deleteDB(self):
+ def _deleteDB(self) -> None:
path = self.db._path
self.close()
os.unlink(path)
self.connect()
- def dir(self):
+ def dir(self) -> Any:
return self._dir
- def _isFAT32(self):
+ def _isFAT32(self) -> Optional[bool]:
if not isWin:
return
# 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
- def addFile(self, opath):
+ def addFile(self, opath) -> Any:
with open(opath, "rb") as f:
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
fname = os.path.basename(opath)
@@ -193,7 +196,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# String manipulation
##########################################################################
- def filesInStr(self, mid, string, includeRemote=False):
+ def filesInStr(self, mid, string, includeRemote=False) -> List[str]:
l = []
model = self.col.models.get(mid)
strings = []
@@ -215,7 +218,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
l.append(fname)
return l
- def _expandClozes(self, string):
+ def _expandClozes(self, string) -> List[str]:
ords = set(re.findall(r"{{c(\d+)::.+?}}", string))
strings = []
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))
return strings
- def transformNames(self, txt, func):
+ def transformNames(self, txt, func) -> Any:
for reg in self.regexps:
txt = re.sub(reg, func, txt)
return txt
- def strip(self, txt):
+ def strip(self, txt: _T0) -> Union[str, _T0]:
for reg in self.regexps:
txt = re.sub(reg, "", txt)
return txt
- def escapeImages(self, string, unescape=False):
+ def escapeImages(self, string: _T0, unescape=False) -> Union[str, _T0]:
if unescape:
fn = urllib.parse.unquote
else:
@@ -261,7 +264,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Rebuilding DB
##########################################################################
- def check(self, local=None):
+ def check(self, local=None) -> Any:
"Return (missingFiles, unusedFiles)."
mdir = self.dir()
# 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."))
return (nohave, unused, warnings)
- def _normalizeNoteRefs(self, nid):
+ def _normalizeNoteRefs(self, nid) -> None:
note = self.col.getNote(nid)
for c, fld in enumerate(note.fields):
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
##########################################################################
- def have(self, fname):
+ def have(self, fname) -> bool:
return os.path.exists(os.path.join(self.dir(), fname))
# 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]')
- def stripIllegal(self, str):
+ def stripIllegal(self, str) -> str:
return re.sub(self._illegalCharReg, "", 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 False
- def cleanFilename(self, fname):
+ def cleanFilename(self, fname) -> str:
fname = self.stripIllegal(fname)
fname = self._cleanWin32Filename(fname)
fname = self._cleanLongFilename(fname)
@@ -375,7 +378,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return fname
- def _cleanWin32Filename(self, fname):
+ def _cleanWin32Filename(self, fname: _T0) -> Union[str, _T0]:
if not isWin:
return fname
@@ -387,7 +390,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return fname
- def _cleanLongFilename(self, fname):
+ def _cleanLongFilename(self, fname) -> Any:
# a fairly safe limit that should work on typical windows
# paths and on eCryptfs partitions, even with a duplicate
# suffix appended
@@ -416,22 +419,22 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Tracking changes
##########################################################################
- def findChanges(self):
+ def findChanges(self) -> None:
"Scan the media folder if it's changed, and note any changes."
if self._changed():
self._logChanges()
- def haveDirty(self):
+ def haveDirty(self) -> Any:
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)
- def _checksum(self, path):
+ def _checksum(self, path) -> str:
with open(path, "rb") as f:
return checksum(f.read())
- def _changed(self):
+ def _changed(self) -> int:
"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
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 mtime
- def _logChanges(self):
+ def _logChanges(self) -> None:
(added, removed) = self._changes()
media = []
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.commit()
- def _changes(self):
+ def _changes(self) -> Tuple[List[Tuple[str, int]], List[str]]:
self.cache = {}
for (name, csum, mod) in self.db.execute(
"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
##########################################################################
- def lastUsn(self):
+ def lastUsn(self) -> Any:
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.commit()
- def syncInfo(self, fname):
+ def syncInfo(self, fname) -> Any:
ret = self.db.first(
"select csum, dirty from media where fname=?", fname)
return ret or (None, 0)
- def markClean(self, fnames):
+ def markClean(self, fnames) -> None:
for fname in fnames:
self.db.execute(
"update media set dirty=0 where fname=?", fname)
- def syncDelete(self, fname):
+ def syncDelete(self, fname) -> None:
if os.path.exists(fname):
os.unlink(fname)
self.db.execute("delete from media where fname=?", fname)
- def mediaCount(self):
+ def mediaCount(self) -> Any:
return self.db.scalar(
"select count() from media where csum is not null")
- def dirtyCount(self):
+ def dirtyCount(self) -> Any:
return self.db.scalar(
"select count() from media where dirty=1")
- def forceResync(self):
+ def forceResync(self) -> None:
self.db.execute("delete from media")
self.db.execute("update meta set lastUsn=0,dirMod=0")
self.db.commit()
@@ -557,7 +560,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# Media syncing: zips
##########################################################################
- def mediaChangesZip(self):
+ def mediaChangesZip(self) -> Tuple[bytes, list]:
f = io.BytesIO()
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()
return f.getvalue(), fnames
- def addFilesFromZip(self, zipData):
+ def addFilesFromZip(self, zipData) -> int:
"Extract zip data; true if finished."
f = io.BytesIO(zipData)
z = zipfile.ZipFile(f, "r")
diff --git a/anki/models.py b/anki/models.py
index 2bfc0d47b..005198324 100644
--- a/anki/models.py
+++ b/anki/models.py
@@ -10,6 +10,7 @@ from anki.lang import _
from anki.consts import *
from anki.hooks import runHook
import time
+from typing import List, Optional, Tuple, Union
# Models
##########################################################################
@@ -75,17 +76,17 @@ class ModelManager:
# Saving/loading registry
#############################################################
- def __init__(self, col):
+ def __init__(self, col) -> None:
self.col = col
self.models = {}
self.changed = False
- def load(self, json_):
+ def load(self, json_) -> None:
"Load registry from JSON."
self.changed = False
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."
if m and m['id']:
m['mod'] = intTime()
@@ -97,7 +98,7 @@ class ModelManager:
self.changed = True
runHook("newModel")
- def flush(self):
+ def flush(self) -> None:
"Flush the registry if any models were changed."
if self.changed:
self.ensureNotEmpty()
@@ -105,7 +106,7 @@ class ModelManager:
json.dumps(self.models))
self.changed = False
- def ensureNotEmpty(self):
+ def ensureNotEmpty(self) -> Optional[bool]:
if not self.models:
from anki.stdmodels import addBasicModel
addBasicModel(self.col)
@@ -114,37 +115,37 @@ class ModelManager:
# Retrieving and creating models
#############################################################
- def current(self, forDeck=True):
+ def current(self, forDeck=True) -> Any:
"Get current model."
m = self.get(self.col.decks.current().get('mid'))
if not forDeck or not m:
m = self.get(self.col.conf['curModel'])
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.setMod()
- def get(self, id):
+ def get(self, id) -> Any:
"Get model with ID, or None."
id = str(id)
if id in self.models:
return self.models[id]
- def all(self):
+ def all(self) -> List:
"Get all models."
return list(self.models.values())
- def allNames(self):
+ def allNames(self) -> List:
return [m['name'] for m in self.all()]
- def byName(self, name):
+ def byName(self, name) -> Any:
"Get model with NAME."
for m in list(self.models.values()):
if m['name'] == name:
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."
# caller should call save() after modifying
m = defaultModel.copy()
@@ -156,7 +157,7 @@ class ModelManager:
m['id'] = None
return m
- def rem(self, m):
+ def rem(self, m) -> None:
"Delete model, and all its cards/notes."
self.col.modSchema(check=True)
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:
self.setCurrent(list(self.models.values())[0])
- def add(self, m):
+ def add(self, m) -> None:
self._setID(m)
self.update(m)
self.setCurrent(m)
self.save(m)
- def ensureNameUnique(self, m):
+ def ensureNameUnique(self, m) -> None:
for mcur in self.all():
if (mcur['name'] == m['name'] and mcur['id'] != m['id']):
m['name'] += "-" + checksum(str(time.time()))[:5]
break
- def update(self, m):
+ def update(self, m) -> None:
"Add or update an existing model. Used for syncing and merging."
self.ensureNameUnique(m)
self.models[str(m['id'])] = m
# mark registry changed, but don't bump mod time
self.save()
- def _setID(self, m):
+ def _setID(self, m) -> None:
while 1:
id = str(intTime(1000))
if id not in self.models:
break
m['id'] = id
- def have(self, id):
+ def have(self, id) -> bool:
return str(id) in self.models
- def ids(self):
+ def ids(self) -> List[str]:
return list(self.models.keys())
# Tools
##################################################
- def nids(self, m):
+ def nids(self, m) -> Any:
"Note ids for M."
return self.col.db.list(
"select id from notes where mid = ?", m['id'])
- def useCount(self, m):
+ def useCount(self, m) -> Any:
"Number of note using M."
return self.col.db.scalar(
"select count() from notes where mid = ?", m['id'])
- def tmplUseCount(self, m, ord):
+ def tmplUseCount(self, m, ord) -> Any:
return self.col.db.scalar("""
select count() from cards, notes where cards.nid = notes.id
and notes.mid = ? and cards.ord = ?""", m['id'], ord)
@@ -224,7 +225,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# Copying
##################################################
- def copy(self, m):
+ def copy(self, m) -> Any:
"Copy, save and return."
m2 = copy.deepcopy(m)
m2['name'] = _("%s copy") % m2['name']
@@ -234,30 +235,30 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# Fields
##################################################
- def newField(self, name):
+ def newField(self, name) -> Dict[str, Any]:
assert(isinstance(name, str))
f = defaultField.copy()
f['name'] = name
return f
- def fieldMap(self, m):
+ def fieldMap(self, m) -> Dict[Any, Tuple[Any, Any]]:
"Mapping of field name -> (ord, field)."
return dict((f['name'], (f['ord'], f)) for f in m['flds'])
- def fieldNames(self, m):
+ def fieldNames(self, m) -> List:
return [f['name'] for f in m['flds']]
- def sortIdx(self, m):
+ def sortIdx(self, m) -> Any:
return m['sortf']
- def setSortIdx(self, m, idx):
+ def setSortIdx(self, m, idx) -> None:
assert 0 <= idx < len(m['flds'])
self.col.modSchema(check=True)
m['sortf'] = idx
self.col.updateFieldCache(self.nids(m))
self.save(m, updateReqs=False)
- def addField(self, m, field):
+ def addField(self, m, field) -> None:
# only mod schema if model isn't new
if m['id']:
self.col.modSchema(check=True)
@@ -269,7 +270,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
return fields
self._transformFields(m, add)
- def remField(self, m, field):
+ def remField(self, m, field) -> None:
self.col.modSchema(check=True)
# save old sort field
sortFldName = m['flds'][m['sortf']]['name']
@@ -292,7 +293,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# saves
self.renameField(m, field, None)
- def moveField(self, m, field, idx):
+ def moveField(self, m, field, idx) -> None:
self.col.modSchema(check=True)
oldidx = m['flds'].index(field)
if oldidx == idx:
@@ -313,7 +314,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
return fields
self._transformFields(m, move)
- def renameField(self, m, field, newName):
+ def renameField(self, m, field, newName) -> None:
self.col.modSchema(check=True)
pat = r'{{([^{}]*)([:#^/]|[^:#/^}][^:}]*?:|)%s}}'
def wrap(txt):
@@ -331,11 +332,11 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
field['name'] = newName
self.save(m)
- def _updateFieldOrds(self, m):
+ def _updateFieldOrds(self, m) -> None:
for c, f in enumerate(m['flds']):
f['ord'] = c
- def _transformFields(self, m, fn):
+ def _transformFields(self, m, fn) -> None:
# model hasn't been added yet?
if not m['id']:
return
@@ -350,12 +351,12 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
# Templates
##################################################
- def newTemplate(self, name):
+ def newTemplate(self, name: str) -> Dict[str, Any]:
t = defaultTemplate.copy()
t['name'] = name
return t
- def addTemplate(self, m, template):
+ def addTemplate(self, m, template) -> None:
"Note: should col.genCards() afterwards."
if m['id']:
self.col.modSchema(check=True)
@@ -363,7 +364,7 @@ and notes.mid = ? and cards.ord = ?""", m['id'], ord)
self._updateTemplOrds(m)
self.save(m)
- def remTemplate(self, m, template):
+ def remTemplate(self, m, template) -> bool:
"False if removing template would leave orphan notes."
assert len(m['tmpls']) > 1
# find cards using this template
@@ -393,11 +394,11 @@ update cards set ord = ord - 1, usn = ?, mod = ?
self.save(m)
return True
- def _updateTemplOrds(self, m):
+ def _updateTemplOrds(self, m) -> None:
for c, t in enumerate(m['tmpls']):
t['ord'] = c
- def moveTemplate(self, m, template, idx):
+ def moveTemplate(self, m, template, idx) -> None:
oldidx = m['tmpls'].index(template)
if oldidx == idx:
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),
self.col.usn(), intTime(), m['id'])
- def _syncTemplates(self, m):
+ def _syncTemplates(self, m) -> None:
rem = self.col.genCards(self.nids(m))
# 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
# - 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)
assert newModel['id'] == m['id'] or (fmap and cmap)
if fmap:
@@ -433,7 +434,7 @@ select id from notes where mid = ?)""" % " ".join(map),
self._changeCards(nids, m, newModel, cmap)
self.col.genCards(nids)
- def _changeNotes(self, nids, newModel, map):
+ def _changeNotes(self, nids, newModel, map) -> None:
d = []
nfields = len(newModel['flds'])
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)
self.col.updateFieldCache(nids)
- def _changeCards(self, nids, oldModel, newModel, map):
+ def _changeCards(self, nids, oldModel, newModel, map) -> None:
d = []
deleted = []
for (cid, ord) in self.col.db.execute(
@@ -482,7 +483,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# Schema hash
##########################################################################
- def scmhash(self, m):
+ def scmhash(self, m) -> str:
"Return a hash of the schema, to see if models are compatible."
s = ""
for f in m['flds']:
@@ -494,7 +495,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# Required field/text cache
##########################################################################
- def _updateRequired(self, m):
+ def _updateRequired(self, m) -> None:
if m['type'] == MODEL_CLOZE:
# nothing to do
return
@@ -505,7 +506,7 @@ select id from notes where mid = ?)""" % " ".join(map),
req.append([t['ord'], ret[0], ret[1]])
m['req'] = req
- def _reqForTemplate(self, m, flds, t):
+ def _reqForTemplate(self, m, flds, t) -> Tuple[Union[str, List[int]], ...]:
a = []
b = []
for f in flds:
@@ -542,7 +543,7 @@ select id from notes where mid = ?)""" % " ".join(map),
req.append(i)
return type, req
- def availOrds(self, m, flds):
+ def availOrds(self, m, flds) -> List:
"Given a joined field string, return available template ordinals."
if m['type'] == MODEL_CLOZE:
return self._availClozeOrds(m, flds)
@@ -576,7 +577,7 @@ select id from notes where mid = ?)""" % " ".join(map),
avail.append(ord)
return avail
- def _availClozeOrds(self, m, flds, allowEmpty=True):
+ def _availClozeOrds(self, m, flds, allowEmpty=True) -> List:
sflds = splitFields(flds)
map = self.fieldMap(m)
ords = set()
@@ -598,7 +599,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# Sync handling
##########################################################################
- def beforeUpload(self):
+ def beforeUpload(self) -> None:
for m in self.all():
m['usn'] = 0
self.save()
diff --git a/anki/mpv.py b/anki/mpv.py
index 13267deb0..c111f1343 100644
--- a/anki/mpv.py
+++ b/anki/mpv.py
@@ -56,6 +56,7 @@ class MPVTimeoutError(MPVError):
pass
from anki.utils import isWin
+from typing import Any
if isWin:
# pylint: disable=import-error
import win32file, win32pipe, pywintypes, winerror # pytype: disable=import-error
@@ -76,7 +77,7 @@ class MPVBase:
"--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.debug = debug
@@ -87,18 +88,18 @@ class MPVBase:
self._prepare_thread()
self._start_thread()
- def __del__(self):
+ def __del__(self) -> None:
self._stop_thread()
self._stop_process()
self._stop_socket()
- def _thread_id(self):
+ def _thread_id(self) -> int:
return threading.get_ident()
#
# Process
#
- def _prepare_process(self):
+ def _prepare_process(self) -> None:
"""Prepare the argument list for the mpv process.
"""
self.argv = [self.executable]
@@ -107,12 +108,12 @@ class MPVBase:
if self.window_id is not None:
self.argv += ["--wid", str(self.window_id)]
- def _start_process(self):
+ def _start_process(self) -> None:
"""Start the mpv process.
"""
self._proc = subprocess.Popen(self.argv, env=self.popenEnv)
- def _stop_process(self):
+ def _stop_process(self) -> None:
"""Stop the mpv process.
"""
if hasattr(self, "_proc"):
@@ -125,7 +126,7 @@ class MPVBase:
#
# Socket communication
#
- def _prepare_socket(self):
+ def _prepare_socket(self) -> None:
"""Create a random socket filename which we pass to mpv with the
--input-unix-socket option.
"""
@@ -136,7 +137,7 @@ class MPVBase:
os.close(fd)
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
startup.
"""
@@ -173,7 +174,7 @@ class MPVBase:
else:
raise MPVProcessError("unable to start process")
- def _stop_socket(self):
+ def _stop_socket(self) -> None:
"""Clean up the socket.
"""
if hasattr(self, "_sock"):
@@ -184,7 +185,7 @@ class MPVBase:
except OSError:
pass
- def _prepare_thread(self):
+ def _prepare_thread(self) -> None:
"""Set up the queues for the communication threads.
"""
self._request_queue = Queue(1)
@@ -192,14 +193,14 @@ class MPVBase:
self._event_queue = Queue()
self._stop_event = threading.Event()
- def _start_thread(self):
+ def _start_thread(self) -> None:
"""Start up the communication threads.
"""
self._thread = threading.Thread(target=self._reader)
self._thread.daemon = True
self._thread.start()
- def _stop_thread(self):
+ def _stop_thread(self) -> None:
"""Stop the communication threads.
"""
if hasattr(self, "_stop_event"):
@@ -207,7 +208,7 @@ class MPVBase:
if hasattr(self, "_thread"):
self._thread.join()
- def _reader(self):
+ def _reader(self) -> None:
"""Read the incoming json messages from the unix socket that is
connected to the mpv process. Pass them on to the message handler.
"""
@@ -249,21 +250,21 @@ class MPVBase:
#
# Message handling
#
- def _compose_message(self, message):
+ def _compose_message(self, message) -> bytes:
"""Return a json representation from a message dictionary.
"""
# XXX may be strict is too strict ;-)
data = json.dumps(message)
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.
"""
# XXX may be strict is too strict ;-)
data = data.decode("utf8", "strict")
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
commands or asynchronous events.
"""
@@ -283,7 +284,7 @@ class MPVBase:
else:
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
dictionary of the form {"command": ["arg1", "arg2", ...]}. Responses
from the mpv process must be collected using _get_response().
@@ -320,7 +321,7 @@ class MPVBase:
raise MPVCommunicationError("broken sender socket")
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
error a MPVCommandError exception is raised, otherwise the command
specific data is returned.
@@ -335,7 +336,7 @@ class MPVBase:
else:
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
from the mpv process. If a timeout is specified and there have not
been any events during that period, None is returned.
@@ -345,7 +346,7 @@ class MPVBase:
except Empty:
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.
"""
self.ensure_running()
@@ -365,12 +366,12 @@ class MPVBase:
#
# Public API
#
- def is_running(self):
+ def is_running(self) -> bool:
"""Return True if the mpv process is still active.
"""
return self._proc.poll() is None
- def ensure_running(self):
+ def ensure_running(self) -> None:
if not self.is_running():
self._stop_thread()
self._stop_process()
@@ -382,7 +383,7 @@ class MPVBase:
self._prepare_thread()
self._start_thread()
- def close(self):
+ def close(self) -> None:
"""Shutdown the mpv process and our communication setup.
"""
if self.is_running():
@@ -413,7 +414,7 @@ class MPV(MPVBase):
threads to the same MPV instance are synchronized.
"""
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._callbacks = {}
@@ -463,7 +464,7 @@ class MPV(MPVBase):
#
# Event/callback API
#
- def _event_reader(self):
+ def _event_reader(self) -> None:
"""Collect incoming event messages and call the event handler.
"""
while not self._stop_event.is_set():
@@ -473,7 +474,7 @@ class MPV(MPVBase):
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.
"""
if message["event"] == "property-change":
@@ -487,7 +488,7 @@ class MPV(MPVBase):
else:
callback()
- def register_callback(self, name, callback):
+ def register_callback(self, name, callback) -> None:
"""Register a function `callback` for the event `name`.
"""
try:
@@ -497,7 +498,7 @@ class MPV(MPVBase):
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
`name`.
"""
@@ -511,7 +512,7 @@ class MPV(MPVBase):
except ValueError:
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
property `name`.
"""
@@ -533,7 +534,7 @@ class MPV(MPVBase):
self._property_serials[(name, callback)] = 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
property-change event on property `name`.
"""
@@ -553,17 +554,17 @@ class MPV(MPVBase):
#
# 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.
"""
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 self.command("get_property", name)
- def set_property(self, name, value):
+ def set_property(self, name, value) -> Any:
"""Set the value of property `name`.
"""
return self.command("set_property", name, value)
diff --git a/anki/notes.py b/anki/notes.py
index d83c8f55f..b1bd5953e 100644
--- a/anki/notes.py
+++ b/anki/notes.py
@@ -4,10 +4,11 @@
from anki.utils import fieldChecksum, intTime, \
joinFields, splitFields, stripHTMLMedia, timestampID, guid64
+from typing import Any, List, Tuple
class Note:
- def __init__(self, col, model=None, id=None):
+ def __init__(self, col, model=None, id=None) -> None:
assert not (model and id)
self.col = col
self.newlyAdded = False
@@ -26,7 +27,7 @@ class Note:
self._fmap = self.col.models.fieldMap(self._model)
self.scm = self.col.scm
- def load(self):
+ def load(self) -> None:
(self.guid,
self.mid,
self.mod,
@@ -43,7 +44,7 @@ from notes where id = ?""", self.id)
self._fmap = self.col.models.fieldMap(self._model)
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."
assert self.scm == self.col.scm
self._preFlush()
@@ -66,57 +67,57 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
self.col.tags.register(self.tags)
self._postFlush()
- def joinedFields(self):
+ def joinedFields(self) -> str:
return joinFields(self.fields)
- def cards(self):
+ def cards(self) -> List:
return [self.col.getCard(id) for id in self.col.db.list(
"select id from cards where nid = ? order by ord", self.id)]
- def model(self):
+ def model(self) -> Any:
return self._model
# Dict interface
##################################################
- def keys(self):
+ def keys(self) -> List:
return list(self._fmap.keys())
- def values(self):
+ def values(self) -> Any:
return self.fields
- def items(self):
+ def items(self) -> List[Tuple[Any, Any]]:
return [(f['name'], self.fields[ord])
for ord, f in sorted(self._fmap.values())]
- def _fieldOrd(self, key):
+ def _fieldOrd(self, key) -> Any:
try:
return self._fmap[key][0]
except:
raise KeyError(key)
- def __getitem__(self, key):
+ def __getitem__(self, key) -> Any:
return self.fields[self._fieldOrd(key)]
- def __setitem__(self, key, value):
+ def __setitem__(self, key, value) -> None:
self.fields[self._fieldOrd(key)] = value
- def __contains__(self, key):
+ def __contains__(self, key) -> bool:
return key in list(self._fmap.keys())
# Tags
##################################################
- def hasTag(self, tag):
+ def hasTag(self, tag) -> Any:
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))
- def setTagsFromStr(self, str):
+ def setTagsFromStr(self, str) -> None:
self.tags = self.col.tags.split(str)
- def delTag(self, tag):
+ def delTag(self, tag) -> None:
rem = []
for t in self.tags:
if t.lower() == tag.lower():
@@ -124,14 +125,14 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
for r in rem:
self.tags.remove(r)
- def addTag(self, tag):
+ def addTag(self, tag) -> None:
# duplicates will be stripped on save
self.tags.append(tag)
# Unique/duplicate check
##################################################
- def dupeOrEmpty(self):
+ def dupeOrEmpty(self) -> int:
"1 if first is empty; 2 if first is a duplicate, False otherwise."
val = self.fields[0]
if not val.strip():
@@ -149,12 +150,12 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
# Flushing cloze notes
##################################################
- def _preFlush(self):
+ def _preFlush(self) -> None:
# have we been added yet?
self.newlyAdded = not self.col.db.scalar(
"select 1 from cards where nid = ?", self.id)
- def _postFlush(self):
+ def _postFlush(self) -> None:
# generate missing cards
if not self.newlyAdded:
rem = self.col.genCards([self.id])
diff --git a/anki/sched.py b/anki/sched.py
index dcce7963f..ae2ad53ed 100644
--- a/anki/sched.py
+++ b/anki/sched.py
@@ -13,6 +13,9 @@ from anki.utils import ids2str, intTime, fmtTimeSpan
from anki.lang import _
from anki.consts import *
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
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
@@ -24,7 +27,7 @@ class Scheduler:
_spreadRev = True
_burySiblingsOnAnswer = True
- def __init__(self, col):
+ def __init__(self, col) -> None:
self.col = col
self.queueLimit = 50
self.reportLimit = 1000
@@ -33,7 +36,7 @@ class Scheduler:
self._haveQueues = False
self._updateCutoff()
- def getCard(self):
+ def getCard(self) -> Any:
"Pop the next card from the queue. None if finished."
self._checkDay()
if not self._haveQueues:
@@ -47,14 +50,14 @@ class Scheduler:
card.startTimer()
return card
- def reset(self):
+ def reset(self) -> None:
self._updateCutoff()
self._resetLrn()
self._resetRev()
self._resetNew()
self._haveQueues = True
- def answerCard(self, card, ease):
+ def answerCard(self, card, ease) -> None:
self.col.log()
assert 1 <= ease <= 4
self.col.markReview(card)
@@ -93,7 +96,7 @@ class Scheduler:
card.usn = self.col.usn()
card.flushSched()
- def counts(self, card=None):
+ def counts(self, card=None) -> tuple:
counts = [self.newCount, self.lrnCount, self.revCount]
if card:
idx = self.countIdx(card)
@@ -103,7 +106,7 @@ class Scheduler:
counts[idx] += 1
return tuple(counts)
- def dueForecast(self, days=7):
+ def dueForecast(self, days=7) -> List:
"Return counts over next DAYS. Includes today."
daysd = dict(self.col.db.all("""
select due, count() from cards
@@ -121,12 +124,12 @@ order by due""" % self._deckLimit(),
ret = [x[1] for x in sorted(daysd.items())]
return ret
- def countIdx(self, card):
+ def countIdx(self, card) -> Any:
if card.queue == 3:
return 1
return card.queue
- def answerButtons(self, card):
+ def answerButtons(self, card) -> int:
if card.odue:
# normal review in dyn deck?
if card.odid and card.queue == 2:
@@ -140,7 +143,7 @@ order by due""" % self._deckLimit(),
else:
return 3
- def unburyCards(self):
+ def unburyCards(self) -> None:
"Unbury cards."
self.col.conf['lastUnburied'] = self.today
self.col.log(
@@ -148,7 +151,7 @@ order by due""" % self._deckLimit(),
self.col.db.execute(
"update cards set queue=type where queue = -2")
- def unburyCardsForDeck(self):
+ def unburyCardsForDeck(self) -> None:
sids = ids2str(self.col.decks.active())
self.col.log(
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
##########################################################################
- def _updateStats(self, card, type, cnt=1):
+ def _updateStats(self, card, type, cnt=1) -> None:
key = type+"Today"
for g in ([self.col.decks.get(card.did)] +
self.col.decks.parents(card.did)):
@@ -168,7 +171,7 @@ order by due""" % self._deckLimit(),
g[key][1] += cnt
self.col.decks.save(g)
- def extendLimits(self, new, rev):
+ def extendLimits(self, new, rev) -> None:
cur = self.col.decks.current()
parents = self.col.decks.parents(cur['id'])
children = [self.col.decks.get(did) for (name, did) in
@@ -179,7 +182,7 @@ order by due""" % self._deckLimit(),
g['revToday'][1] -= rev
self.col.decks.save(g)
- def _walkingCount(self, limFn=None, cntFn=None):
+ def _walkingCount(self, limFn=None, cntFn=None) -> Any:
tot = 0
pcounts = {}
# for each of the active decks
@@ -213,7 +216,7 @@ order by due""" % self._deckLimit(),
# Deck list
##########################################################################
- def deckDueList(self):
+ def deckDueList(self) -> List[list]:
"Returns [deckname, did, rev, lrn, new]"
self._checkDay()
self.col.decks.checkIntegrity()
@@ -247,10 +250,10 @@ order by due""" % self._deckLimit(),
lims[deck['name']] = [nlim, rlim]
return data
- def deckDueTree(self):
+ def deckDueTree(self) -> Any:
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
for g in grps:
g[0] = g[0].split("::")
@@ -259,7 +262,7 @@ order by due""" % self._deckLimit(),
# then run main function
return self._groupChildrenMain(grps)
- def _groupChildrenMain(self, grps):
+ def _groupChildrenMain(self, grps) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]:
tree = []
# group and recurse
def key(grp):
@@ -300,7 +303,7 @@ order by due""" % self._deckLimit(),
# Getting the next card
##########################################################################
- def _getCard(self):
+ def _getCard(self) -> Any:
"Return the next due card id, or None."
# learning card due?
c = self._getLrnCard()
@@ -329,19 +332,19 @@ order by due""" % self._deckLimit(),
# New cards
##########################################################################
- def _resetNewCount(self):
+ def _resetNewCount(self) -> None:
cntFn = lambda did, lim: self.col.db.scalar("""
select count() from (select 1 from cards where
did = ? and queue = 0 limit ?)""", did, lim)
self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn)
- def _resetNew(self):
+ def _resetNew(self) -> None:
self._resetNewCount()
self._newDids = self.col.decks.active()[:]
self._newQueue = []
self._updateNewCardRatio()
- def _fillNew(self):
+ def _fillNew(self) -> Any:
if self._newQueue:
return True
if not self.newCount:
@@ -365,12 +368,12 @@ did = ? and queue = 0 limit ?)""", did, lim)
self._resetNew()
return self._fillNew()
- def _getNewCard(self):
+ def _getNewCard(self) -> Any:
if self._fillNew():
self.newCount -= 1
return self.col.getCard(self._newQueue.pop())
- def _updateNewCardRatio(self):
+ def _updateNewCardRatio(self) -> None:
if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.newCount:
self.newCardModulus = (
@@ -381,7 +384,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
return
self.newCardModulus = 0
- def _timeForNewCard(self):
+ def _timeForNewCard(self) -> Optional[int]:
"True if it's time to display a new card when distributing."
if not self.newCount:
return False
@@ -392,7 +395,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
elif self.newCardModulus:
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:
fn = self._deckNewLimitSingle
sel = self.col.decks.get(did)
@@ -406,7 +409,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
lim = min(rem, lim)
return lim
- def _newForDeck(self, did, lim):
+ def _newForDeck(self, did, lim) -> Any:
"New count for a single deck."
if not lim:
return 0
@@ -415,14 +418,14 @@ did = ? and queue = 0 limit ?)""", did, lim)
select count() from
(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."
if g['dyn']:
return self.reportLimit
c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1])
- def totalNewForCurrentDeck(self):
+ def totalNewForCurrentDeck(self) -> Any:
return self.col.db.scalar(
"""
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
##########################################################################
- def _resetLrnCount(self):
+ def _resetLrnCount(self) -> None:
# sub-day
self.lrnCount = self.col.db.scalar("""
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),
self.today)
- def _resetLrn(self):
+ def _resetLrn(self) -> None:
self._resetLrnCount()
self._lrnQueue = []
self._lrnDayQueue = []
self._lrnDids = self.col.decks.active()[:]
# sub-day learning
- def _fillLrn(self):
+ def _fillLrn(self) -> Any:
if not self.lrnCount:
return False
if self._lrnQueue:
@@ -465,7 +468,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
self._lrnQueue.sort()
return self._lrnQueue
- def _getLrnCard(self, collapse=False):
+ def _getLrnCard(self, collapse=False) -> Any:
if self._fillLrn():
cutoff = time.time()
if collapse:
@@ -477,7 +480,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
return card
# daily learning
- def _fillLrnDay(self):
+ def _fillLrnDay(self) -> Optional[bool]:
if not self.lrnCount:
return False
if self._lrnDayQueue:
@@ -501,12 +504,12 @@ did = ? and queue = 3 and due <= ? limit ?""",
# nothing left in the deck; move to next
self._lrnDids.pop(0)
- def _getLrnDayCard(self):
+ def _getLrnDayCard(self) -> Any:
if self._fillLrnDay():
self.lrnCount -= 1
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
conf = self._lrnConf(card)
if card.odid and not card.wasNew:
@@ -568,7 +571,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.queue = 3
self._logLrn(card, ease, conf, leaving, type, lastLeft)
- def _delayForGrade(self, conf, left):
+ def _delayForGrade(self, conf, left) -> Any:
left = left % 1000
try:
delay = conf['delays'][-left]
@@ -580,13 +583,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
delay = 1
return delay*60
- def _lrnConf(self, card):
+ def _lrnConf(self, card) -> Any:
if card.type == 2:
return self._lapseConf(card)
else:
return self._newConf(card)
- def _rescheduleAsRev(self, card, conf, early):
+ def _rescheduleAsRev(self, card, conf, early) -> None:
lapse = card.type == 2
if lapse:
if self._resched(card):
@@ -609,7 +612,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.queue = card.type = 0
card.due = self.col.nextID("pos")
- def _startingLeft(self, card):
+ def _startingLeft(self, card) -> int:
if card.type == 2:
conf = self._lapseConf(card)
else:
@@ -618,7 +621,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
tod = self._leftToday(conf['delays'], tot)
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."
if not now:
now = intTime()
@@ -631,7 +634,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
ok = i
return ok+1
- def _graduatingIvl(self, card, conf, early, adj=True):
+ def _graduatingIvl(self, card, conf, early, adj=True) -> Any:
if card.type == 2:
# lapsed card being relearnt
if card.odid:
@@ -649,13 +652,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
else:
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."
card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today+card.ivl
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))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log():
@@ -670,7 +673,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
time.sleep(0.01)
log()
- def removeLrn(self, ids=None):
+ def removeLrn(self, ids=None) -> None:
"Remove cards from the learning queues."
if 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(
"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(
"""
select sum(left/1000) from
@@ -705,16 +708,16 @@ and due <= ? limit ?)""",
# Reviews
##########################################################################
- def _deckRevLimit(self, did):
+ def _deckRevLimit(self, did) -> Any:
return self._deckNewLimit(did, self._deckRevLimitSingle)
- def _deckRevLimitSingle(self, d):
+ def _deckRevLimitSingle(self, d) -> Any:
if d['dyn']:
return self.reportLimit
c = self.col.decks.confForDid(d['id'])
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)
return self.col.db.scalar(
"""
@@ -723,7 +726,7 @@ select count() from
and due <= ? limit ?)""",
did, self.today, lim)
- def _resetRevCount(self):
+ def _resetRevCount(self) -> None:
def cntFn(did, lim):
return self.col.db.scalar("""
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._deckRevLimitSingle, cntFn)
- def _resetRev(self):
+ def _resetRev(self) -> None:
self._resetRevCount()
self._revQueue = []
self._revDids = self.col.decks.active()[:]
- def _fillRev(self):
+ def _fillRev(self) -> Any:
if self._revQueue:
return True
if not self.revCount:
@@ -774,12 +777,12 @@ did = ? and queue = 2 and due <= ? limit ?""",
self._resetRev()
return self._fillRev()
- def _getRevCard(self):
+ def _getRevCard(self) -> Any:
if self._fillRev():
self.revCount -= 1
return self.col.getCard(self._revQueue.pop())
- def totalRevForCurrentDeck(self):
+ def totalRevForCurrentDeck(self) -> Any:
return self.col.db.scalar(
"""
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
##########################################################################
- def _answerRevCard(self, card, ease):
+ def _answerRevCard(self, card, ease) -> None:
delay = 0
if ease == 1:
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._logRev(card, ease, delay)
- def _rescheduleLapse(self, card):
+ def _rescheduleLapse(self, card) -> Any:
conf = self._lapseConf(card)
card.lastIvl = card.ivl
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
return delay
- def _nextLapseIvl(self, card, conf):
+ def _nextLapseIvl(self, card, conf) -> Any:
return max(conf['minInt'], int(card.ivl*conf['mult']))
- def _rescheduleRev(self, card, ease):
+ def _rescheduleRev(self, card, ease) -> None:
# update interval
card.lastIvl = card.ivl
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.odue = 0
- def _logRev(self, card, ease, delay):
+ def _logRev(self, card, ease, delay) -> None:
def log():
self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
@@ -868,7 +871,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Interval management
##########################################################################
- def _nextRevIvl(self, card, ease):
+ def _nextRevIvl(self, card, ease) -> Any:
"Ideal next interval for CARD, given EASE."
delay = self._daysLate(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?
return min(interval, conf['maxIvl'])
- def _fuzzedIvl(self, ivl):
+ def _fuzzedIvl(self, ivl) -> int:
min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max)
- def _fuzzIvlRange(self, ivl):
+ def _fuzzIvlRange(self, ivl) -> List:
if ivl < 2:
return [1, 1]
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)
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."
new = ivl * conf.get('ivlFct', 1)
return int(max(new, prev+1))
- def _daysLate(self, card):
+ def _daysLate(self, card) -> Any:
"Number of days later than scheduled."
due = card.odue if card.odid else card.due
return max(0, self.today - due)
- def _updateRevIvl(self, card, ease):
+ def _updateRevIvl(self, card, ease) -> None:
idealIvl = self._nextRevIvl(card, ease)
card.ivl = min(max(self._adjRevIvl(card, idealIvl), card.ivl+1),
self._revConf(card)['maxIvl'])
- def _adjRevIvl(self, card, idealIvl):
+ def _adjRevIvl(self, card, idealIvl) -> int:
if self._spreadRev:
idealIvl = self._fuzzedIvl(idealIvl)
return idealIvl
@@ -928,7 +931,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Dynamic deck handling
##########################################################################
- def rebuildDyn(self, did=None):
+ def rebuildDyn(self, did=None) -> Any:
"Rebuild a dynamic deck."
did = did or self.col.decks.selected()
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)
return ids
- def _fillDyn(self, deck):
+ def _fillDyn(self, deck) -> Any:
search, limit, order = deck['terms'][0]
orderlimit = self._dynOrder(order, limit)
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)
return ids
- def emptyDyn(self, did, lim=None):
+ def emptyDyn(self, did, lim=None) -> None:
if not lim:
lim = "did = %s" % did
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,
self.col.usn())
- def remFromDyn(self, cids):
+ def remFromDyn(self, cids) -> None:
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:
t = "(select max(id) from revlog where cid=c.id)"
elif o == DYN_RANDOM:
@@ -997,7 +1000,7 @@ due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim,
t = "c.due"
return t + " limit %d" % l
- def _moveToDyn(self, did, ids):
+ def _moveToDyn(self, did, ids) -> None:
deck = self.col.decks.get(did)
data = []
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),
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.factor
elapsed = card.ivl - (card.odue - self.today)
@@ -1028,7 +1031,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Leeches
##########################################################################
- def _checkLeech(self, card, conf):
+ def _checkLeech(self, card, conf) -> Optional[bool]:
"Leech handler. True if card was a leech."
lf = conf['leechFails']
if not lf:
@@ -1057,10 +1060,10 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Tools
##########################################################################
- def _cardConf(self, card):
+ def _cardConf(self, card) -> Any:
return self.col.decks.confForDid(card.did)
- def _newConf(self, card):
+ def _newConf(self, card) -> Any:
conf = self._cardConf(card)
# normal deck
if not card.odid:
@@ -1080,7 +1083,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
perDay=self.reportLimit
)
- def _lapseConf(self, card):
+ def _lapseConf(self, card) -> Any:
conf = self._cardConf(card)
# normal deck
if not card.odid:
@@ -1099,7 +1102,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
resched=conf['resched'],
)
- def _revConf(self, card):
+ def _revConf(self, card) -> Any:
conf = self._cardConf(card)
# normal deck
if not card.odid:
@@ -1107,10 +1110,10 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# dynamic deck
return self.col.decks.confForDid(card.odid)['rev']
- def _deckLimit(self):
+ def _deckLimit(self) -> str:
return ids2str(self.col.decks.active())
- def _resched(self, card):
+ def _resched(self, card) -> Any:
conf = self._cardConf(card)
if not conf['dyn']:
return True
@@ -1119,7 +1122,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Daily cutoff
##########################################################################
- def _updateCutoff(self):
+ def _updateCutoff(self) -> None:
oldToday = self.today
# days since col created
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:
self.unburyCards()
- def _checkDay(self):
+ def _checkDay(self) -> None:
# check if the day has rolled over
if time.time() > self.dayCutoff:
self.reset()
@@ -1149,12 +1152,12 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
# Deck finished state
##########################################################################
- def finishedMsg(self):
+ def finishedMsg(self) -> str:
return ("
"+_(
"Congratulations! You have finished this deck for now.")+
"" + self._nextDueMsg())
- def _nextDueMsg(self):
+ def _nextDueMsg(self) -> str:
line = []
# the new line replacements are so we don't break translations
# 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."""))
return "
".join(line)
- def revDue(self):
+ def revDue(self) -> Any:
"True if there are any rev cards due."
return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 2 "
"and due <= ? limit 1") % self._deckLimit(),
self.today)
- def newDue(self):
+ def newDue(self) -> Any:
"True if there are any new cards due."
return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 0 "
"limit 1") % self._deckLimit())
- def haveBuried(self):
+ def haveBuried(self) -> bool:
sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar(
"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
##########################################################################
- def nextIvlStr(self, card, ease, short=False):
+ def nextIvlStr(self, card, ease, short=False) -> Any:
"Return the next interval for CARD as a string."
ivl = self.nextIvl(card, ease)
if not ivl:
@@ -1213,7 +1216,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
s = "<"+s
return s
- def nextIvl(self, card, ease):
+ def nextIvl(self, card, ease) -> Any:
"Return the next interval for CARD, in seconds."
if card.queue in (0,1,3):
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
# this isn't easily extracted from the learn code
- def _nextLrnIvl(self, card, ease):
+ def _nextLrnIvl(self, card, ease) -> Any:
if card.queue == 0:
card.left = self._startingLeft(card)
conf = self._lrnConf(card)
@@ -1253,7 +1256,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# Suspending
##########################################################################
- def suspendCards(self, ids):
+ def suspendCards(self, ids) -> None:
"Suspend cards."
self.col.log(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 "+
ids2str(ids), intTime(), self.col.usn())
- def unsuspendCards(self, ids):
+ def unsuspendCards(self, ids) -> None:
"Unsuspend cards."
self.col.log(ids)
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),
intTime(), self.col.usn())
- def buryCards(self, cids):
+ def buryCards(self, cids) -> None:
self.col.log(cids)
self.remFromDyn(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),
intTime(), self.col.usn())
- def buryNote(self, nid):
+ def buryNote(self, nid) -> None:
"Bury all cards for note until next session."
cids = self.col.db.list(
"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
##########################################################################
- def _burySiblings(self, card):
+ def _burySiblings(self, card) -> None:
toBury = []
nconf = self._newConf(card)
buryNew = nconf.get("bury", True)
@@ -1324,7 +1327,7 @@ and (queue=0 or (queue=2 and due<=?))""",
# Resetting
##########################################################################
- def forgetCards(self, ids):
+ def forgetCards(self, ids) -> None:
"Put cards at the end of the new queue."
self.remFromDyn(ids)
self.col.db.execute(
@@ -1336,7 +1339,7 @@ and (queue=0 or (queue=2 and due<=?))""",
self.sortCards(ids, start=pmax+1)
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)."
d = []
t = self.today
@@ -1352,7 +1355,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
d)
self.col.log(ids)
- def resetCards(self, ids):
+ def resetCards(self, ids) -> None:
"Completely reset cards for export."
sids = ids2str(ids)
# 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
##########################################################################
- 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)
now = intTime()
nids = []
@@ -1411,15 +1414,15 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.col.db.executemany(
"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)
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)
self.sortCards(cids)
- def resortConf(self, conf):
+ def resortConf(self, conf) -> None:
for did in self.col.decks.didsForConf(conf):
if conf['new']['order'] == 0:
self.randomizeCards(did)
@@ -1427,7 +1430,7 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.orderCards(did)
# for post-import
- def maybeRandomizeDeck(self, did=None):
+ def maybeRandomizeDeck(self, did=None) -> None:
if not did:
did = self.col.decks.selected()
conf = self.col.decks.confForDid(did)
diff --git a/anki/schedv2.py b/anki/schedv2.py
index 955e7bf03..34b21fec5 100644
--- a/anki/schedv2.py
+++ b/anki/schedv2.py
@@ -13,6 +13,7 @@ from anki.utils import ids2str, intTime, fmtTimeSpan
from anki.lang import _
from anki.consts import *
from anki.hooks import runHook
+from typing import Any, List, Optional, Tuple
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
@@ -26,7 +27,7 @@ class Scheduler:
haveCustomStudy = True
_burySiblingsOnAnswer = True
- def __init__(self, col):
+ def __init__(self, col) -> None:
self.col = col
self.queueLimit = 50
self.reportLimit = 1000
@@ -37,7 +38,7 @@ class Scheduler:
self._lrnCutoff = 0
self._updateCutoff()
- def getCard(self):
+ def getCard(self) -> Any:
"Pop the next card from the queue. None if finished."
self._checkDay()
if not self._haveQueues:
@@ -51,14 +52,14 @@ class Scheduler:
card.startTimer()
return card
- def reset(self):
+ def reset(self) -> None:
self._updateCutoff()
self._resetLrn()
self._resetRev()
self._resetNew()
self._haveQueues = True
- def answerCard(self, card, ease):
+ def answerCard(self, card, ease) -> None:
self.col.log()
assert 1 <= ease <= 4
assert 0 <= card.queue <= 4
@@ -73,7 +74,7 @@ class Scheduler:
card.usn = self.col.usn()
card.flushSched()
- def _answerCard(self, card, ease):
+ def _answerCard(self, card, ease) -> None:
if self._previewingCard(card):
self._answerCardPreview(card, ease)
return
@@ -103,7 +104,7 @@ class Scheduler:
if card.odue:
card.odue = 0
- def _answerCardPreview(self, card, ease):
+ def _answerCardPreview(self, card, ease) -> None:
assert 1 <= ease <= 2
if ease == 1:
@@ -116,14 +117,14 @@ class Scheduler:
self._restorePreviewCard(card)
self._removeFromFiltered(card)
- def counts(self, card=None):
+ def counts(self, card=None) -> tuple:
counts = [self.newCount, self.lrnCount, self.revCount]
if card:
idx = self.countIdx(card)
counts[idx] += 1
return tuple(counts)
- def dueForecast(self, days=7):
+ def dueForecast(self, days=7) -> List:
"Return counts over next DAYS. Includes today."
daysd = dict(self.col.db.all("""
select due, count() from cards
@@ -141,12 +142,12 @@ order by due""" % self._deckLimit(),
ret = [x[1] for x in sorted(daysd.items())]
return ret
- def countIdx(self, card):
+ def countIdx(self, card) -> Any:
if card.queue in (3,4):
return 1
return card.queue
- def answerButtons(self, card):
+ def answerButtons(self, card) -> int:
conf = self._cardConf(card)
if card.odid and not conf['resched']:
return 2
@@ -155,7 +156,7 @@ order by due""" % self._deckLimit(),
# Rev/lrn/time daily stats
##########################################################################
- def _updateStats(self, card, type, cnt=1):
+ def _updateStats(self, card, type, cnt=1) -> None:
key = type+"Today"
for g in ([self.col.decks.get(card.did)] +
self.col.decks.parents(card.did)):
@@ -163,7 +164,7 @@ order by due""" % self._deckLimit(),
g[key][1] += cnt
self.col.decks.save(g)
- def extendLimits(self, new, rev):
+ def extendLimits(self, new, rev) -> None:
cur = self.col.decks.current()
parents = self.col.decks.parents(cur['id'])
children = [self.col.decks.get(did) for (name, did) in
@@ -174,7 +175,7 @@ order by due""" % self._deckLimit(),
g['revToday'][1] -= rev
self.col.decks.save(g)
- def _walkingCount(self, limFn=None, cntFn=None):
+ def _walkingCount(self, limFn=None, cntFn=None) -> Any:
tot = 0
pcounts = {}
# for each of the active decks
@@ -208,7 +209,7 @@ order by due""" % self._deckLimit(),
# Deck list
##########################################################################
- def deckDueList(self):
+ def deckDueList(self) -> List[list]:
"Returns [deckname, did, rev, lrn, new]"
self._checkDay()
self.col.decks.checkIntegrity()
@@ -245,10 +246,10 @@ order by due""" % self._deckLimit(),
lims[deck['name']] = [nlim, rlim]
return data
- def deckDueTree(self):
+ def deckDueTree(self) -> Any:
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
for g in grps:
g[0] = g[0].split("::")
@@ -257,7 +258,7 @@ order by due""" % self._deckLimit(),
# then run main function
return self._groupChildrenMain(grps)
- def _groupChildrenMain(self, grps):
+ def _groupChildrenMain(self, grps) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]:
tree = []
# group and recurse
def key(grp):
@@ -296,7 +297,7 @@ order by due""" % self._deckLimit(),
# Getting the next card
##########################################################################
- def _getCard(self):
+ def _getCard(self) -> Any:
"Return the next due card id, or None."
# learning card due?
c = self._getLrnCard()
@@ -338,19 +339,19 @@ order by due""" % self._deckLimit(),
# New cards
##########################################################################
- def _resetNewCount(self):
+ def _resetNewCount(self) -> None:
cntFn = lambda did, lim: self.col.db.scalar("""
select count() from (select 1 from cards where
did = ? and queue = 0 limit ?)""", did, lim)
self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn)
- def _resetNew(self):
+ def _resetNew(self) -> None:
self._resetNewCount()
self._newDids = self.col.decks.active()[:]
self._newQueue = []
self._updateNewCardRatio()
- def _fillNew(self):
+ def _fillNew(self) -> Any:
if self._newQueue:
return True
if not self.newCount:
@@ -374,12 +375,12 @@ did = ? and queue = 0 limit ?)""", did, lim)
self._resetNew()
return self._fillNew()
- def _getNewCard(self):
+ def _getNewCard(self) -> Any:
if self._fillNew():
self.newCount -= 1
return self.col.getCard(self._newQueue.pop())
- def _updateNewCardRatio(self):
+ def _updateNewCardRatio(self) -> None:
if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.newCount:
self.newCardModulus = (
@@ -390,7 +391,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
return
self.newCardModulus = 0
- def _timeForNewCard(self):
+ def _timeForNewCard(self) -> Optional[int]:
"True if it's time to display a new card when distributing."
if not self.newCount:
return False
@@ -401,7 +402,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
elif self.newCardModulus:
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:
fn = self._deckNewLimitSingle
sel = self.col.decks.get(did)
@@ -415,7 +416,7 @@ did = ? and queue = 0 limit ?)""", did, lim)
lim = min(rem, lim)
return lim
- def _newForDeck(self, did, lim):
+ def _newForDeck(self, did, lim) -> Any:
"New count for a single deck."
if not lim:
return 0
@@ -424,14 +425,14 @@ did = ? and queue = 0 limit ?)""", did, lim)
select count() from
(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."
if g['dyn']:
return self.dynReportLimit
c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1])
- def totalNewForCurrentDeck(self):
+ def totalNewForCurrentDeck(self) -> Any:
return self.col.db.scalar(
"""
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
- def _updateLrnCutoff(self, force):
+ def _updateLrnCutoff(self, force) -> bool:
nextCutoff = intTime() + self.col.conf['collapseTime']
if nextCutoff - self._lrnCutoff > 60 or force:
self._lrnCutoff = nextCutoff
return True
return False
- def _maybeResetLrn(self, force):
+ def _maybeResetLrn(self, force) -> None:
if self._updateLrnCutoff(force):
self._resetLrn()
- def _resetLrnCount(self):
+ def _resetLrnCount(self) -> None:
# sub-day
self.lrnCount = self.col.db.scalar("""
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
""" % (self._deckLimit()))
- def _resetLrn(self):
+ def _resetLrn(self) -> None:
self._updateLrnCutoff(force=True)
self._resetLrnCount()
self._lrnQueue = []
@@ -478,7 +479,7 @@ select count() from cards where did in %s and queue = 4
self._lrnDids = self.col.decks.active()[:]
# sub-day learning
- def _fillLrn(self):
+ def _fillLrn(self) -> Any:
if not self.lrnCount:
return False
if self._lrnQueue:
@@ -492,7 +493,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=cutoff)
self._lrnQueue.sort()
return self._lrnQueue
- def _getLrnCard(self, collapse=False):
+ def _getLrnCard(self, collapse=False) -> Any:
self._maybeResetLrn(force=collapse and self.lrnCount == 0)
if self._fillLrn():
cutoff = time.time()
@@ -505,7 +506,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=cutoff)
return card
# daily learning
- def _fillLrnDay(self):
+ def _fillLrnDay(self) -> Optional[bool]:
if not self.lrnCount:
return False
if self._lrnDayQueue:
@@ -529,12 +530,12 @@ did = ? and queue = 3 and due <= ? limit ?""",
# nothing left in the deck; move to next
self._lrnDids.pop(0)
- def _getLrnDayCard(self):
+ def _getLrnDayCard(self) -> Any:
if self._fillLrnDay():
self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop())
- def _answerLrnCard(self, card, ease):
+ def _answerLrnCard(self, card, ease) -> None:
conf = self._lrnConf(card)
if card.type in (2,3):
type = 2
@@ -565,11 +566,11 @@ did = ? and queue = 3 and due <= ? limit ?""",
self._logLrn(card, ease, conf, leaving, type, lastLeft)
- def _updateRevIvlOnFail(self, card, conf):
+ def _updateRevIvlOnFail(self, card, conf) -> None:
card.lastIvl = card.ivl
card.ivl = self._lapseIvl(card, conf)
- def _moveToFirstStep(self, card, conf):
+ def _moveToFirstStep(self, card, conf) -> Any:
card.left = self._startingLeft(card)
# relearning card?
@@ -578,18 +579,18 @@ did = ? and queue = 3 and due <= ? limit ?""",
return self._rescheduleLrnCard(card, conf)
- def _moveToNextStep(self, card, conf):
+ def _moveToNextStep(self, card, conf) -> None:
# decrement real left count and recalculate left today
left = (card.left % 1000) - 1
card.left = self._leftToday(conf['delays'], left)*1000 + left
self._rescheduleLrnCard(card, conf)
- def _repeatStep(self, card, conf):
+ def _repeatStep(self, card, conf) -> None:
delay = self._delayForRepeatingGrade(conf, card.left)
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?
if delay is None:
delay = self._delayForGrade(conf, card.left)
@@ -619,7 +620,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.queue = 3
return delay
- def _delayForGrade(self, conf, left):
+ def _delayForGrade(self, conf, left) -> Any:
left = left % 1000
try:
delay = conf['delays'][-left]
@@ -631,7 +632,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
delay = 1
return delay*60
- def _delayForRepeatingGrade(self, conf, left):
+ def _delayForRepeatingGrade(self, conf, left) -> Any:
# halfway between last and next
delay1 = self._delayForGrade(conf, left)
if len(conf['delays']) > 1:
@@ -641,13 +642,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
avg = (delay1+max(delay1, delay2))//2
return avg
- def _lrnConf(self, card):
+ def _lrnConf(self, card) -> Any:
if card.type in (2, 3):
return self._lapseConf(card)
else:
return self._newConf(card)
- def _rescheduleAsRev(self, card, conf, early):
+ def _rescheduleAsRev(self, card, conf, early) -> None:
lapse = card.type in (2,3)
if lapse:
@@ -659,14 +660,14 @@ did = ? and queue = 3 and due <= ? limit ?""",
if card.odid:
self._removeFromFiltered(card)
- def _rescheduleGraduatingLapse(self, card, early=False):
+ def _rescheduleGraduatingLapse(self, card, early=False) -> None:
if early:
card.ivl += 1
card.due = self.today+card.ivl
card.queue = 2
card.type = 2
- def _startingLeft(self, card):
+ def _startingLeft(self, card) -> int:
if card.type == 3:
conf = self._lapseConf(card)
else:
@@ -675,7 +676,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
tod = self._leftToday(conf['delays'], tot)
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."
if not now:
now = intTime()
@@ -688,7 +689,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
ok = i
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):
bonus = early and 1 or 0
return card.ivl + bonus
@@ -702,14 +703,14 @@ did = ? and queue = 3 and due <= ? limit ?""",
ideal = self._fuzzedIvl(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."
card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today+card.ivl
card.factor = conf['initialFactor']
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))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log():
@@ -724,7 +725,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
time.sleep(0.01)
log()
- def _lrnForDeck(self, did):
+ def _lrnForDeck(self, did) -> Any:
cnt = self.col.db.scalar(
"""
select count() from
@@ -740,11 +741,11 @@ and due <= ? limit ?)""",
# Reviews
##########################################################################
- def _currentRevLimit(self):
+ def _currentRevLimit(self) -> Any:
d = self.col.decks.get(self.col.decks.selected(), default=False)
return self._deckRevLimitSingle(d)
- def _deckRevLimitSingle(self, d, parentLimit=None):
+ def _deckRevLimitSingle(self, d, parentLimit=None) -> Any:
# invalid deck selected?
if not d:
return 0
@@ -765,7 +766,7 @@ and due <= ? limit ?)""",
lim = min(lim, self._deckRevLimitSingle(parent, parentLimit=lim))
return lim
- def _revForDeck(self, did, lim, childMap):
+ def _revForDeck(self, did, lim, childMap) -> Any:
dids = [did] + self.col.decks.childDids(did, childMap)
lim = min(lim, self.reportLimit)
return self.col.db.scalar(
@@ -775,18 +776,18 @@ select count() from
and due <= ? limit ?)""" % ids2str(dids),
self.today, lim)
- def _resetRevCount(self):
+ def _resetRevCount(self) -> None:
lim = self._currentRevLimit()
self.revCount = self.col.db.scalar("""
select count() from (select id from cards where
did in %s and queue = 2 and due <= ? limit %d)""" % (
ids2str(self.col.decks.active()), lim), self.today)
- def _resetRev(self):
+ def _resetRev(self) -> None:
self._resetRevCount()
self._revQueue = []
- def _fillRev(self):
+ def _fillRev(self) -> Any:
if self._revQueue:
return True
if not self.revCount:
@@ -813,12 +814,12 @@ limit ?""" % (ids2str(self.col.decks.active())),
self._resetRev()
return self._fillRev()
- def _getRevCard(self):
+ def _getRevCard(self) -> Any:
if self._fillRev():
self.revCount -= 1
return self.col.getCard(self._revQueue.pop())
- def totalRevForCurrentDeck(self):
+ def totalRevForCurrentDeck(self) -> Any:
return self.col.db.scalar(
"""
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
##########################################################################
- def _answerRevCard(self, card, ease):
+ def _answerRevCard(self, card, ease) -> None:
delay = 0
early = card.odid and (card.odue > self.today)
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)
- def _rescheduleLapse(self, card):
+ def _rescheduleLapse(self, card) -> Any:
conf = self._lapseConf(card)
card.lapses += 1
@@ -862,11 +863,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
return delay
- def _lapseIvl(self, card, conf):
+ def _lapseIvl(self, card, conf) -> Any:
ivl = max(1, conf['minInt'], int(card.ivl*conf['mult']))
return ivl
- def _rescheduleRev(self, card, ease, early):
+ def _rescheduleRev(self, card, ease, early) -> None:
# update interval
card.lastIvl = card.ivl
if early:
@@ -881,7 +882,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# card leaves filtered deck
self._removeFromFiltered(card)
- def _logRev(self, card, ease, delay, type):
+ def _logRev(self, card, ease, delay, type) -> None:
def log():
self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
@@ -898,7 +899,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Interval management
##########################################################################
- def _nextRevIvl(self, card, ease, fuzz):
+ def _nextRevIvl(self, card, ease, fuzz) -> int:
"Next review interval for CARD, given EASE."
delay = self._daysLate(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)
return ivl4
- def _fuzzedIvl(self, ivl):
+ def _fuzzedIvl(self, ivl) -> int:
min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max)
- def _fuzzIvlRange(self, ivl):
+ def _fuzzIvlRange(self, ivl) -> List:
if ivl < 2:
return [1, 1]
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)
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))
if fuzz:
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'])
return int(ivl)
- def _daysLate(self, card):
+ def _daysLate(self, card) -> Any:
"Number of days later than scheduled."
due = card.odue if card.odid else card.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)
- def _updateEarlyRevIvl(self, card, ease):
+ def _updateEarlyRevIvl(self, card, ease) -> None:
card.ivl = self._earlyReviewIvl(card, ease)
# 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.factor
assert ease > 1
@@ -997,7 +998,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# Dynamic deck handling
##########################################################################
- def rebuildDyn(self, did=None):
+ def rebuildDyn(self, did=None) -> Optional[int]:
"Rebuild a dynamic deck."
did = did or self.col.decks.selected()
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)
return cnt
- def _fillDyn(self, deck):
+ def _fillDyn(self, deck) -> int:
start = -100000
total = 0
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)
return total
- def emptyDyn(self, did, lim=None):
+ def emptyDyn(self, did, lim=None) -> None:
if not lim:
lim = "did = %s" % did
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.col.usn())
- def remFromDyn(self, cids):
+ def remFromDyn(self, cids) -> None:
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:
t = "(select max(id) from revlog where cid=c.id)"
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"
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)
data = []
u = self.col.usn()
@@ -1089,13 +1090,13 @@ where id = ?
""" % queue
self.col.db.executemany(query, data)
- def _removeFromFiltered(self, card):
+ def _removeFromFiltered(self, card) -> None:
if card.odid:
card.did = card.odid
card.odue = 0
card.odid = 0
- def _restorePreviewCard(self, card):
+ def _restorePreviewCard(self, card) -> None:
assert card.odid
card.due = card.odue
@@ -1113,7 +1114,7 @@ where id = ?
# Leeches
##########################################################################
- def _checkLeech(self, card, conf):
+ def _checkLeech(self, card, conf) -> Optional[bool]:
"Leech handler. True if card was a leech."
lf = conf['leechFails']
if not lf:
@@ -1136,10 +1137,10 @@ where id = ?
# Tools
##########################################################################
- def _cardConf(self, card):
+ def _cardConf(self, card) -> Any:
return self.col.decks.confForDid(card.did)
- def _newConf(self, card):
+ def _newConf(self, card) -> Any:
conf = self._cardConf(card)
# normal deck
if not card.odid:
@@ -1158,7 +1159,7 @@ where id = ?
perDay=self.reportLimit
)
- def _lapseConf(self, card):
+ def _lapseConf(self, card) -> Any:
conf = self._cardConf(card)
# normal deck
if not card.odid:
@@ -1176,7 +1177,7 @@ where id = ?
resched=conf['resched'],
)
- def _revConf(self, card):
+ def _revConf(self, card) -> Any:
conf = self._cardConf(card)
# normal deck
if not card.odid:
@@ -1184,20 +1185,20 @@ where id = ?
# dynamic deck
return self.col.decks.confForDid(card.odid)['rev']
- def _deckLimit(self):
+ def _deckLimit(self) -> str:
return ids2str(self.col.decks.active())
- def _previewingCard(self, card):
+ def _previewingCard(self, card) -> Any:
conf = self._cardConf(card)
return conf['dyn'] and not conf['resched']
- def _previewDelay(self, card):
+ def _previewDelay(self, card) -> Any:
return self._cardConf(card).get("previewDelay", 10)*60
# Daily cutoff
##########################################################################
- def _updateCutoff(self):
+ def _updateCutoff(self) -> None:
oldToday = self.today
# days since col created
self.today = self._daysSinceCreation()
@@ -1220,12 +1221,12 @@ where id = ?
self.unburyCards()
self.col.conf['lastUnburied'] = self.today
- def _checkDay(self):
+ def _checkDay(self) -> None:
# check if the day has rolled over
if time.time() > self.dayCutoff:
self.reset()
- def _dayCutoff(self):
+ def _dayCutoff(self) -> int:
rolloverTime = self.col.conf.get("rollover", 4)
if rolloverTime < 0:
rolloverTime = 24+rolloverTime
@@ -1237,7 +1238,7 @@ where id = ?
stamp = int(time.mktime(date.timetuple()))
return stamp
- def _daysSinceCreation(self):
+ def _daysSinceCreation(self) -> int:
startDate = datetime.datetime.fromtimestamp(self.col.crt)
startDate = startDate.replace(hour=self.col.conf.get("rollover", 4),
minute=0, second=0, microsecond=0)
@@ -1246,12 +1247,12 @@ where id = ?
# Deck finished state
##########################################################################
- def finishedMsg(self):
+ def finishedMsg(self) -> str:
return (""+_(
"Congratulations! You have finished this deck for now.")+
"
" + self._nextDueMsg())
- def _nextDueMsg(self):
+ def _nextDueMsg(self) -> str:
line = []
# the new line replacements are so we don't break translations
# 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."""))
return "
".join(line)
- def revDue(self):
+ def revDue(self) -> Any:
"True if there are any rev cards due."
return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 2 "
"and due <= ? limit 1") % self._deckLimit(),
self.today)
- def newDue(self):
+ def newDue(self) -> Any:
"True if there are any new cards due."
return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 0 "
"limit 1") % self._deckLimit())
- def haveBuriedSiblings(self):
+ def haveBuriedSiblings(self) -> bool:
sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar(
"select 1 from cards where queue = -2 and did in %s limit 1" % sdids)
return not not cnt
- def haveManuallyBuried(self):
+ def haveManuallyBuried(self) -> bool:
sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar(
"select 1 from cards where queue = -3 and did in %s limit 1" % sdids)
return not not cnt
- def haveBuried(self):
+ def haveBuried(self) -> bool:
return self.haveManuallyBuried() or self.haveBuriedSiblings()
# 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."
ivl = self.nextIvl(card, ease)
if not ivl:
@@ -1319,7 +1320,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
s = "<"+s
return s
- def nextIvl(self, card, ease):
+ def nextIvl(self, card, ease) -> Any:
"Return the next interval for CARD, in seconds."
# preview mode?
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
# this isn't easily extracted from the learn code
- def _nextLrnIvl(self, card, ease):
+ def _nextLrnIvl(self, card, ease) -> Any:
if card.queue == 0:
card.left = self._startingLeft(card)
conf = self._lrnConf(card)
@@ -1377,14 +1378,14 @@ else
end)
"""
- def suspendCards(self, ids):
+ def suspendCards(self, ids) -> None:
"Suspend cards."
self.col.log(ids)
self.col.db.execute(
"update cards set queue=-1,mod=?,usn=? where id in "+
ids2str(ids), intTime(), self.col.usn())
- def unsuspendCards(self, ids):
+ def unsuspendCards(self, ids) -> None:
"Unsuspend cards."
self.col.log(ids)
self.col.db.execute(
@@ -1392,27 +1393,27 @@ end)
"where queue = -1 and id in %s") % (self._restoreQueueSnippet, ids2str(ids)),
intTime(), self.col.usn())
- def buryCards(self, cids, manual=True):
+ def buryCards(self, cids, manual=True) -> None:
queue = manual and -3 or -2
self.col.log(cids)
self.col.db.execute("""
update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids),
queue, intTime(), self.col.usn())
- def buryNote(self, nid):
+ def buryNote(self, nid) -> None:
"Bury all cards for note until next session."
cids = self.col.db.list(
"select id from cards where nid = ? and queue >= 0", nid)
self.buryCards(cids)
- def unburyCards(self):
+ def unburyCards(self) -> None:
"Unbury all buried cards in all decks."
self.col.log(
self.col.db.list("select id from cards where queue in (-2, -3)"))
self.col.db.execute(
"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":
queue = "queue in (-2, -3)"
elif type == "manual":
@@ -1433,7 +1434,7 @@ update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids),
# Sibling spacing
##########################################################################
- def _burySiblings(self, card):
+ def _burySiblings(self, card) -> None:
toBury = []
nconf = self._newConf(card)
buryNew = nconf.get("bury", True)
@@ -1467,7 +1468,7 @@ and (queue=0 or (queue=2 and due<=?))""",
# Resetting
##########################################################################
- def forgetCards(self, ids):
+ def forgetCards(self, ids) -> None:
"Put cards at the end of the new queue."
self.remFromDyn(ids)
self.col.db.execute(
@@ -1479,7 +1480,7 @@ and (queue=0 or (queue=2 and due<=?))""",
self.sortCards(ids, start=pmax+1)
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)."
d = []
t = self.today
@@ -1495,7 +1496,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
d)
self.col.log(ids)
- def resetCards(self, ids):
+ def resetCards(self, ids) -> None:
"Completely reset cards for export."
sids = ids2str(ids)
# 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
##########################################################################
- 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)
now = intTime()
nids = []
@@ -1554,15 +1555,15 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.col.db.executemany(
"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)
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)
self.sortCards(cids)
- def resortConf(self, conf):
+ def resortConf(self, conf) -> None:
for did in self.col.decks.didsForConf(conf):
if conf['new']['order'] == 0:
self.randomizeCards(did)
@@ -1570,7 +1571,7 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
self.orderCards(did)
# for post-import
- def maybeRandomizeDeck(self, did=None):
+ def maybeRandomizeDeck(self, did=None) -> None:
if not did:
did = self.col.decks.selected()
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
##########################################################################
- def _emptyAllFiltered(self):
+ def _emptyAllFiltered(self) -> None:
self.col.db.execute("""
update cards set did = odid, queue = (case
when type = 1 then 0
@@ -1593,7 +1594,7 @@ else type end),
due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
self.col.usn())
- def _removeAllFromLearning(self, schedVer=2):
+ def _removeAllFromLearning(self, schedVer=2) -> None:
# remove review cards from relearning
if schedVer == 1:
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)"))
# v1 doesn't support buried/suspended (re)learning cards
- def _resetSuspendedLearning(self):
+ def _resetSuspendedLearning(self) -> None:
self.col.db.execute("""
update cards set type = (case
when type = 1 then 0
@@ -1624,15 +1625,15 @@ mod = %d, usn = %d
where queue < 0""" % (intTime(), self.col.usn()))
# 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())
# adding 'hard' in v2 scheduler means old ease entries need shifting
# 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)
- def moveToV1(self):
+ def moveToV1(self) -> None:
self._emptyAllFiltered()
self._removeAllFromLearning()
@@ -1640,7 +1641,7 @@ where queue < 0""" % (intTime(), self.col.usn()))
self._resetSuspendedLearning()
self._remapLearningAnswers("ease=ease-1 where ease in (3,4)")
- def moveToV2(self):
+ def moveToV2(self) -> None:
self._emptyAllFiltered()
self._removeAllFromLearning(schedVer=1)
self._remapLearningAnswers("ease=ease+1 where ease in (2,3)")
diff --git a/anki/sound.py b/anki/sound.py
index 72760b485..f98406ea7 100644
--- a/anki/sound.py
+++ b/anki/sound.py
@@ -5,7 +5,9 @@
import html
import re, sys, threading, time, subprocess, os, atexit
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.utils import tmpdir, isWin, isMac, isLin
from anki.lang import _
@@ -15,19 +17,19 @@ from anki.lang import _
_soundReg = r"\[sound:(.*?)\]"
-def playFromText(text):
+def playFromText(text) -> None:
for match in allSounds(text):
# filename is html encoded
match = html.unescape(match)
play(match)
-def allSounds(text):
+def allSounds(text) -> List:
return re.findall(_soundReg, text)
-def stripSounds(text):
+def stripSounds(text) -> str:
return re.sub(_soundReg, "", text)
-def hasSound(text):
+def hasSound(text) -> bool:
return re.search(_soundReg, text) is not None
# Packaged commands
@@ -35,7 +37,7 @@ def hasSound(text):
# return modified command array that points to bundled command, and return
# required environment
-def _packagedCmd(cmd):
+def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
cmd = cmd[:]
env = os.environ.copy()
if "LD_LIBRARY_PATH" in env:
@@ -76,7 +78,7 @@ if sys.platform == "win32":
else:
si = None
-def retryWait(proc):
+def retryWait(proc) -> Any:
# osx throws interrupted system call errors frequently
while 1:
try:
@@ -89,6 +91,10 @@ def retryWait(proc):
from anki.mpv import MPV, MPVBase
+_player: Optional[Callable[[Any], Any]]
+_queueEraser: Optional[Callable[[], Any]]
+_soundReg: str
+
mpvPath, mpvEnv = _packagedCmd(["mpv"])
class MpvManager(MPV):
@@ -101,28 +107,28 @@ class MpvManager(MPV):
"--input-media-keys=no",
]
- def __init__(self):
+ def __init__(self) -> None:
super().__init__(window_id=None, debug=False)
- def queueFile(self, file):
+ def queueFile(self, file) -> None:
runHook("mpvWillPlay", file)
path = os.path.join(os.getcwd(), file)
self.command("loadfile", path, "append-play")
- def clearQueue(self):
+ def clearQueue(self) -> None:
self.command("stop")
- def togglePause(self):
+ def togglePause(self) -> None:
self.set_property("pause", not self.get_property("pause"))
- def seekRelative(self, secs):
+ def seekRelative(self, secs) -> None:
self.command("seek", secs, "relative")
- def on_idle(self):
+ def on_idle(self) -> None:
runHook("mpvIdleHook")
-def setMpvConfigBase(base):
+def setMpvConfigBase(base) -> None:
mpvConfPath = os.path.join(base, "mpv.conf")
MpvManager.default_argv += [
"--no-config",
@@ -131,14 +137,14 @@ def setMpvConfigBase(base):
mpvManager = None
-def setupMPV():
+def setupMPV() -> None:
global mpvManager, _player, _queueEraser
mpvManager = MpvManager()
_player = mpvManager.queueFile
_queueEraser = mpvManager.clearQueue
atexit.register(cleanupMPV)
-def cleanupMPV():
+def cleanupMPV() -> None:
global mpvManager, _player, _queueEraser
if mpvManager:
mpvManager.close()
@@ -151,7 +157,7 @@ def cleanupMPV():
# if anki crashes, an old mplayer instance may be left lying around,
# which prevents renaming or deleting the profile
-def cleanupOldMplayerProcesses():
+def cleanupOldMplayerProcesses() -> None:
# pylint: disable=import-error
import psutil # pytype: disable=import-error
@@ -189,7 +195,7 @@ class MplayerMonitor(threading.Thread):
mplayer = None
deadPlayers: List[subprocess.Popen] = []
- def run(self):
+ def run(self) -> NoReturn:
global mplayerClear
self.mplayer = None
self.deadPlayers = []
@@ -244,7 +250,7 @@ class MplayerMonitor(threading.Thread):
return True
self.deadPlayers = [pl for pl in self.deadPlayers if clean(pl)]
- def kill(self):
+ def kill(self) -> None:
if not self.mplayer:
return
try:
@@ -255,7 +261,7 @@ class MplayerMonitor(threading.Thread):
pass
self.mplayer = None
- def startProcess(self):
+ def startProcess(self) -> subprocess.Popen:
try:
cmd = mplayerCmd + ["-slave", "-idle"]
cmd, env = _packagedCmd(cmd)
@@ -267,7 +273,7 @@ class MplayerMonitor(threading.Thread):
mplayerEvt.clear()
raise Exception("Did you install mplayer?")
-def queueMplayer(path):
+def queueMplayer(path) -> None:
ensureMplayerThreads()
if isWin and os.path.exists(path):
# mplayer on windows doesn't like the encoding, so we create a
@@ -284,13 +290,13 @@ def queueMplayer(path):
mplayerQueue.append(path)
mplayerEvt.set()
-def clearMplayerQueue():
+def clearMplayerQueue() -> None:
global mplayerClear, mplayerQueue
mplayerQueue = []
mplayerClear = True
mplayerEvt.set()
-def ensureMplayerThreads():
+def ensureMplayerThreads() -> None:
global mplayerManager
if not mplayerManager:
mplayerManager = MplayerMonitor()
@@ -302,7 +308,7 @@ def ensureMplayerThreads():
# clean up mplayer on exit
atexit.register(stopMplayer)
-def stopMplayer(*args):
+def stopMplayer(*args) -> None:
if not mplayerManager:
return
mplayerManager.kill()
@@ -326,7 +332,7 @@ except:
class _Recorder:
- def postprocess(self, encode=True):
+ def postprocess(self, encode=True) -> None:
self.encode = encode
for c in processingChain:
#print c
@@ -344,18 +350,18 @@ class _Recorder:
"Error running %s") %
" ".join(cmd))
- def cleanup(self):
+ def cleanup(self) -> None:
if os.path.exists(processingSrc):
os.unlink(processingSrc)
class PyAudioThreadedRecorder(threading.Thread):
- def __init__(self, startupDelay):
+ def __init__(self, startupDelay) -> None:
threading.Thread.__init__(self)
self.startupDelay = startupDelay
self.finish = False
- def run(self):
+ def run(self) -> Any:
chunk = 1024
p = pyaudio.PyAudio()
@@ -421,10 +427,10 @@ if not pyaudio:
_player = queueMplayer
_queueEraser = clearMplayerQueue
-def play(path):
+def play(path) -> None:
_player(path)
-def clearAudioQueue():
+def clearAudioQueue() -> None:
_queueEraser()
Recorder = PyAudioRecorder
diff --git a/anki/stats.py b/anki/stats.py
index 69df4b935..d9c98789c 100644
--- a/anki/stats.py
+++ b/anki/stats.py
@@ -8,6 +8,7 @@ import json
from anki.utils import fmtTimeSpan, ids2str
from anki.lang import _, ngettext
+from typing import Any, List, Tuple, Optional
# Card stats
@@ -15,12 +16,12 @@ from anki.lang import _, ngettext
class CardStats:
- def __init__(self, col, card):
+ def __init__(self, col, card) -> None:
self.col = col
self.card = card
self.txt = ""
- def report(self):
+ def report(self) -> str:
c = self.card
# pylint: disable=unnecessary-lambda
fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs)
@@ -65,24 +66,24 @@ class CardStats:
self.txt += ""
return self.txt
- def addLine(self, k, v):
+ def addLine(self, k, v) -> None:
self.txt += self.makeLine(k, v)
- def makeLine(self, k, v):
+ def makeLine(self, k, v) -> str:
txt = "
"
txt += "%s | %s |
" % (k, v)
return txt
- def date(self, tm):
+ def date(self, tm) -> str:
return time.strftime("%Y-%m-%d", time.localtime(tm))
- def time(self, tm):
- str = ""
+ def time(self, tm) -> str:
+ s = ""
if tm >= 60:
- str = fmtTimeSpan((tm/60)*60, short=True, point=-1, unit=1)
- if tm%60 != 0 or not str:
- str += fmtTimeSpan(tm%60, point=2 if not str else -1, short=True)
- return str
+ s = fmtTimeSpan((tm/60)*60, short=True, point=-1, unit=1)
+ if tm%60 != 0 or not s:
+ s += fmtTimeSpan(tm%60, point=2 if not s else -1, short=True)
+ return s
# Collection stats
##########################################################################
@@ -101,7 +102,7 @@ colSusp = "#ff0"
class CollectionStats:
- def __init__(self, col):
+ def __init__(self, col) -> None:
self.col = col
self._stats = None
self.type = 0
@@ -110,7 +111,7 @@ class CollectionStats:
self.wholeCollection = False
# assumes jquery & plot are available in document
- def report(self, type=0):
+ def report(self, type=0) -> str:
# 0=days, 1=weeks, 2=months
self.type = type
from .statsbg import bg
@@ -126,7 +127,7 @@ class CollectionStats:
txt += self._section(self.footer())
return "
%s" % txt
- def _section(self, txt):
+ def _section(self, txt) -> str:
return "
%s
" % txt
css = """
@@ -143,7 +144,7 @@ body {background-image: url(data:image/png;base64,%s); }
# Today stats
######################################################################
- def todayStats(self):
+ def todayStats(self) -> str:
b = self._title(_("Today"))
# studied today
lim = self._revlogLimit()
@@ -199,13 +200,13 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
# 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
if self.type == 0:
end, chunk = 31, 1
elif self.type == 1:
end, chunk = 52, 7
- elif self.type == 2:
+ else: # self.type == 2:
end = None
if self._deckAge(by) <= 100:
chunk = 1
@@ -215,7 +216,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
chunk = 31
return start, end, chunk
- def dueGraph(self):
+ def dueGraph(self) -> str:
start, end, chunk = self.get_start_end_chunk()
d = self._due(start, end, chunk)
yng = []
@@ -251,7 +252,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
txt += self._dueInfo(tot, len(totd)*chunk)
return txt
- def _dueInfo(self, tot, num):
+ def _dueInfo(self, tot, num) -> str:
i = []
self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot)
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)
return self._lineTbl(i)
- def _due(self, start=None, end=None, chunk=1):
+ def _due(self, start=None, end=None, chunk=1) -> Any:
lim = ""
if start is not None:
lim += " and due-:today >= %d" % start
@@ -283,7 +284,7 @@ group by day order by day""" % (self._limit(), lim),
# Added, reps and time spent
######################################################################
- def introductionGraph(self):
+ def introductionGraph(self) -> str:
start, days, chunk = self.get_start_end_chunk()
data = self._added(days, chunk)
if not data:
@@ -315,7 +316,7 @@ group by day order by day""" % (self._limit(), lim),
return txt
- def repsGraphs(self):
+ def repsGraphs(self) -> str:
start, days, chunk = self.get_start_end_chunk()
data = self._done(days, chunk)
if not data:
@@ -363,7 +364,7 @@ group by day order by day""" % (self._limit(), lim),
txt2 += rep
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)
tot = totd[-1][1]
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))
return self._lineTbl(i), int(tot)
- def _splitRepData(self, data, spec):
+ def _splitRepData(self, data, spec) -> Tuple[List[dict], List[Tuple[Any, Any]]]:
sep = {}
totcnt = {}
totd = {}
@@ -433,7 +434,7 @@ group by day order by day""" % (self._limit(), lim),
bars={'show': False}, lines=dict(show=True), stack=-n))
return (ret, alltot)
- def _added(self, num=7, chunk=1):
+ def _added(self, num=7, chunk=1) -> Any:
lims = []
if num is not None:
lims.append("id > %d" % (
@@ -454,7 +455,7 @@ count(id)
from cards %s
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 = []
if num is not None:
lims.append("id > %d" % (
@@ -490,7 +491,7 @@ group by day order by day""" % lim,
tf=tf,
chunk=chunk)
- def _daysStudied(self):
+ def _daysStudied(self) -> Any:
lims = []
num = self._periodDays()
if num:
@@ -516,7 +517,7 @@ group by day order by day)""" % lim,
# Intervals
######################################################################
- def ivlGraph(self):
+ def ivlGraph(self) -> str:
(ivls, all, avg, max_), chunk = self._ivls()
tot = 0
totd = []
@@ -545,7 +546,7 @@ group by day order by day)""" % lim,
self._line(i, _("Longest interval"), fmtTimeSpan(max_*86400))
return txt + self._lineTbl(i)
- def _ivls(self):
+ def _ivls(self) -> Tuple[list, int]:
start, end, chunk = self.get_start_end_chunk()
lim = "and grp <= %d" % end if end else ""
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
######################################################################
- def easeGraph(self):
+ def easeGraph(self) -> str:
# 3 + 4 + 4 + spaces on sides and middle = 15
# yng starts at 1+3+1 = 5
# 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)
return txt
- def _easeInfo(self, eases):
+ def _easeInfo(self, eases) -> str:
types = {0: [0, 0], 1: [0, 0], 2: [0,0]}
for (type, ease, cnt) in eases:
if ease == 1:
@@ -614,7 +615,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2""" %
"
".join(i) +
" | ")
- def _eases(self):
+ def _eases(self) -> Any:
lims = []
lim = self._revlogLimit()
if lim:
@@ -643,7 +644,7 @@ order by thetype, ease""" % (ease4repl, lim))
# Hourly retention
######################################################################
- def hourGraph(self):
+ def hourGraph(self) -> str:
data = self._hourRet()
if not data:
return ""
@@ -690,7 +691,7 @@ order by thetype, ease""" % (ease4repl, lim))
txt += _("Hours with less than 30 reviews are not shown.")
return txt
- def _hourRet(self):
+ def _hourRet(self) -> Any:
lim = self._revlogLimit()
if lim:
lim = " and " + lim
@@ -715,7 +716,7 @@ group by hour having count() > 30 order by hour""" % lim,
# Cards
######################################################################
- def cardGraph(self):
+ def cardGraph(self) -> str:
# graph data
div = self._cards()
d = []
@@ -749,7 +750,7 @@ when you answer "good" on a review.''')
info)
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".
colon = _(":")
if bold:
@@ -757,10 +758,10 @@ when you answer "good" on a review.''')
else:
i.append(("
%s%s | %s |
") % (a,colon,b))
- def _lineTbl(self, i):
+ def _lineTbl(self, i) -> str:
return "
"
- def _factors(self):
+ def _factors(self) -> Any:
return self.col.db.first("""
select
min(factor) / 10.0,
@@ -768,7 +769,7 @@ avg(factor) / 10.0,
max(factor) / 10.0
from cards where did in %s and queue = 2""" % self._limit())
- def _cards(self):
+ def _cards(self) -> Any:
return self.col.db.first("""
select
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
######################################################################
- def footer(self):
+ def footer(self) -> str:
b = "
"
b += _("Generated on %s") % time.asctime(time.localtime(time.time()))
b += "
"
@@ -801,7 +802,7 @@ from cards where did in %s""" % self._limit())
######################################################################
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:
conf = {}
# display settings
@@ -902,21 +903,21 @@ $(function () {
ylab=ylabel, ylab2=ylabel2,
data=json.dumps(data), conf=json.dumps(conf)))
- def _limit(self):
+ def _limit(self) -> Any:
if self.wholeCollection:
return ids2str([d['id'] for d in self.col.decks.all()])
return self.col.sched._deckLimit()
- def _revlogLimit(self):
+ def _revlogLimit(self) -> str:
if self.wholeCollection:
return ""
return ("cid in (select id from cards where did in %s)" %
ids2str(self.col.decks.active()))
- def _title(self, title, subtitle=""):
+ def _title(self, title, subtitle="") -> str:
return '%s
%s' % (title, subtitle)
- def _deckAge(self, by):
+ def _deckAge(self, by) -> int:
lim = self._revlogLimit()
if lim:
lim = " where " + lim
@@ -932,13 +933,13 @@ $(function () {
1, int(1+((self.col.sched.dayCutoff - (t/1000)) / 86400)))
return period
- def _periodDays(self):
+ def _periodDays(self) -> Optional[int]:
start, end, chunk = self.get_start_end_chunk()
if end is None:
return None
return end * chunk
- def _avgDay(self, tot, num, unit):
+ def _avgDay(self, tot, num, unit) -> str:
vals = []
try:
vals.append(_("%(a)0.1f %(b)s/day") % dict(a=tot/float(num), b=unit))
diff --git a/anki/stdmodels.py b/anki/stdmodels.py
index ba86fc72d..c96413a73 100644
--- a/anki/stdmodels.py
+++ b/anki/stdmodels.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+from typing import Dict, Any
from anki.lang import _
from anki.consts import MODEL_CLOZE
@@ -10,7 +11,7 @@ models = []
# Basic
##########################################################################
-def _newBasicModel(col, name=None):
+def _newBasicModel(col, name=None) -> Dict[str, Any]:
mm = col.models
m = mm.new(name or _("Basic"))
fm = mm.newField(_("Front"))
@@ -23,7 +24,7 @@ def _newBasicModel(col, name=None):
mm.addTemplate(m, t)
return m
-def addBasicModel(col):
+def addBasicModel(col) -> Dict[str, Any]:
m = _newBasicModel(col)
col.models.add(m)
return m
@@ -33,7 +34,7 @@ models.append((lambda: _("Basic"), addBasicModel))
# Basic w/ typing
##########################################################################
-def addBasicTypingModel(col):
+def addBasicTypingModel(col) -> Dict[str, Any]:
mm = col.models
m = _newBasicModel(col, _("Basic (type in the answer)"))
t = m['tmpls'][0]
@@ -47,7 +48,7 @@ models.append((lambda: _("Basic (type in the answer)"), addBasicTypingModel))
# Forward & Reverse
##########################################################################
-def _newForwardReverse(col, name=None):
+def _newForwardReverse(col, name=None) -> Dict[str, Any]:
mm = col.models
m = _newBasicModel(col, name or _("Basic (and reversed card)"))
t = mm.newTemplate(_("Card 2"))
@@ -56,7 +57,7 @@ def _newForwardReverse(col, name=None):
mm.addTemplate(m, t)
return m
-def addForwardReverse(col):
+def addForwardReverse(col) -> Dict[str, Any]:
m = _newForwardReverse(col)
col.models.add(m)
return m
@@ -66,7 +67,7 @@ models.append((lambda: _("Basic (and reversed card)"), addForwardReverse))
# Forward & Optional Reverse
##########################################################################
-def addForwardOptionalReverse(col):
+def addForwardOptionalReverse(col) -> Dict[str, Any]:
mm = col.models
m = _newForwardReverse(col, _("Basic (optional reversed card)"))
av = _("Add Reverse")
@@ -83,7 +84,7 @@ models.append((lambda: _("Basic (optional reversed card)"),
# Cloze
##########################################################################
-def addClozeModel(col):
+def addClozeModel(col) -> Dict[str, Any]:
mm = col.models
m = mm.new(_("Cloze"))
m['type'] = MODEL_CLOZE
diff --git a/anki/storage.py b/anki/storage.py
index 6375d6f1c..815ee7e72 100644
--- a/anki/storage.py
+++ b/anki/storage.py
@@ -14,8 +14,11 @@ from anki.collection import _Collection
from anki.consts import *
from anki.stdmodels import addBasicModel, addClozeModel, addForwardReverse, \
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."
assert path.endswith(".anki2")
path = os.path.abspath(path)
@@ -54,7 +57,7 @@ def Collection(path, lock=True, server=False, log=False):
col.lock()
return col
-def _upgradeSchema(db):
+def _upgradeSchema(db) -> Any:
ver = db.scalar("select ver from col")
if ver == SCHEMA_VERSION:
return ver
@@ -83,7 +86,7 @@ id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data from notes2""")
_updateIndices(db)
return ver
-def _upgrade(col, ver):
+def _upgrade(col, ver) -> None:
if ver < 3:
# new deck properties
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.db.execute("update col set ver = 11")
-def _upgradeClozeModel(col, m):
+def _upgradeClozeModel(col, m) -> None:
m['type'] = MODEL_CLOZE
# convert first template
t = m['tmpls'][0]
@@ -205,7 +208,7 @@ def _upgradeClozeModel(col, m):
# Creating a new collection
######################################################################
-def _createDB(db):
+def _createDB(db) -> int:
db.execute("pragma page_size = 4096")
db.execute("pragma legacy_file_format = 0")
db.execute("vacuum")
@@ -214,7 +217,7 @@ def _createDB(db):
db.execute("analyze")
return SCHEMA_VERSION
-def _addSchema(db, setColConf=True):
+def _addSchema(db, setColConf=True) -> None:
db.executescript("""
create table if not exists col (
id integer primary key,
@@ -291,7 +294,7 @@ values(1,0,0,%(s)s,%(v)s,0,0,0,'','{}','','','{}');
if setColConf:
_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.decks
g = copy.deepcopy(anki.decks.defaultDeck)
@@ -303,14 +306,14 @@ def _getColVars(db):
gc['id'] = 1
return g, gc, anki.collection.defaultConf.copy()
-def _addColVars(db, g, gc, c):
+def _addColVars(db, g, gc, c) -> None:
db.execute("""
update col set conf = ?, decks = ?, dconf = ?""",
json.dumps(c),
json.dumps({'1': g}),
json.dumps({'1': gc}))
-def _updateIndices(db):
+def _updateIndices(db) -> None:
"Add indices to the DB."
db.executescript("""
-- syncing
diff --git a/anki/sync.py b/anki/sync.py
index 48de878ef..ec75f4eca 100644
--- a/anki/sync.py
+++ b/anki/sync.py
@@ -16,6 +16,7 @@ from anki.utils import versionWithBuild
from .hooks import runHook
import anki
from .lang import ngettext
+from typing import Any, Dict, List, Optional, Tuple, Union
# syncing vars
HTTP_TIMEOUT = 90
@@ -30,7 +31,7 @@ class UnexpectedSchemaChange(Exception):
class Syncer:
- def __init__(self, col, server=None):
+ def __init__(self, col, server=None) -> None:
self.col = col
self.server = server
@@ -39,7 +40,7 @@ class Syncer:
self.maxUsn = 0
self.tablesLeft = []
- def sync(self):
+ def sync(self) -> str:
"Returns 'noChanges', 'fullSync', 'success', etc"
self.syncMsg = ""
self.uname = ""
@@ -138,14 +139,14 @@ class Syncer:
self.finish(mod)
return "success"
- def _forceFullSync(self):
+ def _forceFullSync(self) -> str:
# roll back and force full sync
self.col.rollback()
self.col.modSchema(False)
self.col.save()
return "sanityCheckFailed"
- def _gravesChunk(self, graves):
+ def _gravesChunk(self, graves: Dict) -> Tuple[Dict, Optional[Dict]]:
lim = 250
chunk = dict(notes=[], cards=[], decks=[])
for cat in "notes", "cards", "decks":
@@ -159,7 +160,7 @@ class Syncer:
return chunk, graves
return chunk, None
- def meta(self):
+ def meta(self) -> dict:
return dict(
mod=self.col.mod,
scm=self.col.scm,
@@ -170,7 +171,7 @@ class Syncer:
cont=True
)
- def changes(self):
+ def changes(self) -> dict:
"Bundle up small objects."
d = dict(models=self.getModels(),
decks=self.getDecks(),
@@ -180,7 +181,7 @@ class Syncer:
d['crt'] = self.col.crt
return d
- def mergeChanges(self, lchg, rchg):
+ def mergeChanges(self, lchg, rchg) -> None:
# then the other objects
self.mergeModels(rchg['models'])
self.mergeDecks(rchg['decks'])
@@ -192,7 +193,7 @@ class Syncer:
self.col.crt = rchg['crt']
self.prepareToChunk()
- def sanityCheck(self):
+ def sanityCheck(self) -> Union[list, str]:
if not self.col.basicCheck():
return "failed basic check"
for t in "cards", "notes", "revlog", "graves":
@@ -226,10 +227,10 @@ class Syncer:
len(self.col.decks.allConf()),
]
- def usnLim(self):
+ def usnLim(self) -> str:
return "usn = -1"
- def finish(self, mod=None):
+ def finish(self, mod: int) -> int:
self.col.ls = mod
self.col._usn = self.maxUsn + 1
# ensure we save the mod time even if no changes made
@@ -240,11 +241,11 @@ class Syncer:
# Chunked syncing
##########################################################################
- def prepareToChunk(self):
+ def prepareToChunk(self) -> None:
self.tablesLeft = ["revlog", "cards", "notes"]
self.cursor = None
- def cursorForTable(self, table):
+ def cursorForTable(self, table) -> Any:
lim = self.usnLim()
x = self.col.db.execute
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
from notes where %s""" % d)
- def chunk(self):
+ def chunk(self) -> dict:
buf = dict(done=False)
lim = 250
while self.tablesLeft and lim:
@@ -284,7 +285,7 @@ from notes where %s""" % d)
buf['done'] = True
return buf
- def applyChunk(self, chunk):
+ def applyChunk(self, chunk) -> None:
if "revlog" in chunk:
self.mergeRevlog(chunk['revlog'])
if "cards" in chunk:
@@ -295,7 +296,7 @@ from notes where %s""" % d)
# Deletions
##########################################################################
- def removed(self):
+ def removed(self) -> dict:
cards = []
notes = []
decks = []
@@ -316,7 +317,7 @@ from notes where %s""" % d)
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
self.col.server = True
@@ -333,14 +334,14 @@ from notes where %s""" % d)
# Models
##########################################################################
- def getModels(self):
+ def getModels(self) -> List:
mods = [m for m in self.col.models.all() if m['usn'] == -1]
for m in mods:
m['usn'] = self.maxUsn
self.col.models.save()
return mods
- def mergeModels(self, rchg):
+ def mergeModels(self, rchg) -> None:
for r in rchg:
l = self.col.models.get(r['id'])
# if missing locally or server is newer, update
@@ -358,7 +359,7 @@ from notes where %s""" % d)
# Decks
##########################################################################
- def getDecks(self):
+ def getDecks(self) -> List[list]:
decks = [g for g in self.col.decks.all() if g['usn'] == -1]
for g in decks:
g['usn'] = self.maxUsn
@@ -368,7 +369,7 @@ from notes where %s""" % d)
self.col.decks.save()
return [decks, dconf]
- def mergeDecks(self, rchg):
+ def mergeDecks(self, rchg) -> None:
for r in rchg[0]:
l = self.col.decks.get(r['id'], False)
# work around mod time being stored as string
@@ -390,7 +391,7 @@ from notes where %s""" % d)
# Tags
##########################################################################
- def getTags(self):
+ def getTags(self) -> List:
tags = []
for t, usn in self.col.tags.allItems():
if usn == -1:
@@ -399,18 +400,18 @@ from notes where %s""" % d)
self.col.tags.save()
return tags
- def mergeTags(self, tags):
+ def mergeTags(self, tags) -> None:
self.col.tags.register(tags, usn=self.maxUsn)
# Cards/notes/revlog
##########################################################################
- def mergeRevlog(self, logs):
+ def mergeRevlog(self, logs) -> None:
self.col.db.executemany(
"insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)",
logs)
- def newerRows(self, data, table, modIdx):
+ def newerRows(self, data, table, modIdx) -> List:
ids = (r[0] for r in data)
lmods = {}
for id, mod in self.col.db.execute(
@@ -424,13 +425,13 @@ from notes where %s""" % d)
self.col.log(table, data)
return update
- def mergeCards(self, cards):
+ def mergeCards(self, cards) -> None:
self.col.db.executemany(
"insert or replace into cards values "
"(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
self.newerRows(cards, "cards", 4))
- def mergeNotes(self, notes):
+ def mergeNotes(self, notes) -> None:
rows = self.newerRows(notes, "notes", 3)
self.col.db.executemany(
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
@@ -440,10 +441,10 @@ from notes where %s""" % d)
# Col config
##########################################################################
- def getConf(self):
+ def getConf(self) -> Any:
return self.col.conf
- def mergeConf(self, conf):
+ def mergeConf(self, conf) -> None:
self.col.conf = conf
# Wrapper for requests that tracks upload/download progress
@@ -454,22 +455,22 @@ class AnkiRequestsClient:
verify = True
timeout = 60
- def __init__(self):
+ def __init__(self) -> None:
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
headers['User-Agent'] = self._agentName()
return self.session.post(
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:
headers = {}
headers['User-Agent'] = self._agentName()
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()
buf = io.BytesIO()
@@ -478,7 +479,7 @@ class AnkiRequestsClient:
buf.write(chunk)
return buf.getvalue()
- def _agentName(self):
+ def _agentName(self) -> str:
from anki import version
return "Anki {}".format(version)
@@ -490,7 +491,7 @@ if os.environ.get("ANKI_NOVERIFYSSL"):
warnings.filterwarnings("ignore")
class _MonitoringFile(io.BufferedReader):
- def read(self, size=-1):
+ def read(self, size=-1) -> bytes:
data = io.BufferedReader.read(self, HTTP_BUF_SIZE)
runHook("httpSend", len(data))
return data
@@ -500,7 +501,7 @@ class _MonitoringFile(io.BufferedReader):
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.skey = checksum(str(random.random()))[:8]
self.client = client or AnkiRequestsClient()
@@ -508,14 +509,14 @@ class HttpSyncer:
self.hostNum = hostNum
self.prefix = "sync/"
- def syncURL(self):
+ def syncURL(self) -> str:
if devMode:
url = "https://l1sync.ankiweb.net/"
else:
url = SYNC_BASE % (self.hostNum or "")
return url + self.prefix
- def assertOk(self, resp):
+ def assertOk(self, resp) -> None:
# not using raise_for_status() as aqt expects this error msg
if resp.status_code != 200:
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
# 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"
bdry = b"--"+BOUNDARY
buf = io.BytesIO()
@@ -573,7 +574,7 @@ Content-Type: application/octet-stream\r\n\r\n""")
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)
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):
- def __init__(self, hkey, hostNum):
+ def __init__(self, hkey, hostNum) -> None:
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."
self.postVars = dict()
ret = self.req(
@@ -604,7 +605,7 @@ class RemoteServer(HttpSyncer):
self.hkey = json.loads(ret.decode("utf8"))['key']
return self.hkey
- def meta(self):
+ def meta(self) -> Any:
self.postVars = dict(
k=self.hkey,
s=self.skey,
@@ -618,31 +619,31 @@ class RemoteServer(HttpSyncer):
return
return json.loads(ret.decode("utf8"))
- def applyGraves(self, **kw):
+ def applyGraves(self, **kw) -> Any:
return self._run("applyGraves", kw)
- def applyChanges(self, **kw):
+ def applyChanges(self, **kw) -> Any:
return self._run("applyChanges", kw)
- def start(self, **kw):
+ def start(self, **kw) -> Any:
return self._run("start", kw)
- def chunk(self, **kw):
+ def chunk(self, **kw) -> Any:
return self._run("chunk", kw)
- def applyChunk(self, **kw):
+ def applyChunk(self, **kw) -> Any:
return self._run("applyChunk", kw)
- def sanityCheck2(self, **kw):
+ def sanityCheck2(self, **kw) -> Any:
return self._run("sanityCheck2", kw)
- def finish(self, **kw):
+ def finish(self, **kw) -> Any:
return self._run("finish", kw)
- def abort(self, **kw):
+ def abort(self, **kw) -> Any:
return self._run("abort", kw)
- def _run(self, cmd, data):
+ def _run(self, cmd, data) -> Any:
return json.loads(
self.req(cmd, io.BytesIO(json.dumps(data).encode("utf8"))).decode("utf8"))
@@ -651,7 +652,7 @@ class RemoteServer(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)
self.postVars = dict(
k=self.hkey,
@@ -659,7 +660,7 @@ class FullSyncer(HttpSyncer):
)
self.col = col
- def download(self):
+ def download(self) -> Optional[str]:
runHook("sync", "download")
localNotEmpty = self.col.db.scalar("select 1 from cards")
self.col.close()
@@ -683,7 +684,7 @@ class FullSyncer(HttpSyncer):
os.rename(tpath, self.col.path)
self.col = None
- def upload(self):
+ def upload(self) -> bool:
"True if upload successful."
runHook("sync", "upload")
# make sure it's ok before we try to upload
@@ -709,12 +710,12 @@ class FullSyncer(HttpSyncer):
class MediaSyncer:
- def __init__(self, col, server=None):
+ def __init__(self, col, server=None) -> None:
self.col = col
self.server = server
self.downloadCount = 0
- def sync(self):
+ def sync(self) -> Any:
# check if there have been any changes
runHook("sync", "findMedia")
self.col.log("findChanges")
@@ -824,7 +825,7 @@ class MediaSyncer:
self.col.media.forceResync()
return ret
- def _downloadFiles(self, fnames):
+ def _downloadFiles(self, fnames) -> None:
self.col.log("%d files to fetch"%len(fnames))
while fnames:
top = fnames[0:SYNC_ZIP_COUNT]
@@ -845,12 +846,12 @@ class MediaSyncer:
class RemoteMediaServer(HttpSyncer):
- def __init__(self, col, hkey, client, hostNum):
+ def __init__(self, col, hkey, client, hostNum) -> None:
self.col = col
HttpSyncer.__init__(self, hkey, client, hostNum=hostNum)
self.prefix = "msync/"
- def begin(self):
+ def begin(self) -> Any:
self.postVars = dict(
k=self.hkey,
v="ankidesktop,%s,%s"%(anki.version, platDesc())
@@ -861,7 +862,7 @@ class RemoteMediaServer(HttpSyncer):
return ret
# args: lastUsn
- def mediaChanges(self, **kw):
+ def mediaChanges(self, **kw) -> Any:
self.postVars = dict(
sk=self.skey,
)
@@ -869,20 +870,20 @@ class RemoteMediaServer(HttpSyncer):
self.req("mediaChanges", io.BytesIO(json.dumps(kw).encode("utf8"))))
# args: files
- def downloadFiles(self, **kw):
+ def downloadFiles(self, **kw) -> Any:
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
return self._dataOnly(
self.req("uploadChanges", io.BytesIO(zip), comp=0))
# args: local
- def mediaSanity(self, **kw):
+ def mediaSanity(self, **kw) -> Any:
return self._dataOnly(
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"))
if resp['err']:
self.col.log("error returned:%s"%resp['err'])
@@ -890,7 +891,7 @@ class RemoteMediaServer(HttpSyncer):
return resp['data']
# only for unit tests
- def mediatest(self, cmd):
+ def mediatest(self, cmd) -> Any:
self.postVars = dict(
k=self.hkey,
)
diff --git a/anki/tags.py b/anki/tags.py
index 60ddaa9ad..171b0a20e 100644
--- a/anki/tags.py
+++ b/anki/tags.py
@@ -14,21 +14,22 @@ import json
from anki.utils import intTime, ids2str
from anki.hooks import runHook
import re
+from typing import Any, List, Tuple
class TagManager:
# Registry save/load
#############################################################
- def __init__(self, col):
+ def __init__(self, col) -> None:
self.col = col
self.tags = {}
- def load(self, json_):
+ def load(self, json_) -> None:
self.tags = json.loads(json_)
self.changed = False
- def flush(self):
+ def flush(self) -> None:
if self.changed:
self.col.db.execute("update col set tags=?",
json.dumps(self.tags))
@@ -37,7 +38,7 @@ class TagManager:
# 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."
found = False
for t in tags:
@@ -48,10 +49,10 @@ class TagManager:
if found:
runHook("newTag")
- def all(self):
+ def all(self) -> List:
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."
# when called without an argument, the old list is cleared first.
if nids:
@@ -63,13 +64,13 @@ class TagManager:
self.register(set(self.split(
" ".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())
- def save(self):
+ def save(self) -> None:
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"
if not children:
query = basequery + " AND c.did=?"
@@ -85,7 +86,7 @@ class TagManager:
# 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."
newTags = self.split(tags)
if not newTags:
@@ -117,23 +118,23 @@ class TagManager:
"update notes set tags=:t,mod=:n,usn=:u where id = :id",
[fix(row) for row in res])
- def bulkRem(self, ids, tags):
+ def bulkRem(self, ids, tags) -> None:
self.bulkAdd(ids, tags, False)
# String-based utilities
##########################################################################
- def split(self, tags):
+ def split(self, tags) -> List:
"Parse a string and return a list of tags."
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."
if not tags:
return ""
return " %s " % " ".join(tags)
- def addToStr(self, addtags, tags):
+ def addToStr(self, addtags, tags) -> str:
"Add tags if they don't exist, and canonify."
currentTags = self.split(tags)
for tag in self.split(addtags):
@@ -141,7 +142,7 @@ class TagManager:
currentTags.append(tag)
return self.join(self.canonify(currentTags))
- def remFromStr(self, deltags, tags):
+ def remFromStr(self, deltags, tags) -> str:
"Delete tags if they exist."
def wildcard(pat, str):
pat = re.escape(pat).replace('\\*', '.*')
@@ -161,7 +162,7 @@ class TagManager:
# List-based utilities
##########################################################################
- def canonify(self, tagList):
+ def canonify(self, tagList) -> List:
"Strip duplicates, adjust case to match existing tags, and sort."
strippedTags = []
for t in tagList:
@@ -172,14 +173,14 @@ class TagManager:
strippedTags.append(s)
return sorted(set(strippedTags))
- def inList(self, tag, tags):
+ def inList(self, tag, tags) -> bool:
"True if TAG is in TAGS. Ignore case."
return tag.lower() in [t.lower() for t in tags]
# Sync handling
##########################################################################
- def beforeUpload(self):
+ def beforeUpload(self) -> None:
for k in list(self.tags.keys()):
self.tags[k] = 0
self.save()
diff --git a/anki/template/__init__.py b/anki/template/__init__.py
index 291b3e0e2..afb92d515 100644
--- a/anki/template/__init__.py
+++ b/anki/template/__init__.py
@@ -1,8 +1,9 @@
from .template import Template
from . import furigana; furigana.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.update(kwargs)
return Template(template, context).render()
diff --git a/anki/template/furigana.py b/anki/template/furigana.py
index 929b0817d..bef895c1d 100644
--- a/anki/template/furigana.py
+++ b/anki/template/furigana.py
@@ -5,11 +5,12 @@
import re
from anki.hooks import addHook
+from typing import Any, Callable
r = r' ?([^ >]+?)\[(.+?)\]'
ruby = r'\1'
-def noSound(repl):
+def noSound(repl) -> Callable[[Any], Any]:
def func(match):
if match.group(2).startswith("sound:"):
# return without modification
@@ -18,19 +19,19 @@ def noSound(repl):
return re.sub(r, repl, match.group(0))
return func
-def _munge(s):
+def _munge(s) -> Any:
return s.replace(" ", " ")
-def kanji(txt, *args):
+def kanji(txt, *args) -> str:
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))
-def furigana(txt, *args):
+def furigana(txt, *args) -> str:
return re.sub(r, noSound(ruby), _munge(txt))
-def install():
+def install() -> None:
addHook('fmod_kanji', kanji)
addHook('fmod_kana', kana)
addHook('fmod_furigana', furigana)
diff --git a/anki/template/hint.py b/anki/template/hint.py
index ad4e9e6b4..d2bfaf65a 100644
--- a/anki/template/hint.py
+++ b/anki/template/hint.py
@@ -5,7 +5,7 @@
from anki.hooks import addHook
from anki.lang import _
-def hint(txt, extra, context, tag, fullname):
+def hint(txt, extra, context, tag, fullname) -> str:
if not txt.strip():
return ""
# random id
@@ -16,5 +16,5 @@ onclick="this.style.display='none';document.getElementById('%s').style.display='
%s%s
""" % (domid, _("Show %s") % tag, domid, txt)
-def install():
+def install() -> None:
addHook('fmod_hint', hint)
diff --git a/anki/template/template.py b/anki/template/template.py
index ad53d47e9..2d6881ad6 100644
--- a/anki/template/template.py
+++ b/anki/template/template.py
@@ -1,11 +1,12 @@
import re
from anki.utils import stripHTML, stripHTMLMedia
from anki.hooks import runFilter
+from typing import Any, Callable, NoReturn, Optional
clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}"
modifiers = {}
-def modifier(symbol):
+def modifier(symbol) -> Callable[[Any], Any]:
"""Decorator for associating a function with a Mustache tag modifier.
@modifier('P')
@@ -20,7 +21,7 @@ def modifier(symbol):
return set_modifier
-def get_or_attr(obj, name, default=None):
+def get_or_attr(obj, name, default=None) -> Any:
try:
return obj[name]
except KeyError:
@@ -45,12 +46,12 @@ class Template:
# Closing tag delimiter
ctag = '}}'
- def __init__(self, template, context=None):
+ def __init__(self, template, context=None) -> None:
self.template = template
self.context = context or {}
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."""
template = template or self.template
context = context or self.context
@@ -61,7 +62,7 @@ class Template:
result = result.encode(encoding)
return result
- def compile_regexps(self):
+ def compile_regexps(self) -> None:
"""Compiles our section and tag regular expressions."""
tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) }
@@ -71,7 +72,7 @@ class Template:
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
self.tag_re = re.compile(tag % tags)
- def render_sections(self, template, context):
+ def render_sections(self, template, context) -> NoReturn:
"""Expands sections."""
while 1:
match = self.section_re.search(template)
@@ -104,7 +105,7 @@ class 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."""
repCount = 0
while 1:
@@ -130,16 +131,16 @@ class Template:
# {{{ functions just like {{ in anki
@modifier('{')
- def render_tag(self, tag_name, context):
+ def render_tag(self, tag_name, context) -> Any:
return self.render_unescaped(tag_name, context)
@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."""
return ''
@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."""
txt = get_or_attr(context, tag_name)
if txt is not None:
@@ -192,7 +193,7 @@ class Template:
return '{unknown field %s}' % tag_name
return txt
- def clozeText(self, txt, ord, type):
+ def clozeText(self, txt, ord, type) -> str:
reg = clozeReg
if not re.search(reg%ord, txt):
return ""
@@ -215,7 +216,7 @@ class Template:
return re.sub(reg%r"\d+", "\\2", txt)
# look for clozes wrapped in mathjax, and change {{cx to {{Cx
- def _removeFormattingFromMathjax(self, txt, ord):
+ def _removeFormattingFromMathjax(self, txt, ord) -> str:
opening = ["\\(", "\\["]
closing = ["\\)", "\\]"]
# flags in middle of expression deprecated
@@ -237,7 +238,7 @@ class Template:
return txt
@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."""
try:
self.otag, self.ctag = tag_name.split(' ')
diff --git a/anki/template/view.py b/anki/template/view.py
index 99110e071..29c8e0906 100644
--- a/anki/template/view.py
+++ b/anki/template/view.py
@@ -1,6 +1,7 @@
from .template import Template
import os.path
import re
+from typing import Any
class View:
# Path where this view's template(s) live
@@ -24,7 +25,7 @@ class View:
# do any decoding of the template.
template_encoding = None
- def __init__(self, template=None, context=None, **kwargs):
+ def __init__(self, template=None, context=None, **kwargs) -> None:
self.template = template
self.context = context or {}
@@ -36,7 +37,7 @@ class View:
if kwargs:
self.context.update(kwargs)
- def inherit_settings(self, view):
+ def inherit_settings(self, view) -> None:
"""Given another View, copies its settings."""
if view.template_path:
self.template_path = view.template_path
@@ -44,7 +45,7 @@ class View:
if view.template_name:
self.template_name = view.template_name
- def load_template(self):
+ def load_template(self) -> Any:
if self.template:
return self.template
@@ -65,7 +66,7 @@ class View:
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')
try:
template = f.read()
@@ -75,7 +76,7 @@ class View:
f.close()
return template
- def get_template_name(self, name=None):
+ def get_template_name(self, name=None) -> Any:
"""TemplatePartial => template_partial
Takes a string but defaults to using the current class' name or
the `template_name` attribute
@@ -91,16 +92,16 @@ class View:
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)
- def __getitem__(self, attr):
+ def __getitem__(self, attr) -> Any:
val = self.get(attr, None)
if not val:
raise KeyError("No such key.")
return val
- def get(self, attr, default):
+ def get(self, attr, default) -> Any:
attr = self.context.get(attr, getattr(self, attr, default))
if hasattr(attr, '__call__'):
@@ -108,9 +109,9 @@ class View:
else:
return attr
- def render(self, encoding=None):
+ def render(self, encoding=None) -> str:
template = self.load_template()
return Template(template, self).render(encoding=encoding)
- def __str__(self):
+ def __str__(self) -> str:
return self.render()
diff --git a/anki/utils.py b/anki/utils.py
index 10f091dbd..cec62d6e4 100644
--- a/anki/utils.py
+++ b/anki/utils.py
@@ -22,11 +22,14 @@ from anki.lang import _, ngettext
# some add-ons expect json to be in the utils module
import json # pylint: disable=unused-import
+from typing import Any, Optional, Tuple
+
+_tmpdir: Optional[str]
# Time handling
##############################################################################
-def intTime(scale=1):
+def intTime(scale=1) -> int:
"The time in integer seconds. Pass scale=1000 to get milliseconds."
return int(time.time()*scale)
@@ -48,7 +51,7 @@ inTimeTable = {
"seconds": lambda n: ngettext("in %s second", "in %s seconds", n),
}
-def shortTimeFmt(type):
+def shortTimeFmt(type) -> Any:
return {
#T: year is an abbreviation for year. %s is a number of years
"years": _("%sy"),
@@ -64,7 +67,7 @@ def shortTimeFmt(type):
"seconds": _("%ss"),
}[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')."
(type, point) = optimalPeriod(time, point, unit)
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}
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:
type = "seconds"
point -= 1
@@ -98,7 +101,7 @@ def optimalPeriod(time, point, unit):
point += 1
return (type, max(point, 0))
-def convertSecondsTo(seconds, type):
+def convertSecondsTo(seconds, type) -> Any:
if type == "seconds":
return seconds
elif type == "minutes":
@@ -113,7 +116,7 @@ def convertSecondsTo(seconds, type):
return seconds / 31536000
assert False
-def _pluralCount(time, point):
+def _pluralCount(time, point) -> int:
if point:
return 2
return math.floor(time)
@@ -121,12 +124,12 @@ def _pluralCount(time, point):
# Locale
##############################################################################
-def fmtPercentage(float_value, point=1):
+def fmtPercentage(float_value, point=1) -> str:
"Return float with percentage sign"
fmt = '%' + "0.%(b)df" % {'b': point}
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"
fmt = '%' + "0.%(b)df" % {'b': point}
return locale.format_string(fmt, float_value)
@@ -140,7 +143,7 @@ reTag = re.compile("(?s)<.*?>")
reEnts = re.compile(r"?\w+;")
reMedia = re.compile("(?i)
]+src=[\"']?([^\"'>]+)[\"']?[^>]*>")
-def stripHTML(s):
+def stripHTML(s) -> str:
s = reComment.sub("", s)
s = reStyle.sub("", s)
s = reScript.sub("", s)
@@ -148,12 +151,12 @@ def stripHTML(s):
s = entsToTxt(s)
return s
-def stripHTMLMedia(s):
+def stripHTMLMedia(s) -> Any:
"Strip HTML but keep media filenames"
s = reMedia.sub(" \\1 ", s)
return stripHTML(s)
-def minimizeHTML(s):
+def minimizeHTML(s) -> str:
"Correct Qt's verbose bold/underline/etc."
s = re.sub('(.*?)', '\\1',
s)
@@ -163,7 +166,7 @@ def minimizeHTML(s):
'\\1', s)
return s
-def htmlToTextLine(s):
+def htmlToTextLine(s) -> Any:
s = s.replace("
", " ")
s = s.replace("
", " ")
s = s.replace("", " ")
@@ -174,7 +177,7 @@ def htmlToTextLine(s):
s = s.strip()
return s
-def entsToTxt(html):
+def entsToTxt(html) -> str:
# entitydefs defines nbsp as \xa0 instead of a standard space, so we
# replace it first
html = html.replace(" ", " ")
@@ -198,7 +201,7 @@ def entsToTxt(html):
return text # leave as is
return reEnts.sub(fixup, html)
-def bodyClass(col, card):
+def bodyClass(col, card) -> str:
bodyclass = "card card%d" % (card.ord+1)
if col.conf.get("nightMode"):
bodyclass += " nightMode"
@@ -207,17 +210,17 @@ def bodyClass(col, card):
# IDs
##############################################################################
-def hexifyID(id):
+def hexifyID(id) -> str:
return "%x" % int(id)
-def dehexifyID(id):
+def dehexifyID(id) -> int:
return int(id, 16)
-def ids2str(ids):
+def ids2str(ids) -> str:
"""Given a list of integers, return a string '(int1,int2,...)'."""
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."
# be careful not to create multiple objects without flushing them, or they
# may share an ID.
@@ -226,7 +229,7 @@ def timestampID(db, table):
t += 1
return t
-def maxID(db):
+def maxID(db) -> Any:
"Return the first safe ID to use."
now = intTime(1000)
for tbl in "cards", "notes":
@@ -234,7 +237,7 @@ def maxID(db):
return now + 1
# used in ankiweb
-def base62(num, extra=""):
+def base62(num, extra="") -> str:
s = string; table = s.ascii_letters + s.digits + extra
buf = ""
while num:
@@ -243,19 +246,19 @@ def base62(num, extra=""):
return buf
_base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
-def base91(num):
+def base91(num) -> str:
# all printable characters minus quotes, backslash and separators
return base62(num, _base91_extra_chars)
-def guid64():
+def guid64() -> Any:
"Return a base91-encoded 64bit random number."
return base91(random.randint(0, 2**64-1))
# increment a guid by one, for note type conflicts
-def incGuid(guid):
+def incGuid(guid) -> str:
return _incGuid(guid[::-1])[::-1]
-def _incGuid(guid):
+def _incGuid(guid) -> str:
s = string; table = s.ascii_letters + s.digits + _base91_extra_chars
idx = table.index(guid[0])
if idx + 1 == len(table):
@@ -268,21 +271,21 @@ def _incGuid(guid):
# Fields
##############################################################################
-def joinFields(list):
+def joinFields(list) -> str:
return "\x1f".join(list)
-def splitFields(string):
+def splitFields(string) -> Any:
return string.split("\x1f")
# Checksums
##############################################################################
-def checksum(data):
+def checksum(data) -> str:
if isinstance(data, str):
data = data.encode("utf-8")
return sha1(data).hexdigest()
-def fieldChecksum(data):
+def fieldChecksum(data) -> int:
# 32 bit unsigned number from first 8 digits of sha1 hash
return int(checksum(stripHTMLMedia(data).encode("utf-8"))[:8], 16)
@@ -291,7 +294,7 @@ def fieldChecksum(data):
_tmpdir = None
-def tmpdir():
+def tmpdir() -> Any:
"A reusable temp folder which we clean out on each program invocation."
global _tmpdir
if not _tmpdir:
@@ -305,12 +308,12 @@ def tmpdir():
os.mkdir(_tmpdir)
return _tmpdir
-def tmpfile(prefix="", suffix=""):
+def tmpfile(prefix="", suffix="") -> Any:
(fd, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix)
os.close(fd)
return name
-def namedtmp(name, rm=True):
+def namedtmp(name, rm=True) -> Any:
"Return tmpdir+name. Deletes any existing file."
path = os.path.join(tmpdir(), name)
if rm:
@@ -330,7 +333,7 @@ def noBundledLibs():
if oldlpath is not None:
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."
# ensure we don't open a separate window for forking process on windows
if isWin:
@@ -372,7 +375,7 @@ devMode = os.getenv("ANKIDEV", "")
invalidFilenameChars = ":*?\"<>|"
-def invalidFilename(str, dirsep=True):
+def invalidFilename(str, dirsep=True) -> Optional[str]:
for c in invalidFilenameChars:
if c in str:
return c
@@ -383,7 +386,7 @@ def invalidFilename(str, dirsep=True):
elif str.strip().startswith("."):
return "."
-def platDesc():
+def platDesc() -> str:
# we may get an interrupted system call, so try this in a loop
n = 0
theos = "unknown"
@@ -410,9 +413,9 @@ def platDesc():
##############################################################################
class TimedLog:
- def __init__(self):
+ def __init__(self) -> None:
self._last = time.time()
- def log(self, s):
+ def log(self, s) -> None:
path, num, fn, y = traceback.extract_stack(limit=2)[0]
sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s))
self._last = time.time()
@@ -420,7 +423,7 @@ class TimedLog:
# Version
##############################################################################
-def versionWithBuild():
+def versionWithBuild() -> str:
from anki import version
try:
from anki.buildhash import build # type: ignore
diff --git a/aqt/main.py b/aqt/main.py
index f098adc6a..32c294bf8 100644
--- a/aqt/main.py
+++ b/aqt/main.py
@@ -13,6 +13,7 @@ from threading import Thread
from typing import Optional
from send2trash import send2trash
from anki.collection import _Collection
+from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import *
from anki.storage import Collection
from anki.utils import isWin, isMac, intTime, splitFields, ids2str, \
@@ -33,7 +34,7 @@ from aqt.qt import sip
from anki.lang import _, ngettext
class AnkiQt(QMainWindow):
- def __init__(self, app: QApplication, profileManager, opts, args):
+ def __init__(self, app: QApplication, profileManager: ProfileManagerType, opts, args):
QMainWindow.__init__(self)
self.state = "startup"
self.opts = opts