Merge pull request #483 from alanhdu/monkeytype

Add some typehints to qt/aqt [3/n]
This commit is contained in:
Damien Elmes 2020-03-02 15:38:21 +10:00 committed by GitHub
commit 4617a4b1f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 118 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._startTime = time.time()
self._startReps = self.sched.reps self._startReps = self.sched.reps
# FIXME: Use Literal[False] when on Python 3.8
def timeboxReached(self) -> Union[bool, Tuple[Any, int]]: def timeboxReached(self) -> Union[bool, Tuple[Any, int]]:
"Return (elapsedTime, reps) if timebox reached, or False." "Return (elapsedTime, reps) if timebox reached, or False."
if not self.conf["timeLim"]: if not self.conf["timeLim"]:

View file

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

View file

@ -87,7 +87,7 @@ class AVPlayer:
# audio be stopped? # audio be stopped?
interrupt_current_audio = True interrupt_current_audio = True
def __init__(self): def __init__(self) -> None:
self._enqueued: List[AVTag] = [] self._enqueued: List[AVTag] = []
self.current_player: Optional[Player] = None self.current_player: Optional[Player] = None
@ -112,7 +112,7 @@ class AVPlayer:
self._enqueued.insert(0, SoundOrVideoTag(filename=filename)) self._enqueued.insert(0, SoundOrVideoTag(filename=filename))
self._play_next_if_idle() self._play_next_if_idle()
def toggle_pause(self): def toggle_pause(self) -> None:
if self.current_player: if self.current_player:
self.current_player.toggle_pause() self.current_player.toggle_pause()
@ -179,7 +179,7 @@ av_player = AVPlayer()
# return modified command array that points to bundled command, and return # return modified command array that points to bundled command, and return
# required environment # required environment
def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]: def _packagedCmd(cmd: List[str]) -> Tuple[Any, Dict[str, str]]:
cmd = cmd[:] cmd = cmd[:]
env = os.environ.copy() env = os.environ.copy()
if "LD_LIBRARY_PATH" in env: if "LD_LIBRARY_PATH" in env:
@ -205,7 +205,7 @@ def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
si = startup_info() si = startup_info()
# osx throws interrupted system call errors frequently # osx throws interrupted system call errors frequently
def retryWait(proc) -> Any: def retryWait(proc: subprocess.Popen) -> int:
while 1: while 1:
try: try:
return proc.wait() return proc.wait()
@ -227,7 +227,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
args: List[str] = [] args: List[str] = []
env: Optional[Dict[str, str]] = None env: Optional[Dict[str, str]] = None
def __init__(self, taskman: TaskManager): def __init__(self, taskman: TaskManager) -> None:
self._taskman = taskman self._taskman = taskman
self._terminate_flag = False self._terminate_flag = False
self._process: Optional[subprocess.Popen] = None self._process: Optional[subprocess.Popen] = None
@ -238,7 +238,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
lambda: self._play(tag), lambda res: self._on_done(res, on_done) lambda: self._play(tag), lambda res: self._on_done(res, on_done)
) )
def stop(self): def stop(self) -> None:
self._terminate_flag = True self._terminate_flag = True
# block until stopped # block until stopped
t = time.time() t = time.time()
@ -252,7 +252,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
) )
self._wait_for_termination(tag) self._wait_for_termination(tag)
def _wait_for_termination(self, tag: AVTag): def _wait_for_termination(self, tag: AVTag) -> None:
self._taskman.run_on_main( self._taskman.run_on_main(
lambda: gui_hooks.av_player_did_begin_playing(self, tag) lambda: gui_hooks.av_player_did_begin_playing(self, tag)
) )
@ -359,7 +359,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
def toggle_pause(self) -> None: def toggle_pause(self) -> None:
self.set_property("pause", not self.get_property("pause")) self.set_property("pause", not self.get_property("pause"))
def seek_relative(self, secs) -> None: def seek_relative(self, secs: int) -> None:
self.command("seek", secs, "relative") self.command("seek", secs, "relative")
def on_idle(self) -> None: def on_idle(self) -> None:
@ -401,7 +401,7 @@ class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
) )
self._wait_for_termination(tag) self._wait_for_termination(tag)
def command(self, *args) -> None: def command(self, *args: Any) -> None:
"""Send a command over the slave interface. """Send a command over the slave interface.
The trailing newline is automatically added.""" The trailing newline is automatically added."""
@ -412,7 +412,7 @@ class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
def seek_relative(self, secs: int) -> None: def seek_relative(self, secs: int) -> None:
self.command("seek", secs, 0) self.command("seek", secs, 0)
def toggle_pause(self): def toggle_pause(self) -> None:
self.command("pause") self.command("pause")
@ -458,12 +458,12 @@ class _Recorder:
class PyAudioThreadedRecorder(threading.Thread): class PyAudioThreadedRecorder(threading.Thread):
def __init__(self, startupDelay) -> None: def __init__(self, startupDelay: float) -> None:
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.startupDelay = startupDelay self.startupDelay = startupDelay
self.finish = False self.finish = False
def run(self) -> Any: def run(self) -> None:
chunk = 1024 chunk = 1024
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
@ -499,7 +499,7 @@ class PyAudioRecorder(_Recorder):
# discard first 250ms which may have pops/cracks # discard first 250ms which may have pops/cracks
startupDelay = 0.25 startupDelay = 0.25
def __init__(self): def __init__(self) -> None:
for t in recFiles + [processingSrc, processingDst]: for t in recFiles + [processingSrc, processingDst]:
try: try:
os.unlink(t) os.unlink(t)
@ -507,15 +507,15 @@ class PyAudioRecorder(_Recorder):
pass pass
self.encode = False self.encode = False
def start(self): def start(self) -> None:
self.thread = PyAudioThreadedRecorder(startupDelay=self.startupDelay) self.thread = PyAudioThreadedRecorder(startupDelay=self.startupDelay)
self.thread.start() self.thread.start()
def stop(self): def stop(self) -> None:
self.thread.finish = True self.thread.finish = True
self.thread.join() self.thread.join()
def file(self): def file(self) -> str:
if self.encode: if self.encode:
tgt = "rec%d.mp3" % time.time() tgt = "rec%d.mp3" % time.time()
os.rename(processingDst, tgt) os.rename(processingDst, tgt)
@ -530,7 +530,7 @@ Recorder = PyAudioRecorder
########################################################################## ##########################################################################
def getAudio(parent, encode=True): def getAudio(parent: QWidget, encode: bool = True) -> Optional[str]:
"Record and return filename" "Record and return filename"
# record first # record first
r = Recorder() r = Recorder()
@ -547,16 +547,16 @@ def getAudio(parent, encode=True):
t = time.time() t = time.time()
r.start() r.start()
time.sleep(r.startupDelay) time.sleep(r.startupDelay)
QApplication.instance().processEvents() QApplication.instance().processEvents() # type: ignore
while not mb.clickedButton(): while not mb.clickedButton():
txt = _("Recording...<br>Time: %0.1f") txt = _("Recording...<br>Time: %0.1f")
mb.setText(txt % (time.time() - t)) mb.setText(txt % (time.time() - t))
mb.show() mb.show()
QApplication.instance().processEvents() QApplication.instance().processEvents() # type: ignore
if mb.clickedButton() == mb.escapeButton(): if mb.clickedButton() == mb.escapeButton():
r.stop() r.stop()
r.cleanup() r.cleanup()
return return None
saveGeom(mb, "audioRecorder") saveGeom(mb, "audioRecorder")
# ensure at least a second captured # ensure at least a second captured
while time.time() - t < 1: while time.time() - t < 1:

