add qconnect helper and some type hints

The type hints allow mypy to check the gui_hook calls, revealing a
bunch of places that are broken as they expect no arguments like the
legacy hooks.

To make mypy happy about PyQt's signal.connect(func), a qconnect()
helper has been added.
This commit is contained in:
Damien Elmes 2020-01-16 07:41:23 +10:00
parent c6d0425020
commit 8310cb7a0e
14 changed files with 83 additions and 74 deletions

View file

@ -1,7 +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 Callable, List
from typing import Callable, List, Optional
import aqt.deckchooser
import aqt.editor
@ -143,7 +143,7 @@ class AddCards(QDialog):
self.history = self.history[:15]
self.historyButton.setEnabled(True)
def onHistory(self):
def onHistory(self) -> None:
m = QMenu(self)
for nid in self.history:
if self.mw.col.findNotes("nid:%s" % nid):
@ -152,7 +152,7 @@ class AddCards(QDialog):
if len(txt) > 30:
txt = txt[:30] + "..."
a = m.addAction(_('Edit "%s"') % txt)
a.triggered.connect(lambda b, nid=nid: self.editHistory(nid))
qconnect(a.triggered, lambda b, nid=nid: self.editHistory(nid))
else:
a = m.addAction(_("(Note deleted)"))
a.setEnabled(False)
@ -164,12 +164,12 @@ class AddCards(QDialog):
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
browser.onSearchActivated()
def addNote(self, note):
def addNote(self, note) -> Optional[Note]:
note.model()["did"] = self.deckChooser.selectedId()
ret = note.dupeOrEmpty()
if ret == 1:
showWarning(_("The first field is empty."), help="AddItems#AddError")
return
return None
if "{{cloze:" in note.model()["tmpls"][0]["qfmt"]:
if not self.mw.col.models._availClozeOrds(
note.model(), note.joinedFields(), False
@ -180,7 +180,7 @@ class AddCards(QDialog):
"but have not made any cloze deletions. Proceed?"
)
):
return
return None
cards = self.mw.col.addNote(note)
if not cards:
showWarning(
@ -191,7 +191,7 @@ question on all cards."""
),
help="AddItems",
)
return
return None
self.mw.col.clearUndo()
self.addHistory(note)
self.mw.requireReset()

View file

@ -9,11 +9,12 @@ import sre_constants
import time
import unicodedata
from operator import itemgetter
from typing import Callable, List, Optional
from typing import Callable, List, Optional, Union
import anki
import aqt.forms
from anki import hooks
from anki.cards import Card
from anki.collection import _Collection
from anki.consts import *
from anki.lang import _, ngettext
@ -27,6 +28,7 @@ from anki.utils import (
isWin,
)
from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor
from aqt.qt import *
from aqt.sound import allSounds, clearAudioQueue, play
from aqt.utils import (
@ -556,8 +558,9 @@ class Browser(QMainWindow):
model: DataModel
mw: AnkiQt
col: _Collection
editor: Optional[Editor]
def __init__(self, mw: AnkiQt):
def __init__(self, mw: AnkiQt) -> None:
QMainWindow.__init__(self, None, Qt.Window)
self.mw = mw
self.col = self.mw.col
@ -572,7 +575,7 @@ class Browser(QMainWindow):
restoreState(self, "editor")
restoreSplitter(self.form.splitter, "editor3")
self.form.splitter.setChildrenCollapsible(False)
self.card = None
self.card: Optional[Card] = None
self.setupColumns()
self.setupTable()
self.setupMenus()
@ -584,7 +587,7 @@ class Browser(QMainWindow):
self.setupSearch()
self.show()
def setupMenus(self):
def setupMenus(self) -> None:
# pylint: disable=unnecessary-lambda
# actions
f = self.form
@ -635,9 +638,9 @@ class Browser(QMainWindow):
f.actionGuide.triggered.connect(self.onHelp)
# keyboard shortcut for shift+home/end
self.pgUpCut = QShortcut(QKeySequence("Shift+Home"), self)
self.pgUpCut.activated.connect(self.onFirstCard)
qconnect(self.pgUpCut.activated, self.onFirstCard)
self.pgDownCut = QShortcut(QKeySequence("Shift+End"), self)
self.pgDownCut.activated.connect(self.onLastCard)
qconnect(self.pgDownCut.activated, self.onLastCard)
# add-on hook
gui_hooks.browser_menus_did_init(self)
self.mw.maybeHideAccelerators(self)
@ -646,14 +649,14 @@ class Browser(QMainWindow):
self.form.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
self.form.tableView.customContextMenuRequested.connect(self.onContextMenu)
def onContextMenu(self, _point):
def onContextMenu(self, _point) -> None:
m = QMenu()
for act in self.form.menu_Cards.actions():
m.addAction(act)
m.addSeparator()
for act in self.form.menu_Notes.actions():
m.addAction(act)
gui_hooks.browser_will_show_context_menu(self, m)
gui_hooks.browser_will_show_context_menu(self)
qtMenuShortcutWorkaround(m)
m.exec_(QCursor.pos())
@ -771,7 +774,7 @@ class Browser(QMainWindow):
self.search()
# search triggered programmatically. caller must have saved note first.
def search(self):
def search(self) -> None:
if "is:current" in self._lastSearchTxt:
# show current card if there is one
c = self.mw.reviewer.card
@ -827,7 +830,7 @@ class Browser(QMainWindow):
"Update current note and hide/show editor."
self.editor.saveNow(lambda: self._onRowChanged(current, previous))
def _onRowChanged(self, current, previous):
def _onRowChanged(self, current, previous) -> None:
update = self.updateTitle()
show = self.model.cards and update == 1
self.form.splitter.widget(1).setVisible(not not show)
@ -841,7 +844,7 @@ class Browser(QMainWindow):
else:
self.editor.setNote(self.card.note(reload=True), focusTo=self.focusTo)
self.focusTo = None
self.editor.card = self.card
self.editor.card = self.card # type: ignore
self.singleCard = True
self._updateFlagsMenu()
gui_hooks.browser_did_change_row(self)
@ -1530,7 +1533,7 @@ where id in %s"""
######################################################################
_previewTimer = None
_lastPreviewRender = 0
_lastPreviewRender: Union[int,float] = 0
_lastPreviewState = None
_previewCardChanged = False
@ -1669,7 +1672,7 @@ where id in %s"""
self._previewTimer.stop()
self._previewTimer = None
def _renderScheduledPreview(self):
def _renderScheduledPreview(self) -> None:
self._cancelPreviewTimer()
self._lastPreviewRender = time.time()
@ -2019,7 +2022,7 @@ update cards set usn=?, mod=?, did=? where id in """
# Edit: undo
######################################################################
def setupHooks(self):
def setupHooks(self) -> None:
gui_hooks.undo_state_did_change.append(self.onUndoState)
gui_hooks.state_did_reset.append(self.onReset)
gui_hooks.editor_did_fire_typing_timer.append(self.refreshCurrentCard)
@ -2029,7 +2032,7 @@ update cards set usn=?, mod=?, did=? where id in """
hooks.note_type_added.append(self.maybeRefreshSidebar)
hooks.deck_added.append(self.maybeRefreshSidebar)
def teardownHooks(self):
def teardownHooks(self) -> None:
gui_hooks.undo_state_did_change.remove(self.onUndoState)
gui_hooks.state_did_reset.remove(self.onReset)
gui_hooks.editor_did_fire_typing_timer.remove(self.refreshCurrentCard)
@ -2261,7 +2264,7 @@ update cards set usn=?, mod=?, did=? where id in """
class ChangeModel(QDialog):
def __init__(self, browser, nids):
def __init__(self, browser, nids) -> None:
QDialog.__init__(self, browser)
self.browser = browser
self.nids = nids
@ -2394,7 +2397,7 @@ class ChangeModel(QDialog):
old=self.oldModel["flds"], combos=self.fcombos, new=self.targetModel["flds"]
)
def cleanup(self):
def cleanup(self) -> None:
gui_hooks.state_did_reset.remove(self.onReset)
gui_hooks.current_note_type_did_change.remove(self.onReset)
self.modelChooser.cleanup()

