diff --git a/anki/cards.py b/anki/cards.py index 505108ced..3affbdeb3 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -7,7 +7,8 @@ import time from anki.hooks import runHook from anki.utils import intTime, timestampID, joinFields from anki.consts import * -from typing import Any, Optional +from anki.notes import Note +from typing import Any, Optional, Dict # Cards ########################################################################## @@ -21,6 +22,10 @@ from typing import Any, Optional # - lrn queue: integer timestamp class Card: + _qa: Optional[Dict[str,str]] + _note: Optional[Note] + timerStarted: Optional[float] + lastIvl: Optional[int] def __init__(self, col, id: Optional[int] = None) -> None: from anki.collection import _Collection @@ -133,10 +138,10 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", data = [self.id, f.id, m['id'], self.odid or self.did, self.ord, f.stringTags(), f.joinedFields(), self.flags] if browser: - args = (t.get('bqfmt'), t.get('bafmt')) + args = [t.get('bqfmt'), t.get('bafmt')] else: - args = tuple() - self._qa = self.col._renderQA(data, *args) + args = [] + self._qa = self.col._renderQA(data, *args) # type: ignore return self._qa def note(self, reload: bool = False) -> Any: @@ -176,6 +181,7 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", self.model(), joinFields(self.note().fields)) if self.ord not in ords: return True + return False def __repr__(self) -> str: d = dict(self.__dict__) diff --git a/anki/collection.py b/anki/collection.py index f6aab1ad1..a27a412c7 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -59,11 +59,20 @@ def timezoneOffset() -> int: else: return time.timezone//60 -from anki.schedv2 import Scheduler +from anki.sched import Scheduler as V1Scheduler +from anki.schedv2 import Scheduler as V2Scheduler # this is initialized by storage.Collection class _Collection: - sched: Scheduler - + db: Optional[DB] + sched: Union[V1Scheduler, V2Scheduler] + crt: int + mod: int + scm: int + dty: bool # no longer used + _usn: int + ls: int + conf: Dict[str, Any] + _undo: List[Any] def __init__(self, db: DB, server: bool = False, log: bool = False) -> None: self._debugLog = log @@ -111,11 +120,9 @@ class _Collection: def _loadScheduler(self) -> None: ver = self.schedVer() if ver == 1: - from anki.sched import Scheduler + self.sched = V1Scheduler(self) elif ver == 2: - from anki.schedv2 import Scheduler - - self.sched = Scheduler(self) + self.sched = V2Scheduler(self) def changeSchedulerVer(self, ver: int) -> None: if ver == self.schedVer(): @@ -126,8 +133,7 @@ class _Collection: self.modSchema(check=True) self.clearUndo() - from anki.schedv2 import Scheduler - v2Sched = Scheduler(self) + v2Sched = V2Scheduler(self) if ver == 1: v2Sched.moveToV1() @@ -149,14 +155,14 @@ class _Collection: self.dty, # no longer used self._usn, self.ls, - self.conf, + conf, models, decks, dconf, tags) = self.db.first(""" select crt, mod, scm, dty, usn, ls, conf, models, decks, dconf, tags from col""") - self.conf = json.loads(self.conf) + self.conf = json.loads(conf) # type: ignore self.models.load(models) self.decks.load(decks, dconf) self.tags.load(tags) @@ -197,6 +203,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", if time.time() - self._lastSave > 300: self.save() return True + return None def lock(self) -> None: # make sure we don't accidentally bump mod time @@ -361,13 +368,13 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", ok.append(t) return ok - def genCards(self, nids: List[int]) -> List: + def genCards(self, nids: List[int]) -> List[int]: "Generate cards for non-empty templates, return ids to remove." # build map of (nid,ord) so we don't create dupes snids = ids2str(nids) - have = {} - dids = {} - dues = {} + have: Dict[int,Dict[int, int]] = {} + dids: Dict[int,Optional[int]] = {} + dues: Dict[int,int] = {} for id, nid, ord, did, due, odue, odid, type in self.db.execute( "select id, nid, ord, did, due, odue, odid, type from cards where nid in "+snids): # existing cards @@ -514,8 +521,8 @@ select id from notes where id in %s and id not in (select nid from cards)""" % ids2str(nids)) self._remNotes(nids) - def emptyCids(self) -> List: - rem = [] + def emptyCids(self) -> List[int]: + rem: List[int] = [] for m in self.models.all(): rem += self.genCards(self.models.nids(m)) return rem @@ -592,7 +599,7 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)): fields['Card'] = template['name'] fields['c%d' % (data[4]+1)] = "1" # render q & a - d = dict(id=data[0]) + d: Dict[str,Any] = dict(id=data[0]) qfmt = qfmt or template['qfmt'] afmt = afmt or template['afmt'] for (type, format) in (("q", qfmt), ("a", afmt)): @@ -664,7 +671,7 @@ where c.nid == f.id self._startTime = time.time() self._startReps = self.sched.reps - def timeboxReached(self) -> Optional[Union[bool, Tuple[Any, int]]]: + def timeboxReached(self) -> Union[bool, Tuple[Any, int]]: "Return (elapsedTime, reps) if timebox reached, or False." if not self.conf['timeLim']: # timeboxing disabled @@ -672,6 +679,7 @@ where c.nid == f.id elapsed = time.time() - self._startTime if elapsed > self.conf['timeLim']: return (self.conf['timeLim'], self.sched.reps - self._startReps) + return False # Undo ########################################################################## @@ -694,7 +702,7 @@ where c.nid == f.id self._undoOp() def markReview(self, card: Card) -> None: - old = [] + old: List[Any] = [] if self._undo: if self._undo[0] == 1: old = self._undo[2] @@ -746,17 +754,17 @@ where c.nid == f.id # DB maintenance ########################################################################## - def basicCheck(self) -> Optional[bool]: + def basicCheck(self) -> bool: "Basic integrity check for syncing. True if ok." # cards without notes if self.db.scalar(""" select 1 from cards where nid not in (select id from notes) limit 1"""): - return + return False # notes without cards or models if self.db.scalar(""" select 1 from notes where id not in (select distinct nid from cards) or mid not in %s limit 1""" % ids2str(self.models.ids())): - return + return False # invalid ords for m in self.models.all(): # ignore clozes @@ -767,7 +775,7 @@ select 1 from cards where ord not in %s and nid in ( select id from notes where mid = ?) limit 1""" % ids2str([t['ord'] for t in m['tmpls']]), m['id']): - return + return False return True def fixIntegrity(self) -> Tuple[Any, bool]: diff --git a/anki/decks.py b/anki/decks.py index bf5c48c59..d8cc6cd6e 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -11,12 +11,11 @@ from anki.hooks import runHook from anki.consts import * from anki.lang import _ from anki.errors import DeckRenameError -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Set, Union # fixmes: # - make sure users can't set grad interval < 1 -from typing import Any, Dict, List, Optional, Union defaultDeck = { 'newToday': [0, 0], # currentDay, count 'revToday': [0, 0], @@ -92,6 +91,8 @@ defaultConf = { } class DeckManager: + decks: Dict[str, Any] + dconf: Dict[str, Any] # Registry save/load ############################################################# @@ -457,7 +458,7 @@ class DeckManager: def _checkDeckTree(self) -> None: decks = self.col.decks.all() decks.sort(key=operator.itemgetter('name')) - names = set() + names: Set[str] = set() for deck in decks: # two decks with the same name? @@ -527,7 +528,7 @@ class DeckManager: arr.append(did) gather(child, arr) - arr = [] + arr: List = [] gather(childMap[did], arr) return arr @@ -537,7 +538,7 @@ class DeckManager: # go through all decks, sorted by name for deck in sorted(self.all(), key=operator.itemgetter("name")): - node = {} + node: Dict[int, Any] = {} childMap[deck['id']] = node # add note to immediate parent @@ -552,7 +553,7 @@ class DeckManager: def parents(self, did: int, nameMap: Optional[Any] = None) -> List: "All parents of did." # get parent and grandparent names - parents = [] + parents: List[str] = [] for part in self.get(did)['name'].split("::")[:-1]: if not parents: parents.append(part) diff --git a/anki/exporting.py b/anki/exporting.py index f4f3ec4d3..b27a5bc24 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -202,6 +202,7 @@ class AnkiExporter(Exporter): if int(m['id']) in mids: self.dst.models.update(m) # decks + dids: List[int] if not self.did: dids = [] else: @@ -294,7 +295,7 @@ class AnkiPackageExporter(AnkiExporter): z.writestr("media", json.dumps(media)) z.close() - def doExport(self, z: ZipFile, path: str) -> Dict[str, str]: + def doExport(self, z: ZipFile, path: str) -> Dict[str, str]: # type: ignore # export into the anki2 file colfile = path.replace(".apkg", ".anki2") AnkiExporter.exportInto(self, colfile) diff --git a/anki/find.py b/anki/find.py index 0ad5d42c3..5772c1ef3 100644 --- a/anki/find.py +++ b/anki/find.py @@ -9,7 +9,7 @@ import unicodedata from anki.utils import ids2str, splitFields, joinFields, intTime, fieldChecksum, stripHTMLMedia from anki.consts import * from anki.hooks import * -from typing import Any, List, Optional, Tuple +from typing import Any, List, Optional, Tuple, Set # Find @@ -130,20 +130,20 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds def _where(self, tokens) -> Tuple[Any, Optional[List[str]]]: # state and query - s = dict(isnot=False, isor=False, join=False, q="", bad=False) - args = [] + s: Dict[str, Any] = dict(isnot=False, isor=False, join=False, q="", bad=False) + args: List[Any] = [] def add(txt, wrap=True): # failed command? if not txt: # if it was to be negated then we can just ignore it if s['isnot']: s['isnot'] = False - return + return None, None else: s['bad'] = True - return + return None, None elif txt == "skip": - return + return None, None # do we need a conjunction? if s['join']: if s['isor']: @@ -273,11 +273,14 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds (c.queue in (2,3) and c.due <= %d) or (c.queue = 1 and c.due <= %d)""" % ( self.col.sched.today, self.col.sched.dayCutoff) + else: + # unknown + return None def _findFlag(self, args) -> Optional[str]: (val, args) = args if not val or len(val)!=1 or val not in "01234": - return + return None val = int(val) mask = 2**3 - 1 return "(c.flags & %d) == %d" % (mask, val) @@ -289,13 +292,13 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds try: days = int(r[0]) except ValueError: - return + return None days = min(days, 31) # ease ease = "" if len(r) > 1: if r[1] not in ("1", "2", "3", "4"): - return + return None ease = "and ease=%s" % r[1] cutoff = (self.col.sched.dayCutoff - 86400*days)*1000 return ("c.id in (select cid from revlog where id>%d %s)" % @@ -306,7 +309,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds try: days = int(val) except ValueError: - return + return None cutoff = (self.col.sched.dayCutoff - 86400*days)*1000 return "c.id > %d" % cutoff @@ -315,7 +318,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds (val, args) = args m = re.match("(^.+?)(<=|>=|!=|=|<|>)(.+?$)", val) if not m: - return + return None prop, cmp, val = m.groups() prop = prop.lower() # pytype: disable=attribute-error # is val valid? @@ -325,10 +328,10 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds else: val = int(val) except ValueError: - return + return None # is prop valid? if prop not in ("due", "ivl", "reps", "lapses", "ease"): - return + return None # query q = [] if prop == "due": @@ -350,19 +353,19 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds def _findNids(self, args) -> Optional[str]: (val, args) = args if re.search("[^0-9,]", val): - return + return None return "n.id in (%s)" % val def _findCids(self, args) -> Optional[str]: (val, args) = args if re.search("[^0-9,]", val): - return + return None return "c.id in (%s)" % val def _findMid(self, args) -> Optional[str]: (val, args) = args if re.search("[^0-9]", val): - return + return None return "n.mid = %s" % val def _findModel(self, args) -> str: @@ -401,7 +404,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds if re.match("(?i)"+val, unicodedata.normalize("NFC", d['name'])): ids.update(dids(d['id'])) if not ids: - return + return None sids = ids2str(ids) return "c.did in %s or c.odid in %s" % (sids, sids) @@ -440,7 +443,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds mods[str(m['id'])] = (m, f['ord']) if not mods: # nothing has that field - return + return None # gather nids regex = re.escape(val).replace("_", ".").replace(re.escape("%"), ".*") nids = [] @@ -456,7 +459,7 @@ where mid in %s and flds like ? escape '\\'""" % ( if re.search("(?si)^"+regex+"$", strg): nids.append(id) except sre_constants.error: - return + return None if not nids: return "0" return "n.id in %s" % ids2str(nids) @@ -467,7 +470,7 @@ where mid in %s and flds like ? escape '\\'""" % ( try: mid, val = val.split(",", 1) except OSError: - return + return None csum = fieldChecksum(val) nids = [] for nid, flds in self.col.db.execute( @@ -531,7 +534,7 @@ def findReplace(col, nids, src, dst, regex=False, field=None, fold=True) -> int: return len(d) def fieldNames(col, downcase=True) -> List: - fields = set() + fields: Set[str] = set() for m in col.models.all(): for f in m['flds']: name=f['name'].lower() if downcase else f['name'] @@ -540,7 +543,7 @@ def fieldNames(col, downcase=True) -> List: return list(fields) def fieldNamesForNotes(col, nids) -> List: - fields = set() + fields: Set[str] = set() mids = col.db.list("select distinct mid from notes where id in %s" % ids2str(nids)) for mid in mids: model = col.models.get(mid) @@ -558,9 +561,9 @@ def findDupes(col, fieldName, search="") -> List[Tuple[Any, List]]: search = "("+search+") " search += "'%s:*'" % fieldName # go through notes - vals = {} + vals: Dict[str, List[int]] = {} dupes = [] - fields = {} + fields: Dict[int, int] = {} def ordForMid(mid): if mid not in fields: model = col.models.get(mid) diff --git a/anki/hooks.py b/anki/hooks.py index e8d283b21..f94d67368 100644 --- a/anki/hooks.py +++ b/anki/hooks.py @@ -14,33 +14,32 @@ automatically but can be called with _old(). """ import decorator -from typing import Dict, List, Callable, Any +from typing import List, Any, Callable, Dict # Hooks ############################################################################## -from typing import Callable, Dict, Union _hooks: Dict[str, List[Callable[..., Any]]] = {} def runHook(hook: str, *args) -> None: "Run all functions on hook." - hook = _hooks.get(hook, None) - if hook: - for func in hook: + hookFuncs = _hooks.get(hook, None) + if hookFuncs: + for func in hookFuncs: try: func(*args) except: - hook.remove(func) + hookFuncs.remove(func) raise def runFilter(hook: str, arg: Any, *args) -> Any: - hook = _hooks.get(hook, None) - if hook: - for func in hook: + hookFuncs = _hooks.get(hook, None) + if hookFuncs: + for func in hookFuncs: try: arg = func(arg, *args) except: - hook.remove(func) + hookFuncs.remove(func) raise return arg diff --git a/anki/lang.py b/anki/lang.py index 8d0281741..9715d644e 100644 --- a/anki/lang.py +++ b/anki/lang.py @@ -106,8 +106,8 @@ compatMap = { threadLocal = threading.local() # global defaults -currentLang = None -currentTranslation = None +currentLang: Any = None +currentTranslation: Any = None def localTranslation() -> Any: "Return the translation local to this thread, or the default." diff --git a/anki/latex.py b/anki/latex.py index 46a283955..41c1799a9 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -6,10 +6,8 @@ import re, os, shutil, html from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML from anki.hooks import addHook from anki.lang import _ -from typing import Any - - from typing import Any, Dict, List, Optional, Union + pngCommands = [ ["latex", "-interaction=nonstopmode", "tmp.tex"], ["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"] diff --git a/anki/media.py b/anki/media.py index 81386ba1c..40ab7187f 100644 --- a/anki/media.py +++ b/anki/media.py @@ -128,18 +128,19 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); def dir(self) -> Any: return self._dir - def _isFAT32(self) -> Optional[bool]: + def _isFAT32(self) -> bool: if not isWin: - return + return False # pylint: disable=import-error import win32api, win32file # pytype: disable=import-error try: name = win32file.GetVolumeNameForVolumeMountPoint(self._dir[:3]) except: # mapped & unmapped network drive; pray that it's not vfat - return + return False if win32api.GetVolumeInformation(name)[4].lower().startswith("fat"): return True + return False # Adding media ########################################################################## @@ -200,7 +201,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); def filesInStr(self, mid: Union[int, str], string: str, includeRemote: bool = False) -> List[str]: l = [] model = self.col.models.get(mid) - strings = [] + strings: List[str] = [] if model['type'] == MODEL_CLOZE and "{{c" in string: # if the field has clozes in it, we'll need to expand the # possibilities so we can render latex @@ -248,6 +249,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); return txt def escapeImages(self, string: str, unescape: bool = False) -> str: + fn: Callable if unescape: fn = urllib.parse.unquote else: @@ -458,7 +460,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); self.db.commit() def _changes(self) -> Tuple[List[Tuple[str, int]], List[str]]: - self.cache = {} + self.cache: Dict[str, Any] = {} for (name, csum, mod) in self.db.execute( "select fname, csum, mtime from media where csum is not null"): # previous entries may not have been in NFC form @@ -614,7 +616,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); # normalize name name = unicodedata.normalize("NFC", name) # save file - with open(name, "wb") as f: + with open(name, "wb") as f: # type: ignore f.write(data) # update db media.append((name, csum, self._mtime(name), 0)) diff --git a/anki/models.py b/anki/models.py index 96c3b8cea..a80298bbf 100644 --- a/anki/models.py +++ b/anki/models.py @@ -3,21 +3,19 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import copy, re, json -from typing import Dict, Any from anki.utils import intTime, joinFields, splitFields, ids2str,\ checksum from anki.lang import _ from anki.consts import * from anki.hooks import runHook import time -from typing import List, Optional, Tuple, Union +from typing import Tuple, Union, Any, Callable, Dict, List, Optional # Models ########################################################################## # - careful not to add any lists/dicts/etc here, as they aren't deep copied -from typing import Any, Callable, Dict, List, Optional defaultModel = { 'sortf': 0, 'did': 1, @@ -73,6 +71,7 @@ defaultTemplate = { } class ModelManager: + models: Dict[str, Any] # Saving/loading registry ############################################################# @@ -112,6 +111,7 @@ class ModelManager: from anki.stdmodels import addBasicModel addBasicModel(self.col) return True + return None # Retrieving and creating models ############################################################# diff --git a/anki/notes.py b/anki/notes.py index da8244c3d..2294ca810 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -8,6 +8,7 @@ from typing import List, Tuple from typing import Any, Optional class Note: + tags: List[str] def __init__(self, col, model: Optional[Any] = None, id: Optional[int] = None) -> None: assert not (model and id) @@ -34,12 +35,12 @@ class Note: self.mod, self.usn, self.tags, - self.fields, + fields, self.flags, self.data) = self.col.db.first(""" select guid, mid, mod, usn, tags, flds, flags, data from notes where id = ?""", self.id) - self.fields = splitFields(self.fields) + self.fields = splitFields(fields) self.tags = self.col.tags.split(self.tags) self._model = self.col.models.get(self.mid) self._fmap = self.col.models.fieldMap(self._model) diff --git a/anki/schedv2.py b/anki/schedv2.py index c893a4e96..591e19a73 100644 --- a/anki/schedv2.py +++ b/anki/schedv2.py @@ -23,7 +23,9 @@ from anki.hooks import runHook from anki.cards import Card #from anki.collection import _Collection -from typing import Any, Callable, Dict, List, Optional, Union, Tuple +from typing import Any, Callable, Dict, List, Optional, Union, Tuple, Set + + class Scheduler: name = "std2" haveCustomStudy = True @@ -35,7 +37,7 @@ class Scheduler: self.reportLimit = 1000 self.dynReportLimit = 99999 self.reps = 0 - self.today = None + self.today: Optional[int] = None self._haveQueues = False self._lrnCutoff = 0 self._updateCutoff() @@ -179,7 +181,7 @@ order by due""" % self._deckLimit(), def _walkingCount(self, limFn: Optional[Callable] = None, cntFn: Optional[Callable] = None) -> Any: tot = 0 - pcounts = {} + pcounts: Dict[int, int] = {} # for each of the active decks nameMap = self.col.decks.nameMap() for did in self.col.decks.active(): @@ -217,7 +219,7 @@ order by due""" % self._deckLimit(), self.col.decks.checkIntegrity() decks = self.col.decks.all() decks.sort(key=itemgetter('name')) - lims = {} + lims: Dict[str, List[int]] = {} data = [] def parent(name): parts = name.split("::") @@ -260,18 +262,18 @@ order by due""" % self._deckLimit(), # then run main function return self._groupChildrenMain(grps) - def _groupChildrenMain(self, grps: List[List[Union[List[str], int]]]) -> Tuple[Tuple[Any, Any, Any, Any, Any, Any], ...]: + def _groupChildrenMain(self, grps: Any) -> Any: tree = [] # group and recurse def key(grp): return grp[0][0] for (head, tail) in itertools.groupby(grps, key=key): - tail = list(tail) + tail = list(tail) # type: ignore did = None rev = 0 new = 0 lrn = 0 - children = [] + children: Any = [] for c in tail: if len(c[0]) == 1: # current node @@ -350,7 +352,7 @@ did = ? and queue = 0 limit ?)""", did, lim) def _resetNew(self) -> None: self._resetNewCount() self._newDids = self.col.decks.active()[:] - self._newQueue = [] + self._newQueue: List[int] = [] self._updateNewCardRatio() def _fillNew(self) -> Any: @@ -403,8 +405,11 @@ did = ? and queue = 0 limit ?)""", did, lim) return True elif self.newCardModulus: return self.reps and self.reps % self.newCardModulus == 0 + else: + # shouldn't reach + return False - def _deckNewLimit(self, did: int, fn: None = None) -> Any: + def _deckNewLimit(self, did: int, fn: Callable[[Dict[str, Any]], int] = None) -> Any: if not fn: fn = self._deckNewLimitSingle sel = self.col.decks.get(did) @@ -476,8 +481,8 @@ select count() from cards where did in %s and queue = 4 def _resetLrn(self) -> None: self._updateLrnCutoff(force=True) self._resetLrnCount() - self._lrnQueue = [] - self._lrnDayQueue = [] + self._lrnQueue: List[Tuple[int,int]] = [] + self._lrnDayQueue: List[int] = [] self._lrnDids = self.col.decks.active()[:] # sub-day learning @@ -531,6 +536,8 @@ did = ? and queue = 3 and due <= ? limit ?""", return True # nothing left in the deck; move to next self._lrnDids.pop(0) + # shouldn't reach here + return False def _getLrnDayCard(self) -> Any: if self._fillLrnDay(): @@ -678,14 +685,14 @@ did = ? and queue = 3 and due <= ? limit ?""", tod = self._leftToday(conf['delays'], tot) return tot + tod*1000 - def _leftToday(self, delays: Union[List[int], List[Union[float, int]]], left: int, now: None = None) -> int: + def _leftToday(self, delays: Union[List[int], List[Union[float, int]]], left: int, now: Optional[int] = None) -> int: "The number of steps that can be completed by the day cutoff." if not now: now = intTime() delays = delays[-left:] ok = 0 for i in range(len(delays)): - now += delays[i]*60 + now += int(delays[i]*60) if now > self.dayCutoff: break ok = i @@ -787,7 +794,7 @@ did in %s and queue = 2 and due <= ? limit %d)""" % ( def _resetRev(self) -> None: self._resetRevCount() - self._revQueue = [] + self._revQueue: List[int] = [] def _fillRev(self) -> Any: if self._revQueue: @@ -1009,7 +1016,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" self.emptyDyn(did) cnt = self._fillDyn(deck) if not cnt: - return + return None # and change to our new deck self.col.decks.select(did) return cnt @@ -1120,7 +1127,7 @@ where id = ? "Leech handler. True if card was a leech." lf = conf['leechFails'] if not lf: - return + return None # if over threshold or every half threshold reps after that if (card.lapses >= lf and (card.lapses-lf) % (max(lf // 2, 1)) == 0): @@ -1135,6 +1142,7 @@ where id = ? # notify UI runHook("leech", card) return True + return None # Tools ########################################################################## @@ -1521,7 +1529,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""", scids = ids2str(cids) now = intTime() nids = [] - nidsSet = set() + nidsSet: Set[int] = set() for id in cids: nid = self.col.db.scalar("select nid from cards where id = ?", id) if nid not in nidsSet: diff --git a/anki/sound.py b/anki/sound.py index f98406ea7..7ff23b5fb 100644 --- a/anki/sound.py +++ b/anki/sound.py @@ -67,6 +67,7 @@ processingChain = [ ] # don't show box on windows +si: Optional[Any] if sys.platform == "win32": si = subprocess.STARTUPINFO() # pytype: disable=module-attr try: @@ -93,7 +94,7 @@ from anki.mpv import MPV, MPVBase _player: Optional[Callable[[Any], Any]] _queueEraser: Optional[Callable[[], Any]] -_soundReg: str +mpvManager: Optional["MpvManager"] = None mpvPath, mpvEnv = _packagedCmd(["mpv"]) @@ -135,8 +136,6 @@ def setMpvConfigBase(base) -> None: "--include="+mpvConfPath, ] -mpvManager = None - def setupMPV() -> None: global mpvManager, _player, _queueEraser mpvManager = MpvManager() @@ -185,14 +184,12 @@ if isWin: cleanupOldMplayerProcesses() mplayerQueue: List[str] = [] -mplayerManager = None -mplayerReader = None mplayerEvt = threading.Event() mplayerClear = False class MplayerMonitor(threading.Thread): - mplayer = None + mplayer: Optional[subprocess.Popen] = None deadPlayers: List[subprocess.Popen] = [] def run(self) -> NoReturn: @@ -273,6 +270,8 @@ class MplayerMonitor(threading.Thread): mplayerEvt.clear() raise Exception("Did you install mplayer?") +mplayerManager: Optional[MplayerMonitor] = None + def queueMplayer(path) -> None: ensureMplayerThreads() if isWin and os.path.exists(path): @@ -326,7 +325,7 @@ try: PYAU_FORMAT = pyaudio.paInt16 PYAU_CHANNELS = 1 - PYAU_INPUT_INDEX = None + PYAU_INPUT_INDEX: Optional[int] = None except: pyaudio = None diff --git a/anki/stats.py b/anki/stats.py index d9c98789c..f5936e7b3 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -8,7 +8,7 @@ import json from anki.utils import fmtTimeSpan, ids2str from anki.lang import _, ngettext -from typing import Any, List, Tuple, Optional +from typing import Any, List, Tuple, Optional, Dict # Card stats @@ -253,7 +253,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000) return txt def _dueInfo(self, tot, num) -> str: - i = [] + i: List[str] = [] self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot) self._line(i, _("Average"), self._avgDay( tot, num, _("reviews"))) @@ -289,7 +289,7 @@ group by day order by day""" % (self._limit(), lim), data = self._added(days, chunk) if not data: return "" - conf = dict( + conf: Dict[str, Any] = dict( xaxis=dict(tickDecimals=0, max=0.5), yaxes=[dict(min=0), dict(position="right", min=0)]) if days is not None: @@ -309,7 +309,7 @@ group by day order by day""" % (self._limit(), lim), if not period: # base off date of earliest added card period = self._deckAge('add') - i = [] + i: List[str] = [] self._line(i, _("Total"), ngettext("%d card", "%d cards", tot) % tot) self._line(i, _("Average"), self._avgDay(tot, period, _("cards"))) txt += self._lineTbl(i) @@ -321,7 +321,7 @@ group by day order by day""" % (self._limit(), lim), data = self._done(days, chunk) if not data: return "" - conf = dict( + conf: Dict[str, Any] = dict( xaxis=dict(tickDecimals=0, max=0.5), yaxes=[dict(min=0), dict(position="right", min=0)]) if days is not None: @@ -371,7 +371,7 @@ group by day order by day""" % (self._limit(), lim), if not period: # base off earliest repetition date period = self._deckAge('review') - i = [] + i: List[str] = [] self._line(i, _("Days studied"), _("%(pct)d%% (%(x)s of %(y)s)") % dict( x=studied, y=period, pct=studied/float(period)*100), @@ -406,9 +406,9 @@ group by day order by day""" % (self._limit(), lim), return self._lineTbl(i), int(tot) def _splitRepData(self, data, spec) -> Tuple[List[dict], List[Tuple[Any, Any]]]: - sep = {} + sep: Dict[int, Any] = {} totcnt = {} - totd = {} + totd: Dict[int, Any] = {} alltot = [] allcnt = 0 for (n, col, lab) in spec: @@ -541,7 +541,7 @@ group by day order by day)""" % lim, ], conf=dict( xaxis=dict(min=-0.5, max=ivlmax+0.5), yaxes=[dict(), dict(position="right", max=105)])) - i = [] + i: List[str] = [] self._line(i, _("Average interval"), fmtTimeSpan(avg*86400)) self._line(i, _("Longest interval"), fmtTimeSpan(max_*86400)) return txt + self._lineTbl(i) @@ -565,7 +565,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2""" % # 3 + 4 + 4 + spaces on sides and middle = 15 # yng starts at 1+3+1 = 5 # mtr starts at 5+4+1 = 10 - d = {'lrn':[], 'yng':[], 'mtr':[]} + d: Dict[str, List] = {'lrn':[], 'yng':[], 'mtr':[]} types = ("lrn", "yng", "mtr") eases = self._eases() for (type, ease, cnt) in eases: @@ -651,7 +651,7 @@ order by thetype, ease""" % (ease4repl, lim)) shifted = [] counts = [] mcount = 0 - trend = [] + trend: List[Tuple[int,int]] = [] peak = 0 for d in data: hour = (d[0] - 4) % 24 @@ -727,7 +727,7 @@ group by hour having count() > 30 order by hour""" % lim, (_("Suspended+Buried"), colSusp))): d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col)) # text data - i = [] + i: List[str] = [] (c, f) = self.col.db.first(""" select count(id), count(distinct nid) from cards where did in %s """ % self._limit()) @@ -839,8 +839,8 @@ from cards where did in %s""" % self._limit()) elif type == "fill": conf['series']['lines'] = dict(show=True, fill=True) elif type == "pie": - width /= 2.3 - height *= 1.5 + width = int(float(width)/2.3) + height = int(float(height)*1.5) ylabel = "" conf['series']['pie'] = dict( show=True, diff --git a/anki/storage.py b/anki/storage.py index c0262bcc7..478f9a840 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -14,9 +14,7 @@ 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 - -_Collection: Type[_Collection] +from typing import Any, Dict, Tuple def Collection(path: str, lock: bool = True, server: bool = False, log: bool = False) -> _Collection: "Open a new or existing collection. Path must be unicode." diff --git a/anki/sync.py b/anki/sync.py index ec75f4eca..83bf8b6b0 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -8,6 +8,7 @@ import random import requests import json import os +import sqlite3 from anki.db import DB, DBError from anki.utils import ids2str, intTime, platDesc, checksum, devMode @@ -20,7 +21,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union # syncing vars HTTP_TIMEOUT = 90 -HTTP_PROXY = None HTTP_BUF_SIZE = 64*1024 class UnexpectedSchemaChange(Exception): @@ -30,6 +30,7 @@ class UnexpectedSchemaChange(Exception): ########################################################################## class Syncer: + cursor: Optional[sqlite3.Cursor] def __init__(self, col, server=None) -> None: self.col = col @@ -38,7 +39,7 @@ class Syncer: # these are set later; provide dummy values for type checking self.lnewer = False self.maxUsn = 0 - self.tablesLeft = [] + self.tablesLeft: List[str] = [] def sync(self) -> str: "Returns 'noChanges', 'fullSync', 'success', etc" @@ -148,7 +149,7 @@ class Syncer: def _gravesChunk(self, graves: Dict) -> Tuple[Dict, Optional[Dict]]: lim = 250 - chunk = dict(notes=[], cards=[], decks=[]) + chunk: Dict[str, Any] = dict(notes=[], cards=[], decks=[]) for cat in "notes", "cards", "decks": if lim and graves[cat]: chunk[cat] = graves[cat][:lim] @@ -245,7 +246,7 @@ class Syncer: self.tablesLeft = ["revlog", "cards", "notes"] self.cursor = None - def cursorForTable(self, table) -> Any: + def cursorForTable(self, table) -> sqlite3.Cursor: lim = self.usnLim() x = self.col.db.execute d = (self.maxUsn, lim) @@ -263,7 +264,7 @@ select id, guid, mid, mod, %d, tags, flds, '', '', flags, data from notes where %s""" % d) def chunk(self) -> dict: - buf = dict(done=False) + buf: Dict[str, Any] = dict(done=False) lim = 250 while self.tablesLeft and lim: curTable = self.tablesLeft[0] @@ -505,7 +506,7 @@ class HttpSyncer: self.hkey = hkey self.skey = checksum(str(random.random()))[:8] self.client = client or AnkiRequestsClient() - self.postVars = {} + self.postVars: Dict[str,str] = {} self.hostNum = hostNum self.prefix = "sync/" @@ -532,7 +533,7 @@ class HttpSyncer: bdry = b"--"+BOUNDARY buf = io.BytesIO() # post vars - self.postVars['c'] = 1 if comp else 0 + self.postVars['c'] = "1" if comp else "0" for (key, value) in list(self.postVars.items()): buf.write(bdry + b"\r\n") buf.write( @@ -550,7 +551,7 @@ Content-Type: application/octet-stream\r\n\r\n""") if comp: tgt = gzip.GzipFile(mode="wb", fileobj=buf, compresslevel=comp) else: - tgt = buf + tgt = buf # type: ignore while 1: data = fobj.read(65536) if not data: @@ -668,7 +669,7 @@ class FullSyncer(HttpSyncer): tpath = self.col.path + ".tmp" if cont == "upgradeRequired": runHook("sync", "upgradeRequired") - return + return None open(tpath, "wb").write(cont) # check the received file is ok d = DB(tpath) @@ -683,6 +684,7 @@ class FullSyncer(HttpSyncer): os.unlink(self.col.path) os.rename(tpath, self.col.path) self.col = None + return None def upload(self) -> bool: "True if upload successful." diff --git a/anki/tags.py b/anki/tags.py index 171b0a20e..93eb7c43e 100644 --- a/anki/tags.py +++ b/anki/tags.py @@ -14,7 +14,8 @@ import json from anki.utils import intTime, ids2str from anki.hooks import runHook import re -from typing import Any, List, Tuple +from typing import Any, List, Tuple, Callable, Dict + class TagManager: @@ -23,7 +24,7 @@ class TagManager: def __init__(self, col) -> None: self.col = col - self.tags = {} + self.tags: Dict[str, int] = {} def load(self, json_) -> None: self.tags = json.loads(json_) @@ -95,6 +96,7 @@ class TagManager: if add: self.register(newTags) # find notes missing the tags + fn: Callable[[str, str], str] if add: l = "tags not " fn = self.addToStr diff --git a/anki/template/template.py b/anki/template/template.py index 2d6881ad6..a39afe573 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -1,11 +1,11 @@ import re from anki.utils import stripHTML, stripHTMLMedia from anki.hooks import runFilter -from typing import Any, Callable, NoReturn, Optional +from typing import Any, Callable, Pattern, Dict clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}" -modifiers = {} +modifiers: Dict[str, Callable] = {} def modifier(symbol) -> Callable[[Any], Any]: """Decorator for associating a function with a Mustache tag modifier. @@ -35,10 +35,10 @@ def get_or_attr(obj, name, default=None) -> Any: class Template: # The regular expression used to find a #section - section_re = None + section_re: Pattern = None # The regular expression used to find a tag. - tag_re = None + tag_re: Pattern = None # Opening tag delimiter otag = '{{' @@ -58,8 +58,8 @@ class Template: template = self.render_sections(template, context) result = self.render_tags(template, context) - if encoding is not None: - result = result.encode(encoding) + # if encoding is not None: + # result = result.encode(encoding) return result def compile_regexps(self) -> None: @@ -72,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) -> NoReturn: + def render_sections(self, template, context) -> str: """Expands sections.""" while 1: match = self.section_re.search(template) @@ -238,12 +238,12 @@ class Template: return txt @modifier('=') - def render_delimiter(self, tag_name=None, context=None) -> Optional[str]: + def render_delimiter(self, tag_name=None, context=None) -> str: """Changes the Mustache delimiter.""" try: self.otag, self.ctag = tag_name.split(' ') except ValueError: # invalid - return + return '' self.compile_regexps() return '' diff --git a/anki/template/view.py b/anki/template/view.py index 29c8e0906..1c3676767 100644 --- a/anki/template/view.py +++ b/anki/template/view.py @@ -12,18 +12,18 @@ class View: # The name of this template. If none is given the View will try # to infer it based on the class name. - template_name = None + template_name: str = None # Absolute path to the template itself. Pystache will try to guess # if it's not provided. - template_file = None + template_file: str = None # Contents of the template. - template = None + template: str = None # Character encoding of the template file. If None, Pystache will not # do any decoding of the template. - template_encoding = None + template_encoding: str = None def __init__(self, template=None, context=None, **kwargs) -> None: self.template = template diff --git a/anki/utils.py b/anki/utils.py index 61b1e1540..7dbdea4c8 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -22,10 +22,10 @@ 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 from anki.db import DB -from typing import Any, Iterator, List, Union +from typing import Any, Iterator, List, Union, Optional, Tuple + _tmpdir: Optional[str] # Time handling @@ -339,12 +339,12 @@ def call(argv: List[str], wait: bool = 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: - si = subprocess.STARTUPINFO() # pytype: disable=module-attr + si = subprocess.STARTUPINFO() # type: ignore try: - si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore except: # pylint: disable=no-member - si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr + si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore else: si = None # run @@ -387,6 +387,7 @@ def invalidFilename(str, dirsep=True) -> Optional[str]: return "\\" elif str.strip().startswith("."): return "." + return None def platDesc() -> str: # we may get an interrupted system call, so try this in a loop diff --git a/aqt/__init__.py b/aqt/__init__.py index d1a38fa0e..002b6f814 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -10,7 +10,7 @@ import tempfile import builtins import locale import gettext -from typing import Optional +from typing import Optional, Any from aqt.qt import * import anki.lang @@ -127,8 +127,8 @@ dialogs = DialogManager() # loaded, and we need the Qt language to match the gettext language or # translated shortcuts will not work. -_gtrans = None -_qtrans = None +_gtrans: Optional[Any] = None +_qtrans: Optional[QTranslator] = None def setupLang(pm, app, force=None): global _gtrans, _qtrans diff --git a/aqt/mediasrv.py b/aqt/mediasrv.py index e2f765985..8242ac574 100644 --- a/aqt/mediasrv.py +++ b/aqt/mediasrv.py @@ -1,7 +1,9 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import Optional +from anki.collection import _Collection from aqt.qt import * from http import HTTPStatus import http.server @@ -45,7 +47,7 @@ class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): class MediaServer(threading.Thread): - _port = None + _port: Optional[int] = None _ready = threading.Event() daemon = True @@ -69,7 +71,7 @@ class MediaServer(threading.Thread): class RequestHandler(http.server.SimpleHTTPRequestHandler): timeout = 1 - mw = None + mw: Optional[_Collection] = None def do_GET(self): f = self.send_head() diff --git a/aqt/utils.py b/aqt/utils.py index 799fdf113..9a4ac7d99 100644 --- a/aqt/utils.py +++ b/aqt/utils.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import Optional from aqt.qt import * import re, os, sys, subprocess @@ -424,8 +425,8 @@ def downArrow(): # Tooltips ###################################################################### -_tooltipTimer = None -_tooltipLabel = None +_tooltipTimer: Optional[QTimer] = None +_tooltipLabel: Optional[str] = None def tooltip(msg, period=3000, parent=None): global _tooltipTimer, _tooltipLabel diff --git a/mypy.ini b/mypy.ini index 69f2551cf..4f76feffc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,8 @@ [mypy] python_version = 3.6 pretty = true +no_strict_optional = true +show_error_codes = true [mypy-win32file] ignore_missing_imports = True