View file

@ -197,7 +197,7 @@ QTabWidget { background-color: %s; }
app.setPalette(palette) app.setPalette(palette)
def _update_stat_colors(self): def _update_stat_colors(self) -> None:
import anki.stats as s import anki.stats as s
s.colLearn = self.str_color("new-count") s.colLearn = self.str_color("new-count")

View file

@ -15,13 +15,13 @@ from aqt.webview import AnkiWebView
# wrapper class for set_bridge_command() # wrapper class for set_bridge_command()
class TopToolbar: class TopToolbar:
def __init__(self, toolbar: Toolbar): def __init__(self, toolbar: Toolbar) -> None:
self.toolbar = toolbar self.toolbar = toolbar
# wrapper class for set_bridge_command() # wrapper class for set_bridge_command()
class BottomToolbar: class BottomToolbar:
def __init__(self, toolbar: Toolbar): def __init__(self, toolbar: Toolbar) -> None:
self.toolbar = toolbar self.toolbar = toolbar
@ -40,7 +40,7 @@ class Toolbar:
buf: str = "", buf: str = "",
web_context: Optional[Any] = None, web_context: Optional[Any] = None,
link_handler: Optional[Callable[[str], Any]] = None, link_handler: Optional[Callable[[str], Any]] = None,
): ) -> None:
web_context = web_context or TopToolbar(self) web_context = web_context or TopToolbar(self)
link_handler = link_handler or self._linkHandler link_handler = link_handler or self._linkHandler
self.web.set_bridge_command(link_handler, web_context) self.web.set_bridge_command(link_handler, web_context)
@ -90,7 +90,7 @@ class Toolbar:
f"""{label}</a>""" f"""{label}</a>"""
) )
def _centerLinks(self): def _centerLinks(self) -> str:
links = [ links = [
self.create_link( self.create_link(
"decks", "decks",
@ -149,15 +149,15 @@ class Toolbar:
# Link handling # Link handling
###################################################################### ######################################################################
def _linkHandler(self, link): def _linkHandler(self, link: str) -> bool:
if link in self.link_handlers: if link in self.link_handlers:
self.link_handlers[link]() self.link_handlers[link]()
return False return False
def _deckLinkHandler(self): def _deckLinkHandler(self) -> None:
self.mw.moveToState("deckBrowser") self.mw.moveToState("deckBrowser")
def _studyLinkHandler(self): def _studyLinkHandler(self) -> None:
# if overview already shown, switch to review # if overview already shown, switch to review
if self.mw.state == "overview": if self.mw.state == "overview":
self.mw.col.startTimebox() self.mw.col.startTimebox()
@ -165,16 +165,16 @@ class Toolbar:
else: else:
self.mw.onOverview() self.mw.onOverview()
def _addLinkHandler(self): def _addLinkHandler(self) -> None:
self.mw.onAddCard() self.mw.onAddCard()
def _browseLinkHandler(self): def _browseLinkHandler(self) -> None:
self.mw.onBrowse() self.mw.onBrowse()
def _statsLinkHandler(self): def _statsLinkHandler(self) -> None:
self.mw.onStats() self.mw.onStats()
def _syncLinkHandler(self): def _syncLinkHandler(self) -> None:
self.mw.onSync() self.mw.onSync()
# HTML & CSS # HTML & CSS
@ -206,7 +206,7 @@ class BottomBar(Toolbar):
buf: str = "", buf: str = "",
web_context: Optional[Any] = None, web_context: Optional[Any] = None,
link_handler: Optional[Callable[[str], Any]] = None, link_handler: Optional[Callable[[str], Any]] = None,
): ) -> None:
# note: some screens may override this # note: some screens may override this
web_context = web_context or BottomToolbar(self) web_context = web_context or BottomToolbar(self)
link_handler = link_handler or self._linkHandler link_handler = link_handler or self._linkHandler

View file

@ -5,7 +5,7 @@ import dataclasses
import json import json
import math import math
import sys import sys
from typing import Any, List, Optional, Tuple from typing import Any, Callable, List, Optional, Sequence, Tuple
from anki.lang import _ from anki.lang import _
from anki.utils import isLin, isMac, isWin from anki.utils import isLin, isMac, isWin
@ -176,7 +176,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
self._domDone = True self._domDone = True
self._pendingActions: List[Tuple[str, List[Any]]] = [] self._pendingActions: List[Tuple[str, Sequence[Any]]] = []
self.requiresCol = True self.requiresCol = True
self.setPage(self._page) self.setPage(self._page)
@ -258,13 +258,13 @@ class AnkiWebView(QWebEngineView): # type: ignore
def dropEvent(self, evt): def dropEvent(self, evt):
pass pass
def setHtml(self, html): def setHtml(self, html: str) -> None:
# discard any previous pending actions # discard any previous pending actions
self._pendingActions = [] self._pendingActions = []
self._domDone = True self._domDone = True
self._queueAction("setHtml", html) self._queueAction("setHtml", html)
def _setHtml(self, html): def _setHtml(self, html: str) -> None:
app = QApplication.instance() app = QApplication.instance()
oldFocus = app.focusWidget() oldFocus = app.focusWidget()
self._domDone = False self._domDone = False
@ -273,7 +273,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
if oldFocus: if oldFocus:
oldFocus.setFocus() oldFocus.setFocus()
def zoomFactor(self): def zoomFactor(self) -> float:
# overridden scale factor? # overridden scale factor?
webscale = os.environ.get("ANKI_WEBSCALE") webscale = os.environ.get("ANKI_WEBSCALE")
if webscale: if webscale:
@ -295,7 +295,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
newFactor = desiredScale / qtIntScale newFactor = desiredScale / qtIntScale
return max(1, newFactor) return max(1, newFactor)
def _getQtIntScale(self, screen): def _getQtIntScale(self, screen) -> int:
# try to detect if Qt has scaled the screen # try to detect if Qt has scaled the screen
# - qt will round the scale factor to a whole number, so a dpi of 125% = 1x, # - qt will round the scale factor to a whole number, so a dpi of 125% = 1x,
# and a dpi of 150% = 2x # and a dpi of 150% = 2x
@ -430,13 +430,13 @@ body {{ zoom: {}; background: {}; {} }}
fname fname
) )
def eval(self, js): def eval(self, js: str) -> None:
self.evalWithCallback(js, None) self.evalWithCallback(js, None)
def evalWithCallback(self, js, cb): def evalWithCallback(self, js: str, cb: Callable) -> None:
self._queueAction("eval", js, cb) self._queueAction("eval", js, cb)
def _evalWithCallback(self, js, cb): def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
if cb: if cb:
def handler(val): def handler(val):
@ -449,11 +449,11 @@ body {{ zoom: {}; background: {}; {} }}
else: else:
self.page().runJavaScript(js) self.page().runJavaScript(js)
def _queueAction(self, name, *args): def _queueAction(self, name: str, *args: Any) -> None:
self._pendingActions.append((name, args)) self._pendingActions.append((name, args))
self._maybeRunActions() self._maybeRunActions()
def _maybeRunActions(self): def _maybeRunActions(self) -> None:
while self._pendingActions and self._domDone: while self._pendingActions and self._domDone:
name, args = self._pendingActions.pop(0) name, args = self._pendingActions.pop(0)
@ -464,10 +464,10 @@ body {{ zoom: {}; background: {}; {} }}
else: else:
raise Exception("unknown action: {}".format(name)) raise Exception("unknown action: {}".format(name))
def _openLinksExternally(self, url): def _openLinksExternally(self, url: str) -> None:
openLink(url) openLink(url)
def _shouldIgnoreWebEvent(self): def _shouldIgnoreWebEvent(self) -> bool:
# async web events may be received after the profile has been closed # async web events may be received after the profile has been closed
# or the underlying webview has been deleted # or the underlying webview has been deleted
from aqt import mw from aqt import mw
@ -499,18 +499,18 @@ body {{ zoom: {}; background: {}; {} }}
else: else:
return self.onBridgeCmd(cmd) return self.onBridgeCmd(cmd)
def defaultOnBridgeCmd(self, cmd: str) -> Any: def defaultOnBridgeCmd(self, cmd: str) -> None:
print("unhandled bridge cmd:", cmd) print("unhandled bridge cmd:", cmd)
# legacy # legacy
def resetHandlers(self): def resetHandlers(self) -> None:
self.onBridgeCmd = self.defaultOnBridgeCmd self.onBridgeCmd = self.defaultOnBridgeCmd
self._bridge_context = None self._bridge_context = None
def adjustHeightToFit(self): def adjustHeightToFit(self) -> None:
self.evalWithCallback("$(document.body).height()", self._onHeight) self.evalWithCallback("$(document.body).height()", self._onHeight)
def _onHeight(self, qvar): def _onHeight(self, qvar: Optional[int]) -> None:
from aqt import mw from aqt import mw
if qvar is None: if qvar is None: