From 6c2dda6c9c338ebb6fe42e04506a37385f610cfe Mon Sep 17 00:00:00 2001 From: Alan Du Date: Wed, 26 Feb 2020 22:11:21 -0500 Subject: [PATCH] Monkeytype qt/aqt/reviewer.py --- pylib/anki/collection.py | 1 + qt/aqt/reviewer.py | 145 +++++++++++++++++++++------------------ 2 files changed, 78 insertions(+), 68 deletions(-) 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|
|)+", " ", 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)