View file

@ -326,7 +326,7 @@ Please create a new card type first."""
self._previewTimer.stop()
self._previewTimer = None
def _renderPreview(self):
def _renderPreview(self) -> None:
self.cancelPreviewTimer()
c = self.card

View file

@ -235,16 +235,16 @@ where id > ?""",
# Options
##########################################################################
def _showOptions(self, did):
def _showOptions(self, did) -> None:
m = QMenu(self.mw)
a = m.addAction(_("Rename"))
a.triggered.connect(lambda b, did=did: self._rename(did))
qconnect(a.triggered, lambda b, did=did: self._rename(did))
a = m.addAction(_("Options"))
a.triggered.connect(lambda b, did=did: self._options(did))
qconnect(a.triggered, lambda b, did=did: self._options(did))
a = m.addAction(_("Export"))
a.triggered.connect(lambda b, did=did: self._export(did))
qconnect(a.triggered, lambda b, did=did: self._export(did))
a = m.addAction(_("Delete"))
a.triggered.connect(lambda b, did=did: self._delete(did))
qconnect(a.triggered, lambda b, did=did: self._delete(did))
gui_hooks.deck_browser_will_show_options_menu(m, did)
m.exec_(QCursor.pos())

View file

@ -9,9 +9,9 @@ from aqt.utils import shortcut
class DeckChooser(QHBoxLayout):
def __init__(self, mw, widget, label=True, start=None):
def __init__(self, mw, widget: QWidget, label=True, start=None) -> None:
QHBoxLayout.__init__(self)
self.widget = widget
self.widget = widget # type: ignore
self.mw = mw
self.deck = mw.col
self.label = label
@ -63,7 +63,7 @@ class DeckChooser(QHBoxLayout):
def hide(self):
self.widget.hide()
def cleanup(self):
def cleanup(self) -> None:
gui_hooks.current_note_type_did_change.remove(self.onModelChange)
def onModelChange(self):

