diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index f06bd038d..bd5795024 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -657,6 +657,7 @@ where c.nid = n.id and c.id in %s group by nid"""
self._startTime = time.time()
self._startReps = self.sched.reps
+ # FIXME: Use Literal[False] when on Python 3.8
def timeboxReached(self) -> Union[bool, Tuple[Any, int]]:
"Return (elapsedTime, reps) if timebox reached, or False."
if not self.conf["timeLim"]:
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index 316c33803..b02b96ab5 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -6,11 +6,12 @@ from __future__ import annotations
import difflib
import html
-import html.parser
import json
import re
import unicodedata as ucd
-from typing import List, Optional
+from typing import Callable, List, Optional, Sequence, Tuple, Union
+
+from PyQt5.QtCore import Qt
from anki import hooks
from anki.cards import Card
@@ -25,7 +26,7 @@ from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, toolti
class ReviewerBottomBar:
- def __init__(self, reviewer: Reviewer):
+ def __init__(self, reviewer: Reviewer) -> None:
self.reviewer = reviewer
@@ -39,28 +40,29 @@ class Reviewer:
self.cardQueue: List[Card] = []
self.hadCardQueue = False
self._answeredIds: List[int] = []
- self._recordedAudio = None
- self.typeCorrect = None # web init happens before this is set
+ self._recordedAudio: Optional[str] = None
+ self.typeCorrect: str = None # web init happens before this is set
self.state: Optional[str] = None
self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech)
- def show(self):
+ def show(self) -> None:
self.mw.col.reset()
- self.mw.setStateShortcuts(self._shortcutKeys())
+ self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore
self.web.set_bridge_command(self._linkHandler, self)
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
- self._reps = None
+ self._reps: int = None
self.nextCard()
- def lastCard(self):
+ def lastCard(self) -> Optional[Card]:
if self._answeredIds:
if not self.card or self._answeredIds[-1] != self.card.id:
try:
return self.mw.col.getCard(self._answeredIds[-1])
except TypeError:
# id was deleted
- return
+ return None
+ return None
def cleanup(self) -> None:
gui_hooks.reviewer_will_end()
@@ -68,9 +70,10 @@ class Reviewer:
# Fetching a card
##########################################################################
- def nextCard(self):
+ def nextCard(self) -> None:
elapsed = self.mw.col.timeboxReached()
if elapsed:
+ assert not isinstance(elapsed, bool)
part1 = (
ngettext("%d card studied in", "%d cards studied in", elapsed[1])
% elapsed[1]
@@ -125,7 +128,7 @@ class Reviewer:
# Initializing the webview
##########################################################################
- def revHtml(self):
+ def revHtml(self) -> str:
extra = self.mw.col.conf.get("reviewExtra", "")
fade = ""
if self.mw.pm.glMode() == "software":
@@ -140,7 +143,7 @@ class Reviewer:
fade, extra
)
- def _initWeb(self):
+ def _initWeb(self) -> None:
self._reps = 0
# main window
self.web.stdHtml(
@@ -167,13 +170,13 @@ class Reviewer:
# Showing the question
##########################################################################
- def _mungeQA(self, buf):
+ def _mungeQA(self, buf: str) -> str:
return self.typeAnsFilter(self.mw.prepare_card_text_for_display(buf))
def _showQuestion(self) -> None:
self._reps += 1
self.state = "question"
- self.typedAnswer = None
+ self.typedAnswer: str = None
c = self.card
# grab the question and play audio
if c.isEmpty():
@@ -206,17 +209,17 @@ The front of this card is empty. Please run Tools>Empty Cards."""
# user hook
gui_hooks.reviewer_did_show_question(c)
- def autoplay(self, card):
+ def autoplay(self, card: Card) -> bool:
return self.mw.col.decks.confForDid(card.odid or card.did)["autoplay"]
def _replayq(self, card, previewer=None):
s = previewer if previewer else self
return s.mw.col.decks.confForDid(s.card.odid or s.card.did).get("replayq", True)
- def _drawFlag(self):
+ def _drawFlag(self) -> None:
self.web.eval("_drawFlag(%s);" % self.card.userFlag())
- def _drawMark(self):
+ def _drawMark(self) -> None:
self.web.eval("_drawMark(%s);" % json.dumps(self.card.note().hasTag("marked")))
# Showing the answer
@@ -246,7 +249,7 @@ The front of this card is empty. Please run Tools>Empty Cards."""
# Answering a card
############################################################
- def _answerCard(self, ease):
+ def _answerCard(self, ease: int) -> None:
"Reschedule card and show next."
if self.mw.state != "review":
# showing resetRequired screen; ignore key
@@ -269,7 +272,9 @@ The front of this card is empty. Please run Tools>Empty Cards."""
# Handlers
############################################################
- def _shortcutKeys(self):
+ def _shortcutKeys(
+ self,
+ ) -> List[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
return [
("e", self.mw.onEditCurrent),
(" ", self.onEnterKey),
@@ -299,18 +304,18 @@ The front of this card is empty. Please run Tools>Empty Cards."""
("7", self.on_seek_forward),
]
- def on_pause_audio(self):
+ def on_pause_audio(self) -> None:
av_player.toggle_pause()
seek_secs = 5
- def on_seek_backward(self):
+ def on_seek_backward(self) -> None:
av_player.seek_relative(-self.seek_secs)
- def on_seek_forward(self):
+ def on_seek_forward(self) -> None:
av_player.seek_relative(self.seek_secs)
- def onEnterKey(self):
+ def onEnterKey(self) -> None:
if self.state == "question":
self._getTypedAnswer()
elif self.state == "answer":
@@ -318,14 +323,14 @@ The front of this card is empty. Please run Tools>Empty Cards."""
"selectedAnswerButton()", self._onAnswerButton
)
- def _onAnswerButton(self, val):
+ def _onAnswerButton(self, val: str) -> None:
# button selected?
if val and val in "1234":
self._answerCard(int(val))
else:
self._answerCard(self._defaultEase())
- def _linkHandler(self, url):
+ def _linkHandler(self, url: str) -> None:
if url == "ans":
self._getTypedAnswer()
elif url.startswith("ease"):
@@ -344,13 +349,13 @@ The front of this card is empty. Please run Tools>Empty Cards."""
typeAnsPat = r"\[\[type:(.+?)\]\]"
- def typeAnsFilter(self, buf):
+ def typeAnsFilter(self, buf: str) -> str:
if self.state == "question":
return self.typeAnsQuestionFilter(buf)
else:
return self.typeAnsAnswerFilter(buf)
- def typeAnsQuestionFilter(self, buf):
+ def typeAnsQuestionFilter(self, buf: str) -> str:
self.typeCorrect = None
clozeIdx = None
m = re.search(self.typeAnsPat, buf)
@@ -397,20 +402,19 @@ Please run Tools>Empty Cards"""
buf,
)
- def typeAnsAnswerFilter(self, buf):
+ def typeAnsAnswerFilter(self, buf: str) -> str:
if not self.typeCorrect:
return re.sub(self.typeAnsPat, "", buf)
origSize = len(buf)
buf = buf.replace("
", "")
hadHR = len(buf) != origSize
# munge correct value
- parser = html.parser.HTMLParser()
cor = self.mw.col.media.strip(self.typeCorrect)
cor = re.sub("(\n|
|?div>)+", " ", cor)
cor = stripHTML(cor)
# ensure we don't chomp multiple whitespace
cor = cor.replace(" ", " ")
- cor = parser.unescape(cor)
+ cor = html.unescape(cor)
cor = cor.replace("\xa0", " ")
cor = cor.strip()
given = self.typedAnswer
@@ -434,7 +438,7 @@ Please run Tools>Empty Cards"""
return re.sub(self.typeAnsPat, repl, buf)
- def _contentForCloze(self, txt, idx):
+ def _contentForCloze(self, txt: str, idx) -> str:
matches = re.findall(r"\{\{c%s::(.+?)\}\}" % idx, txt, re.DOTALL)
if not matches:
return None
@@ -452,24 +456,28 @@ Please run Tools>Empty Cards"""
txt = ", ".join(matches)
return txt
- def tokenizeComparison(self, given, correct):
+ def tokenizeComparison(
+ self, given: str, correct: str
+ ) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
# compare in NFC form so accents appear correct
given = ucd.normalize("NFC", given)
correct = ucd.normalize("NFC", correct)
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
- givenElems = []
- correctElems = []
+ givenElems: List[Tuple[bool, str]] = []
+ correctElems: List[Tuple[bool, str]] = []
givenPoint = 0
correctPoint = 0
offby = 0
- def logBad(old, new, str, array):
+ def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
if old != new:
- array.append((False, str[old:new]))
+ array.append((False, s[old:new]))
- def logGood(start, cnt, str, array):
+ def logGood(
+ start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
+ ) -> None:
if cnt:
- array.append((True, str[start : start + cnt]))
+ array.append((True, s[start : start + cnt]))
for x, y, cnt in s.get_matching_blocks():
# if anything was missed in correct, pad given
@@ -486,17 +494,17 @@ Please run Tools>Empty Cards"""
logGood(y, cnt, correct, correctElems)
return givenElems, correctElems
- def correct(self, given, correct, showBad=True):
+ def correct(self, given: str, correct: str, showBad: bool = True) -> str:
"Diff-corrects the typed-in answer."
givenElems, correctElems = self.tokenizeComparison(given, correct)
- def good(s):
+ def good(s: str) -> str:
return "" + html.escape(s) + ""
- def bad(s):
+ def bad(s: str) -> str:
return "" + html.escape(s) + ""
- def missed(s):
+ def missed(s: str) -> str:
return "" + html.escape(s) + ""
if given == correct:
@@ -519,24 +527,24 @@ Please run Tools>Empty Cards"""
res = "" + res + "
"
return res
- def _noLoneMarks(self, s):
+ def _noLoneMarks(self, s: str) -> str:
# ensure a combining character at the start does not join to
# previous text
if s and ucd.category(s[0]).startswith("M"):
return "\xa0" + s
return s
- def _getTypedAnswer(self):
+ def _getTypedAnswer(self) -> None:
self.web.evalWithCallback("typeans ? typeans.value : null", self._onTypedAnswer)
- def _onTypedAnswer(self, val):
+ def _onTypedAnswer(self, val: None) -> None:
self.typedAnswer = val or ""
self._showAnswer()
# Bottom bar
##########################################################################
- def _bottomHTML(self):
+ def _bottomHTML(self) -> str:
return """
@@ -565,7 +573,7 @@ time = %(time)d;
time=self.card.timeTaken() // 1000,
)
- def _showAnswerButton(self):
+ def _showAnswerButton(self) -> None:
if not self.typeCorrect:
self.bottom.web.setFocus()
middle = """
@@ -587,17 +595,17 @@ time = %(time)d;
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
self.bottom.web.adjustHeightToFit()
- def _showEaseButtons(self):
+ def _showEaseButtons(self) -> None:
self.bottom.web.setFocus()
middle = self._answerButtons()
self.bottom.web.eval("showAnswer(%s);" % json.dumps(middle))
- def _remaining(self):
+ def _remaining(self) -> str:
if not self.mw.col.conf["dueCounts"]:
return ""
if self.hadCardQueue:
# if it's come from the undo queue, don't count it separately
- counts = list(self.mw.col.sched.counts())
+ counts: List[Union[int, str]] = list(self.mw.col.sched.counts())
else:
counts = list(self.mw.col.sched.counts(self.card))
idx = self.mw.col.sched.countIdx(self.card)
@@ -608,13 +616,13 @@ time = %(time)d;
ctxt += space + "%s" % counts[2]
return ctxt
- def _defaultEase(self):
+ def _defaultEase(self) -> int:
if self.mw.col.sched.answerButtons(self.card) == 4:
return 3
else:
return 2
- def _answerButtonList(self):
+ def _answerButtonList(self) -> Sequence[Tuple[int, str]]:
l = ((1, _("Again")),)
cnt = self.mw.col.sched.answerButtons(self.card)
if cnt == 2:
@@ -624,7 +632,7 @@ time = %(time)d;
else:
return l + ((2, _("Hard")), (3, _("Good")), (4, _("Easy")))
- def _answerButtons(self):
+ def _answerButtons(self) -> str:
default = self._defaultEase()
def but(i, label):
@@ -652,7 +660,7 @@ time = %(time)d;
"""
return buf + script
- def _buttonTime(self, i):
+ def _buttonTime(self, i: int) -> str:
if not self.mw.col.conf["estTimes"]:
return ""
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " "
@@ -661,7 +669,7 @@ time = %(time)d;
# Leeches
##########################################################################
- def onLeech(self, card):
+ def onLeech(self, card: Card) -> None:
# for now
s = _("Card was a leech.")
if card.queue < 0:
@@ -730,7 +738,7 @@ time = %(time)d;
qtMenuShortcutWorkaround(m)
m.exec_(QCursor.pos())
- def _addMenuItems(self, m, rows):
+ def _addMenuItems(self, m, rows) -> None:
for row in rows:
if not row:
m.addSeparator()
@@ -753,10 +761,10 @@ time = %(time)d;
a.setChecked(True)
a.triggered.connect(func)
- def onOptions(self):
+ def onOptions(self) -> None:
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
- def setFlag(self, flag):
+ def setFlag(self, flag: int) -> None:
# need to toggle off?
if self.card.userFlag() == flag:
flag = 0
@@ -764,7 +772,7 @@ time = %(time)d;
self.card.flush()
self._drawFlag()
- def onMark(self):
+ def onMark(self) -> None:
f = self.card.note()
if f.hasTag("marked"):
f.delTag("marked")
@@ -773,19 +781,19 @@ time = %(time)d;
f.flush()
self._drawMark()
- def onSuspend(self):
+ def onSuspend(self) -> None:
self.mw.checkpoint(_("Suspend"))
self.mw.col.sched.suspendCards([c.id for c in self.card.note().cards()])
tooltip(_("Note suspended."))
self.mw.reset()
- def onSuspendCard(self):
+ def onSuspendCard(self) -> None:
self.mw.checkpoint(_("Suspend"))
self.mw.col.sched.suspendCards([self.card.id])
tooltip(_("Card suspended."))
self.mw.reset()
- def onDelete(self):
+ def onDelete(self) -> None:
# need to check state because the shortcut is global to the main
# window
if self.mw.state != "review" or not self.card:
@@ -801,23 +809,24 @@ time = %(time)d;
% cnt
)
- def onBuryCard(self):
+ def onBuryCard(self) -> None:
self.mw.checkpoint(_("Bury"))
self.mw.col.sched.buryCards([self.card.id])
self.mw.reset()
tooltip(_("Card buried."))
- def onBuryNote(self):
+ def onBuryNote(self) -> None:
self.mw.checkpoint(_("Bury"))
self.mw.col.sched.buryNote(self.card.nid)
self.mw.reset()
tooltip(_("Note buried."))
- def onRecordVoice(self):
+ def onRecordVoice(self) -> None:
self._recordedAudio = getAudio(self.mw, encode=False)
self.onReplayRecorded()
- def onReplayRecorded(self):
+ def onReplayRecorded(self) -> None:
if not self._recordedAudio:
- return tooltip(_("You haven't recorded your voice yet."))
+ tooltip(_("You haven't recorded your voice yet."))
+ return
av_player.play_file(self._recordedAudio)