Monkeytype qt/aqt/reviewer.py

This commit is contained in:
Alan Du 2020-02-26 22:11:21 -05:00
parent 96ca469d12
commit 6c2dda6c9c
2 changed files with 78 additions and 68 deletions

View File

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

View File

@ -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("<hr id=answer>", "")
hadHR = len(buf) != origSize
# munge correct value
parser = html.parser.HTMLParser()
cor = self.mw.col.media.strip(self.typeCorrect)
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
cor = stripHTML(cor)
# ensure we don't chomp multiple whitespace
cor = cor.replace(" ", "&nbsp;")
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 "<span class=typeGood>" + html.escape(s) + "</span>"
def bad(s):
def bad(s: str) -> str:
return "<span class=typeBad>" + html.escape(s) + "</span>"
def missed(s):
def missed(s: str) -> str:
return "<span class=typeMissed>" + html.escape(s) + "</span>"
if given == correct:
@ -519,24 +527,24 @@ Please run Tools>Empty Cards"""
res = "<div><code id=typeans>" + res + "</code></div>"
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 """
<center id=outer>
<table id=innertable width=100%% cellspacing=0 cellpadding=0>
@ -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 + "<span class=review-count>%s</span>" % 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;
<script>$(function () { $("#defease").focus(); });</script>"""
return buf + script
def _buttonTime(self, i):
def _buttonTime(self, i: int) -> str:
if not self.mw.col.conf["estTimes"]:
return "<div class=spacer></div>"
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;"
@ -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)