View file

@ -10,7 +10,7 @@ from aqt.utils import restoreGeom, saveGeom, tooltip
class EditCurrent(QDialog):
def __init__(self, mw):
def __init__(self, mw) -> None:
QDialog.__init__(self, None, Qt.Window)
mw.setupDialogGC(self)
self.mw = mw
@ -33,7 +33,7 @@ class EditCurrent(QDialog):
# pylint: disable=unnecessary-lambda
self.mw.progress.timer(100, lambda: self.editor.web.setFocus(), False)
def onReset(self):
def onReset(self) -> None:
# lazy approach for now: throw away edits
try:
n = self.editor.note
@ -58,7 +58,7 @@ class EditCurrent(QDialog):
def saveAndClose(self):
self.editor.saveNow(self._saveAndClose)
def _saveAndClose(self):
def _saveAndClose(self) -> None:
gui_hooks.state_did_reset.remove(self.onReset)
r = self.mw.reviewer
try:

View file

@ -21,6 +21,7 @@ import aqt
import aqt.sound
from anki.hooks import runFilter
from anki.lang import _
from anki.notes import Note
from anki.sync import AnkiRequestsClient
from anki.utils import checksum, isWin, namedtmp, stripHTMLMedia
from aqt import AnkiQt, gui_hooks
@ -67,13 +68,13 @@ html { background: %s; }
# caller is responsible for resetting note on reset
class Editor:
def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False):
def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False) -> None:
self.mw = mw
self.widget = widget
self.parentWindow = parentWindow
self.note = None
self.note: Optional[Note] = None
self.addMode = addMode
self.currentField = None
self.currentField: Optional[int] = None
# current card, for card layout
self.card = None
self.setupOuter()
@ -91,7 +92,7 @@ class Editor:
self.widget.setLayout(l)
self.outerLayout = l
def setupWeb(self):
def setupWeb(self) -> None:
self.web = EditorWebView(self.widget, self)
self.web.title = "editor"
self.web.allowDrops = True
@ -159,7 +160,7 @@ class Editor:
fldsTitle=_("Customize Fields"),
cardsTitle=shortcut(_("Customize Card Templates (Ctrl+L)")),
)
bgcol = self.mw.app.palette().window().color().name()
bgcol = self.mw.app.palette().window().color().name() # type: ignore
# then load page
self.web.stdHtml(
_html % (bgcol, bgcol, topbuts, _("Show Duplicates")),
@ -260,7 +261,7 @@ class Editor:
)
)
def setupShortcuts(self):
def setupShortcuts(self) -> None:
# if a third element is provided, enable shortcut even when no field selected
cuts: List[Tuple] = [
("Ctrl+L", self.onCardLayout, True),
@ -292,7 +293,7 @@ class Editor:
fn = self._addFocusCheck(fn)
else:
keys, fn, _ = row
QShortcut(QKeySequence(keys), self.widget, activated=fn)
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
def _addFocusCheck(self, fn):
def checkFocus():
@ -329,7 +330,7 @@ class Editor:
# JS->Python bridge
######################################################################
def onBridgeCmd(self, cmd):
def onBridgeCmd(self, cmd) -> None:
if not self.note:
# shutdown
return
@ -399,7 +400,7 @@ class Editor:
def loadNoteKeepingFocus(self):
self.loadNote(self.currentField)
def loadNote(self, focusTo=None):
def loadNote(self, focusTo=None) -> None:
if not self.note:
return
@ -426,7 +427,7 @@ class Editor:
)
self.web.evalWithCallback(js, oncallback)
def fonts(self):
def fonts(self) -> List[Tuple[str,int,bool]]:
return [
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
for f in self.note.model()["flds"]
@ -528,7 +529,7 @@ class Editor:
if not self.tags.text() or not self.addMode:
self.tags.setText(self.note.stringTags().strip())
def saveTags(self):
def saveTags(self) -> None:
if not self.note:
return
tagsTxt = unicodedata.normalize("NFC", self.tags.text())
@ -1103,14 +1104,14 @@ class EditorWebView(AnkiWebView):
mime.setHtml("<!--anki-->" + html)
clip.setMimeData(mime)
def contextMenuEvent(self, evt):
def contextMenuEvent(self, evt) -> None:
m = QMenu(self)
a = m.addAction(_("Cut"))
a.triggered.connect(self.onCut)
qconnect(a.triggered, self.onCut)
a = m.addAction(_("Copy"))
a.triggered.connect(self.onCopy)
qconnect(a.triggered, self.onCopy)
a = m.addAction(_("Paste"))
a.triggered.connect(self.onPaste)
qconnect(a.triggered, self.onPaste)
gui_hooks.editor_will_show_context_menu(self, m)
m.popup(QCursor.pos())

View file

@ -73,7 +73,7 @@ class ChangeMap(QDialog):
class ImportDialog(QDialog):
def __init__(self, mw: AnkiQt, importer):
def __init__(self, mw: AnkiQt, importer) -> None:
QDialog.__init__(self, mw, Qt.Window)
self.mw = mw
self.importer = importer
@ -278,7 +278,7 @@ you can enter it here. Use \\t to represent tab."""
else:
self.showMapping(keepMapping=True)
def reject(self):
def reject(self) -> None:
self.modelChooser.cleanup()
self.deck.cleanup()
gui_hooks.current_note_type_did_change.remove(self.modelChanged)

View file

@ -569,7 +569,7 @@ from the profile screen."
def _deckBrowserState(self, oldState: str) -> None:
self.deckBrowser.show()
def _colLoadingState(self, oldState):
def _colLoadingState(self, oldState) -> None:
"Run once, when col is loaded."
self.enableColMenuItems()
# ensure cwd is set if media dir exists
@ -918,7 +918,7 @@ QTreeWidget {
# Undo & autosave
##########################################################################
def onUndo(self):
def onUndo(self) -> None:
n = self.col.undoName()
if not n:
return

View file

@ -9,9 +9,9 @@ from aqt.utils import shortcut
class ModelChooser(QHBoxLayout):
def __init__(self, mw, widget, label=True):
def __init__(self, mw, widget, label=True) -> None:
QHBoxLayout.__init__(self)
self.widget = widget
self.widget = widget # type: ignore
self.mw = mw
self.deck = mw.col
self.label = label
@ -19,7 +19,7 @@ class ModelChooser(QHBoxLayout):
self.setSpacing(8)
self.setupModels()
gui_hooks.state_did_reset.append(self.onReset)
self.widget.setLayout(self)
self.widget.setLayout(self) # type: ignore
def setupModels(self):
if self.label:
@ -40,7 +40,7 @@ class ModelChooser(QHBoxLayout):
self.models.setSizePolicy(sizePolicy)
self.updateModels()
def cleanup(self):
def cleanup(self) -> None:
gui_hooks.state_did_reset.remove(self.onReset)
def onReset(self):
@ -57,12 +57,12 @@ class ModelChooser(QHBoxLayout):
aqt.models.Models(self.mw, self.widget)
def onModelChange(self):
def onModelChange(self) -> None:
from aqt.studydeck import StudyDeck
current = self.deck.models.current()["name"]
# edit button
edit = QPushButton(_("Manage"), clicked=self.onEdit)
edit = QPushButton(_("Manage"), clicked=self.onEdit) # type: ignore
def nameFunc():
return sorted(self.deck.models.allNames())

View file

@ -14,6 +14,7 @@ from PyQt5.QtCore import pyqtRemoveInputHook # pylint: disable=no-name-in-modul
from PyQt5.QtGui import * # type: ignore
from PyQt5.QtWebEngineWidgets import * # type: ignore
from PyQt5.QtWidgets import *
from typing import Callable
from anki.utils import isMac, isWin
@ -52,3 +53,7 @@ qtpoint = QT_VERSION & 0xFF
if qtmajor != 5 or qtminor < 9 or qtminor == 10:
raise Exception("Anki does not support your Qt version.")
def qconnect(signal: Callable, func: Callable) -> None:
"Helper to work around type checking not working with signal.connect(func)."
signal.connect(func) # type: ignore

View file

@ -8,7 +8,7 @@ import html.parser
import json
import re
import unicodedata as ucd
from typing import List
from typing import List, Optional
import aqt
from anki import hooks
@ -33,13 +33,13 @@ class Reviewer:
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
self.web = mw.web
self.card = None
self.card: Optional[Card] = None
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.state = None
self.state: Optional[str] = None
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech)
@ -61,7 +61,7 @@ class Reviewer:
# id was deleted
return
def cleanup(self):
def cleanup(self) -> None:
gui_hooks.reviewer_will_end()
# Fetching a card
@ -170,7 +170,7 @@ class Reviewer:
def _mungeQA(self, buf):
return self.typeAnsFilter(mungeQA(self.mw.col, buf))
def _showQuestion(self):
def _showQuestion(self) -> None:
self._reps += 1
self.state = "question"
self.typedAnswer = None
@ -217,7 +217,7 @@ The front of this card is empty. Please run Tools>Empty Cards."""
# Showing the answer
##########################################################################
def _showAnswer(self):
def _showAnswer(self) -> None:
if self.mw.state != "review":
# showing resetRequired screen; ignore space
return
@ -689,7 +689,7 @@ time = %(time)d;
]
return opts
def showContextMenu(self):
def showContextMenu(self) -> None:
opts = self._contextMenu()
m = QMenu(self.mw)
self._addMenuItems(m, opts)

