diff --git a/.mypy.ini b/.mypy.ini index da56d9d6217..648c6a6eaf5 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -104,6 +104,12 @@ strict_optional = True strict_optional = True [mypy-aqt.progress] strict_optional = True +[mypy-aqt.tagedit] +strict_optional = True +[mypy-aqt.utils] +strict_optional = True +[mypy-aqt.sync] +strict_optional = True [mypy-anki.scheduler.base] strict_optional = True [mypy-anki._backend.rsbridge] diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 208ed6448b7..43e443eef7d 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -1128,7 +1128,7 @@ def abort_sync(self) -> None: self._backend.abort_sync() def full_upload_or_download( - self, *, auth: SyncAuth, server_usn: int | None, upload: bool + self, *, auth: SyncAuth | None, server_usn: int | None, upload: bool ) -> None: self._backend.full_upload_or_download( sync_pb2.FullUploadOrDownloadRequest( diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 05caf97f239..7842b27a951 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -94,7 +94,7 @@ def on_deck_changed(self, deck_id: int) -> None: lambda _: self.refresh() ).run_in_background() - def _imagePath(self) -> str: + def _imagePath(self) -> str | None: name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time())) name = f"anki-{tr.statistics_stats()}{name}" file = getSaveFile( @@ -196,7 +196,7 @@ def closeWithCallback(self, callback: Callable[[], None]) -> None: self.reject() callback() - def _imagePath(self) -> str: + def _imagePath(self) -> str | None: name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time())) name = f"anki-{tr.statistics_stats()}{name}" file = getSaveFile( diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index cea64fadc06..2b8bd59af5f 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -168,7 +168,7 @@ def callback(choice: int) -> None: def confirm_full_download( - mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None] + mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None] ) -> None: # confirmation step required, as some users customize their notetypes # in an empty collection, then want to upload them @@ -184,7 +184,7 @@ def callback(choice: int) -> None: def confirm_full_upload( - mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None] + mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None] ) -> None: # confirmation step required, as some users have reported an upload # happening despite having their AnkiWeb collection not being empty @@ -220,7 +220,7 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt, label: str) -> None: def full_download( - mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None] + mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None] ) -> None: label = tr.sync_downloading_from_ankiweb() @@ -372,7 +372,9 @@ def get_id_and_pass_from_user( l2.setBuddy(passwd) vbox.addLayout(g) bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore - bb.button(QDialogButtonBox.StandardButton.Ok).setAutoDefault(True) + ok_button = bb.button(QDialogButtonBox.StandardButton.Ok) + assert ok_button is not None + ok_button.setAutoDefault(True) qconnect(bb.accepted, diag.accept) qconnect(bb.rejected, diag.reject) vbox.addWidget(bb) diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py index 5c968621052..d85d6655a04 100644 --- a/qt/aqt/tagedit.py +++ b/qt/aqt/tagedit.py @@ -42,13 +42,17 @@ def setCol(self, col: Collection) -> None: l = (d.name for d in self.col.decks.all_names_and_ids()) self.model.setStringList(l) - def focusInEvent(self, evt: QFocusEvent) -> None: + def focusInEvent(self, evt: QFocusEvent | None) -> None: QLineEdit.focusInEvent(self, evt) - def keyPressEvent(self, evt: QKeyEvent) -> None: + def keyPressEvent(self, evt: QKeyEvent | None) -> None: + assert evt is not None + popup = self._completer.popup() + assert popup is not None + if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): # show completer on arrow key up/down - if not self._completer.popup().isVisible(): + if not popup.isVisible(): self.showCompleter() return if ( @@ -56,24 +60,21 @@ def keyPressEvent(self, evt: QKeyEvent) -> None: and evt.modifiers() & Qt.KeyboardModifier.ControlModifier ): # select next completion - if not self._completer.popup().isVisible(): + if not popup.isVisible(): self.showCompleter() index = self._completer.currentIndex() - self._completer.popup().setCurrentIndex(index) + popup.setCurrentIndex(index) cur_row = index.row() if not self._completer.setCurrentRow(cur_row + 1): self._completer.setCurrentRow(0) return - if ( - evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) - and self._completer.popup().isVisible() - ): + if evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and popup.isVisible(): # apply first completion if no suggestion selected - selected_row = self._completer.popup().currentIndex().row() + selected_row = popup.currentIndex().row() if selected_row == -1: self._completer.setCurrentRow(0) index = self._completer.currentIndex() - self._completer.popup().setCurrentIndex(index) + popup.setCurrentIndex(index) self.hideCompleter() QWidget.keyPressEvent(self, evt) return @@ -97,15 +98,19 @@ def showCompleter(self) -> None: self._completer.setCompletionPrefix(self.text()) self._completer.complete() - def focusOutEvent(self, evt: QFocusEvent) -> None: + def focusOutEvent(self, evt: QFocusEvent | None) -> None: QLineEdit.focusOutEvent(self, evt) self.lostFocus.emit() # type: ignore - self._completer.popup().hide() + popup = self._completer.popup() + assert popup is not None + popup.hide() def hideCompleter(self) -> None: if sip.isdeleted(self._completer): # type: ignore return - self._completer.popup().hide() + popup = self._completer.popup() + assert popup is not None + popup.hide() class TagCompleter(QCompleter): @@ -120,7 +125,9 @@ def __init__( self.edit = edit self.cursor: int | None = None - def splitPath(self, tags: str) -> list[str]: + def splitPath(self, tags: str | None) -> list[str]: + assert tags is not None + assert self.edit.col is not None stripped_tags = tags.strip() stripped_tags = re.sub(" +", " ", stripped_tags) self.tags = self.edit.col.tags.split(stripped_tags) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index ac11694e19c..23266ebaf4b 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -118,10 +118,13 @@ def aqt_data_folder() -> str: def openHelp(section: HelpPageArgument) -> None: + assert tr.backend is not None + backend = tr.backend() + assert backend is not None if isinstance(section, str): - link = tr.backend().help_page_link(page=HelpPage.INDEX) + section + link = backend.help_page_link(page=HelpPage.INDEX) + section else: - link = tr.backend().help_page_link(page=section) + link = backend.help_page_link(page=section) openLink(link) @@ -170,17 +173,20 @@ def __init__( b = self.addButton(button) # a translator has complained the default Qt translation is inappropriate, so we override it if button == QMessageBox.StandardButton.Discard: + assert b is not None b.setText(tr.actions_discard()) elif isinstance(button, tuple): b = self.addButton(button[0], button[1]) else: continue if callback is not None: + assert b is not None qconnect(b.clicked, partial(callback, i)) if i == default_button: self.setDefaultButton(b) if help is not None: b = self.addButton(QMessageBox.StandardButton.Help) + assert b is not None qconnect(b.clicked, lambda: openHelp(help)) self.open() @@ -316,9 +322,11 @@ def showInfo( mb.setDefaultButton(default) else: b = mb.addButton(QMessageBox.StandardButton.Ok) + assert b is not None b.setDefault(True) if help is not None: b = mb.addButton(QMessageBox.StandardButton.Help) + assert b is not None qconnect(b.clicked, lambda: openHelp(help)) b.setAutoDefault(False) return mb.exec() @@ -363,7 +371,9 @@ def showText( if copyBtn: def onCopy() -> None: - QApplication.clipboard().setText(text.toPlainText()) + clipboard = QApplication.clipboard() + assert clipboard is not None + clipboard.setText(text.toPlainText()) btn = QPushButton(tr.qt_misc_copy_to_clipboard()) qconnect(btn.clicked, onCopy) @@ -415,6 +425,7 @@ def askUser( default = QMessageBox.StandardButton.Yes r = msgfunc(parent, title, text, sb, default) if r == QMessageBox.StandardButton.Help: + assert help is not None openHelp(help) else: break @@ -431,7 +442,7 @@ def __init__( title: str = "Anki", ): QMessageBox.__init__(self, parent) - self._buttons: list[QPushButton] = [] + self._buttons: list[QPushButton | None] = [] self.setWindowTitle(title) self.help = help self.setIcon(QMessageBox.Icon.Warning) @@ -444,11 +455,13 @@ def __init__( def run(self) -> str: self.exec() - but = self.clickedButton().text() - if but == "Help": + clicked_button = self.clickedButton() + assert clicked_button is not None + txt = clicked_button.text() + if txt == "Help": # FIXME stop dialog closing? + assert self.help is not None openHelp(self.help) - txt = self.clickedButton().text() # work around KDE 'helpfully' adding accelerators to button text of Qt apps return txt.replace("&", "") @@ -504,13 +517,18 @@ def __init__( b = QDialogButtonBox(buts) # type: ignore v.addWidget(b) self.setLayout(v) - qconnect(b.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept) - qconnect(b.button(QDialogButtonBox.StandardButton.Cancel).clicked, self.reject) + ok_button = b.button(QDialogButtonBox.StandardButton.Ok) + assert ok_button is not None + qconnect(ok_button.clicked, self.accept) + + cancel_button = b.button(QDialogButtonBox.StandardButton.Cancel) + assert cancel_button is not None + qconnect(cancel_button.clicked, self.reject) + if help: - qconnect( - b.button(QDialogButtonBox.StandardButton.Help).clicked, - self.helpRequested, - ) + help_button = b.button(QDialogButtonBox.StandardButton.Help) + assert help_button is not None + qconnect(help_button.clicked, self.helpRequested) self.l.setFocus() def accept(self) -> None: @@ -520,7 +538,8 @@ def reject(self) -> None: return QDialog.reject(self) def helpRequested(self) -> None: - openHelp(self.help) + if self.help is not None: + openHelp(self.help) def getText( @@ -624,6 +643,7 @@ def getFile( if dir and key: raise Exception("expected dir or key") if not dir: + assert aqt.mw.pm.profile is not None dirkey = f"{key}Directory" dir = aqt.mw.pm.profile.get(dirkey, "") else: @@ -635,6 +655,7 @@ def getFile( else QFileDialog.FileMode.ExistingFile ) d.setFileMode(mode) + assert dir is not None if os.path.exists(dir): d.setDirectory(dir) d.setWindowTitle(title) @@ -644,6 +665,7 @@ def getFile( def accept() -> None: files = list(d.selectedFiles()) if dirkey: + assert aqt.mw.pm.profile is not None dir = os.path.dirname(files[0]) aqt.mw.pm.profile[dirkey] = dir result = files if multi else files[0] @@ -683,10 +705,11 @@ def getSaveFile( dir_description: str, key: str, ext: str, - fname: str | None = None, -) -> str: + fname: str = "", +) -> str | None: """Ask the user for a file to save. Use DIR_DESCRIPTION as config variable. The file dialog will default to open with FNAME.""" + assert aqt.mw.pm.profile is not None config_key = f"{dir_description}Directory" defaultPath = QStandardPaths.writableLocation( @@ -709,9 +732,10 @@ def getSaveFile( dir = os.path.dirname(file) aqt.mw.pm.profile[config_key] = dir # check if it exists - if os.path.exists(file): - if not askUser(tr.qt_misc_this_file_exists_are_you_sure(), parent): - return None + if os.path.exists(file) and not askUser( + tr.qt_misc_this_file_exists_are_you_sure(), parent + ): + return None return file @@ -735,6 +759,7 @@ def _qt_state_key(kind: _QtStateKeyKind, key: str) -> str: def saveGeom(widget: QWidget, key: str) -> None: # restoring a fullscreen window breaks the tab functionality of 5.15 if not widget.isFullScreen() or qtmajor == 6: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key) aqt.mw.pm.profile[key] = widget.saveGeometry() @@ -745,6 +770,7 @@ def restoreGeom( adjustSize: bool = False, default_size: tuple[int, int] | None = None, ) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key) if existing_geom := aqt.mw.pm.profile.get(key): widget.restoreGeometry(existing_geom) @@ -756,7 +782,9 @@ def restoreGeom( def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: - handle = widget.window().windowHandle() + window = widget.window() + assert window is not None + handle = window.windowHandle() if not handle: # window has not yet been shown, retry later aqt.mw.progress.timer( @@ -765,7 +793,9 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: return # ensure widget is smaller than screen bounds - geom = handle.screen().availableGeometry() + screen = handle.screen() + assert screen is not None + geom = screen.availableGeometry() wsize = widget.size() cappedWidth = min(geom.width(), wsize.width()) cappedHeight = min(geom.height(), wsize.height()) @@ -784,44 +814,52 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: def saveState(widget: QFileDialog | QMainWindow, key: str) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.STATE, key) aqt.mw.pm.profile[key] = widget.saveState() def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.STATE, key) if data := aqt.mw.pm.profile.get(key): widget.restoreState(data) def saveSplitter(widget: QSplitter, key: str) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.SPLITTER, key) aqt.mw.pm.profile[key] = widget.saveState() def restoreSplitter(widget: QSplitter, key: str) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.SPLITTER, key) if data := aqt.mw.pm.profile.get(key): widget.restoreState(data) def saveHeader(widget: QHeaderView, key: str) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.HEADER, key) aqt.mw.pm.profile[key] = widget.saveState() def restoreHeader(widget: QHeaderView, key: str) -> None: + assert aqt.mw.pm.profile is not None key = _qt_state_key(_QtStateKeyKind.HEADER, key) if state := aqt.mw.pm.profile.get(key): widget.restoreState(state) def save_is_checked(widget: QCheckBox, key: str) -> None: + assert aqt.mw.pm.profile is not None key += "IsChecked" aqt.mw.pm.profile[key] = widget.isChecked() def restore_is_checked(widget: QCheckBox, key: str) -> None: + assert aqt.mw.pm.profile is not None key += "IsChecked" if aqt.mw.pm.profile.get(key) is not None: widget.setChecked(aqt.mw.pm.profile[key]) @@ -847,8 +885,11 @@ def restore_combo_index_for_session( def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str: + assert aqt.mw.pm.profile is not None name += "BoxHistory" - text_input = comboBox.lineEdit().text() + line_edit = comboBox.lineEdit() + assert line_edit is not None + text_input = line_edit.text() if text_input in history: history.remove(text_input) history.insert(0, text_input) @@ -861,14 +902,17 @@ def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> st def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]: + assert aqt.mw.pm.profile is not None name += "BoxHistory" history = aqt.mw.pm.profile.get(name, []) comboBox.addItems([""] + history) if history: session_input = aqt.mw.pm.session.get(name) if session_input and session_input == history[0]: - comboBox.lineEdit().setText(session_input) - comboBox.lineEdit().selectAll() + line_edit = comboBox.lineEdit() + assert line_edit is not None + line_edit.setText(session_input) + line_edit.selectAll() return history @@ -980,7 +1024,7 @@ def send_to_trash(path: Path) -> None: except Exception as exc: # Linux users may not have a trash folder set up print("trash failure:", path, exc) - if path.is_dir: + if path.is_dir(): shutil.rmtree(path) else: path.unlink() @@ -1005,7 +1049,8 @@ def tooltip( class CustomLabel(QLabel): silentlyClose = True - def mousePressEvent(self, evt: QMouseEvent) -> None: + def mousePressEvent(self, evt: QMouseEvent | None) -> None: + assert evt is not None evt.accept() self.hide() @@ -1074,7 +1119,7 @@ def __init__(self) -> None: print( "MenuList will be removed; please copy it into your add-on's code if you need it." ) - self.children: list[MenuListChild] = [] + self.children: list[MenuListChild | None] = [] def addItem(self, title: str, func: Callable) -> MenuItem: item = MenuItem(title, func) @@ -1114,6 +1159,7 @@ def __init__(self, title: str) -> None: def renderTo(self, menu: QMenu) -> None: submenu = menu.addMenu(self.title) + assert submenu is not None super().renderTo(submenu) @@ -1124,6 +1170,7 @@ def __init__(self, title: str, func: Callable) -> None: def renderTo(self, qmenu: QMenu) -> None: a = qmenu.addAction(self.title) + assert a is not None qconnect(a.triggered, self.func)