View file

@ -23,7 +23,7 @@ class StudyDeck(QDialog):
dyn=False,
buttons=None,
geomKey="default",
):
) -> None:
QDialog.__init__(self, parent or mw)
if buttons is None:
buttons = []
@ -118,7 +118,7 @@ class StudyDeck(QDialog):
self.origNames = self.nameFunc()
self.redraw(self.filt, self.focus)
def accept(self):
def accept(self) -> None:
saveGeom(self, self.geomKey)
gui_hooks.state_did_reset.remove(self.onReset)
row = self.form.list.currentRow()
@ -128,12 +128,12 @@ class StudyDeck(QDialog):
self.name = self.names[self.form.list.currentRow()]
QDialog.accept(self)
def reject(self):
def reject(self) -> None:
saveGeom(self, self.geomKey)
gui_hooks.state_did_reset.remove(self.onReset)
QDialog.reject(self)
def onAddDeck(self):
def onAddDeck(self) -> None:
row = self.form.list.currentRow()
if row < 0:
default = self.form.filter.text()

View file

@ -178,10 +178,10 @@ class AnkiWebView(QWebEngineView): # type: ignore
def onSelectAll(self):
self.triggerPageAction(QWebEnginePage.SelectAll)
def contextMenuEvent(self, evt):
def contextMenuEvent(self, evt) -> None:
m = QMenu(self)
a = m.addAction(_("Copy"))
a.triggered.connect(self.onCopy)
a.triggered.connect(self.onCopy) # type: ignore
gui_hooks.webview_will_show_context_menu(self, m)
m.popup(QCursor.pos())