Merge pull request #723 from phwoo/typehints-for-qt-addons

Typehints for qt addons
This commit is contained in:
Damien Elmes 2020-08-02 10:08:48 +10:00 committed by GitHub
commit 0787a5d1da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -171,7 +171,7 @@ class AddonManager:
"required": ["package", "name"], "required": ["package", "name"],
} }
def __init__(self, mw: aqt.main.AnkiQt): def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw self.mw = mw
self.dirty = False self.dirty = False
f = self.mw.form f = self.mw.form
@ -194,7 +194,7 @@ class AddonManager:
def all_addon_meta(self) -> Iterable[AddonMeta]: def all_addon_meta(self) -> Iterable[AddonMeta]:
return map(self.addon_meta, self.allAddons()) return map(self.addon_meta, self.allAddons())
def addonsFolder(self, dir=None): def addonsFolder(self, dir=None) -> str:
root = self.mw.pm.addonFolder() root = self.mw.pm.addonFolder()
if not dir: if not dir:
return root return root
@ -218,7 +218,7 @@ class AddonManager:
) )
) )
def onAddonsDialog(self): def onAddonsDialog(self) -> None:
AddonsDialog(self) AddonsDialog(self)
# Metadata # Metadata
@ -246,7 +246,7 @@ class AddonManager:
self.writeAddonMeta(addon.dir_name, json_obj) self.writeAddonMeta(addon.dir_name, json_obj)
def _addonMetaPath(self, dir): def _addonMetaPath(self, dir: str) -> str:
return os.path.join(self.addonsFolder(dir), "meta.json") return os.path.join(self.addonsFolder(dir), "meta.json")
# in new code, use self.addon_meta() instead # in new code, use self.addon_meta() instead
@ -263,7 +263,7 @@ class AddonManager:
return dict() return dict()
# in new code, use write_addon_meta() instead # in new code, use write_addon_meta() instead
def writeAddonMeta(self, dir, meta): def writeAddonMeta(self, dir: str, meta: Dict[str, Any]) -> None:
path = self._addonMetaPath(dir) path = self._addonMetaPath(dir)
with open(path, "w", encoding="utf8") as f: with open(path, "w", encoding="utf8") as f:
json.dump(meta, f) json.dump(meta, f)
@ -303,7 +303,7 @@ and have been disabled: %(found)s"
def addonName(self, dir: str) -> str: def addonName(self, dir: str) -> str:
return self.addon_meta(dir).human_name() return self.addon_meta(dir).human_name()
def addonConflicts(self, dir) -> List[str]: def addonConflicts(self, dir: str) -> List[str]:
return self.addon_meta(dir).conflicts return self.addon_meta(dir).conflicts
def annotatedName(self, dir: str) -> str: def annotatedName(self, dir: str) -> str:
@ -325,7 +325,7 @@ and have been disabled: %(found)s"
all_conflicts[other_dir].append(addon.dir_name) all_conflicts[other_dir].append(addon.dir_name)
return all_conflicts return all_conflicts
def _disableConflicting(self, dir, conflicts=None): def _disableConflicting(self, dir: str, conflicts: List[str] = None) -> List[str]:
conflicts = conflicts or self.addonConflicts(dir) conflicts = conflicts or self.addonConflicts(dir)
installed = self.allAddons() installed = self.allAddons()
@ -342,7 +342,7 @@ and have been disabled: %(found)s"
# Installing and deleting add-ons # Installing and deleting add-ons
###################################################################### ######################################################################
def readManifestFile(self, zfile): def readManifestFile(self, zfile: ZipFile) -> Dict[Any, Any]:
try: try:
with zfile.open("manifest.json") as f: with zfile.open("manifest.json") as f:
data = json.loads(f.read()) data = json.loads(f.read())
@ -356,7 +356,7 @@ and have been disabled: %(found)s"
return manifest return manifest
def install( def install(
self, file: Union[IO, str], manifest: dict = None self, file: Union[IO, str], manifest: Dict[str, Any] = None
) -> Union[InstallOk, InstallError]: ) -> Union[InstallOk, InstallError]:
"""Install add-on from path or file-like object. Metadata is read """Install add-on from path or file-like object. Metadata is read
from the manifest file, with keys overriden by supplying a 'manifest' from the manifest file, with keys overriden by supplying a 'manifest'
@ -392,7 +392,7 @@ and have been disabled: %(found)s"
name=meta["name"], conflicts=found_conflicts, compatible=meta2.compatible() name=meta["name"], conflicts=found_conflicts, compatible=meta2.compatible()
) )
def _install(self, dir, zfile): def _install(self, dir: str, zfile: ZipFile) -> None:
# previously installed? # previously installed?
base = self.addonsFolder(dir) base = self.addonsFolder(dir)
if os.path.exists(base): if os.path.exists(base):
@ -417,7 +417,7 @@ and have been disabled: %(found)s"
zfile.extract(n, base) zfile.extract(n, base)
# true on success # true on success
def deleteAddon(self, dir): def deleteAddon(self, dir: str) -> bool:
try: try:
send2trash(self.addonsFolder(dir)) send2trash(self.addonsFolder(dir))
return True return True
@ -525,7 +525,7 @@ and have been disabled: %(found)s"
for item in items: for item in items:
self.update_supported_version(item) self.update_supported_version(item)
def update_supported_version(self, item: UpdateInfo): def update_supported_version(self, item: UpdateInfo) -> None:
addon = self.addon_meta(str(item.id)) addon = self.addon_meta(str(item.id))
updated = False updated = False
is_latest = addon.is_latest(item.current_branch_last_modified) is_latest = addon.is_latest(item.current_branch_last_modified)
@ -579,7 +579,7 @@ and have been disabled: %(found)s"
_configButtonActions: Dict[str, Callable[[], Optional[bool]]] = {} _configButtonActions: Dict[str, Callable[[], Optional[bool]]] = {}
_configUpdatedActions: Dict[str, Callable[[Any], None]] = {} _configUpdatedActions: Dict[str, Callable[[Any], None]] = {}
def addonConfigDefaults(self, dir): def addonConfigDefaults(self, dir: str) -> Optional[Dict[str, Any]]:
path = os.path.join(self.addonsFolder(dir), "config.json") path = os.path.join(self.addonsFolder(dir), "config.json")
try: try:
with open(path, encoding="utf8") as f: with open(path, encoding="utf8") as f:
@ -587,7 +587,7 @@ and have been disabled: %(found)s"
except: except:
return None return None
def addonConfigHelp(self, dir): def addonConfigHelp(self, dir: str) -> str:
path = os.path.join(self.addonsFolder(dir), "config.md") path = os.path.join(self.addonsFolder(dir), "config.md")
if os.path.exists(path): if os.path.exists(path):
with open(path, encoding="utf-8") as f: with open(path, encoding="utf-8") as f:
@ -595,7 +595,7 @@ and have been disabled: %(found)s"
else: else:
return "" return ""
def addonFromModule(self, module): def addonFromModule(self, module: str) -> str:
return module.split(".")[0] return module.split(".")[0]
def configAction(self, addon: str) -> Callable[[], Optional[bool]]: def configAction(self, addon: str) -> Callable[[], Optional[bool]]:
@ -607,10 +607,10 @@ and have been disabled: %(found)s"
# Schema # Schema
###################################################################### ######################################################################
def _addon_schema_path(self, dir): def _addon_schema_path(self, dir: str) -> str:
return os.path.join(self.addonsFolder(dir), "config.schema.json") return os.path.join(self.addonsFolder(dir), "config.schema.json")
def _addon_schema(self, dir): def _addon_schema(self, dir: str):
path = self._addon_schema_path(dir) path = self._addon_schema_path(dir)
try: try:
if not os.path.exists(path): if not os.path.exists(path):
@ -625,7 +625,7 @@ and have been disabled: %(found)s"
# Add-on Config API # Add-on Config API
###################################################################### ######################################################################
def getConfig(self, module: str) -> Optional[dict]: def getConfig(self, module: str) -> Optional[Dict[str, Any]]:
addon = self.addonFromModule(module) addon = self.addonFromModule(module)
# get default config # get default config
config = self.addonConfigDefaults(addon) config = self.addonConfigDefaults(addon)
@ -637,15 +637,15 @@ and have been disabled: %(found)s"
config.update(userConf) config.update(userConf)
return config return config
def setConfigAction(self, module: str, fn: Callable[[], Optional[bool]]): def setConfigAction(self, module: str, fn: Callable[[], Optional[bool]]) -> None:
addon = self.addonFromModule(module) addon = self.addonFromModule(module)
self._configButtonActions[addon] = fn self._configButtonActions[addon] = fn
def setConfigUpdatedAction(self, module: str, fn: Callable[[Any], None]): def setConfigUpdatedAction(self, module: str, fn: Callable[[Any], None]) -> None:
addon = self.addonFromModule(module) addon = self.addonFromModule(module)
self._configUpdatedActions[addon] = fn self._configUpdatedActions[addon] = fn
def writeConfig(self, module: str, conf: dict): def writeConfig(self, module: str, conf: dict) -> None:
addon = self.addonFromModule(module) addon = self.addonFromModule(module)
meta = self.addonMeta(addon) meta = self.addonMeta(addon)
meta["config"] = conf meta["config"] = conf
@ -654,18 +654,18 @@ and have been disabled: %(found)s"
# user_files # user_files
###################################################################### ######################################################################
def _userFilesPath(self, sid): def _userFilesPath(self, sid: str) -> str:
return os.path.join(self.addonsFolder(sid), "user_files") return os.path.join(self.addonsFolder(sid), "user_files")
def _userFilesBackupPath(self): def _userFilesBackupPath(self) -> str:
return os.path.join(self.addonsFolder(), "files_backup") return os.path.join(self.addonsFolder(), "files_backup")
def backupUserFiles(self, sid): def backupUserFiles(self, sid: str) -> None:
p = self._userFilesPath(sid) p = self._userFilesPath(sid)
if os.path.exists(p): if os.path.exists(p):
os.rename(p, self._userFilesBackupPath()) os.rename(p, self._userFilesBackupPath())
def restoreUserFiles(self, sid): def restoreUserFiles(self, sid: str) -> None:
p = self._userFilesPath(sid) p = self._userFilesPath(sid)
bp = self._userFilesBackupPath() bp = self._userFilesBackupPath()
# did we back up userFiles? # did we back up userFiles?
@ -678,11 +678,12 @@ and have been disabled: %(found)s"
_webExports: Dict[str, str] = {} _webExports: Dict[str, str] = {}
def setWebExports(self, module: str, pattern: str): def setWebExports(self, module: str, pattern: str) -> None:
addon = self.addonFromModule(module) addon = self.addonFromModule(module)
self._webExports[addon] = pattern self._webExports[addon] = pattern
def getWebExports(self, addon): # CHECK
def getWebExports(self, addon) -> str:
return self._webExports.get(addon) return self._webExports.get(addon)
@ -691,7 +692,7 @@ and have been disabled: %(found)s"
class AddonsDialog(QDialog): class AddonsDialog(QDialog):
def __init__(self, addonsManager: AddonManager): def __init__(self, addonsManager: AddonManager) -> None:
self.mgr = addonsManager self.mgr = addonsManager
self.mw = addonsManager.mw self.mw = addonsManager.mw
@ -716,7 +717,7 @@ class AddonsDialog(QDialog):
gui_hooks.addons_dialog_will_show(self) gui_hooks.addons_dialog_will_show(self)
self.show() self.show()
def dragEnterEvent(self, event): def dragEnterEvent(self, event: QEvent) -> None:
mime = event.mimeData() mime = event.mimeData()
if not mime.hasUrls(): if not mime.hasUrls():
return None return None
@ -725,7 +726,7 @@ class AddonsDialog(QDialog):
if all(url.toLocalFile().endswith(ext) for url in urls): if all(url.toLocalFile().endswith(ext) for url in urls):
event.acceptProposedAction() event.acceptProposedAction()
def dropEvent(self, event): def dropEvent(self, event: QEvent) -> None:
mime = event.mimeData() mime = event.mimeData()
paths = [] paths = []
for url in mime.urls(): for url in mime.urls():
@ -734,7 +735,7 @@ class AddonsDialog(QDialog):
paths.append(path) paths.append(path)
self.onInstallFiles(paths) self.onInstallFiles(paths)
def reject(self): def reject(self) -> None:
saveGeom(self, "addons") saveGeom(self, "addons")
return QDialog.reject(self) return QDialog.reject(self)
@ -756,7 +757,7 @@ class AddonsDialog(QDialog):
max = abs(addon.max_point_version) max = abs(addon.max_point_version)
return f"Anki <= 2.1.{max}" return f"Anki <= 2.1.{max}"
def should_grey(self, addon: AddonMeta): def should_grey(self, addon: AddonMeta) -> bool:
return not addon.enabled or not addon.compatible() return not addon.enabled or not addon.compatible()
def redrawAddons(self,) -> None: def redrawAddons(self,) -> None:
@ -798,19 +799,19 @@ class AddonsDialog(QDialog):
idxs = [x.row() for x in self.form.addonList.selectedIndexes()] idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
return [self.addons[idx].dir_name for idx in idxs] return [self.addons[idx].dir_name for idx in idxs]
def onlyOneSelected(self): def onlyOneSelected(self) -> Optional[str]:
dirs = self.selectedAddons() dirs = self.selectedAddons()
if len(dirs) != 1: if len(dirs) != 1:
showInfo(_("Please select a single add-on first.")) showInfo(_("Please select a single add-on first."))
return return None
return dirs[0] return dirs[0]
def onToggleEnabled(self): def onToggleEnabled(self) -> None:
for dir in self.selectedAddons(): for dir in self.selectedAddons():
self.mgr.toggleEnabled(dir) self.mgr.toggleEnabled(dir)
self.redrawAddons() self.redrawAddons()
def onViewPage(self): def onViewPage(self) -> None:
addon = self.onlyOneSelected() addon = self.onlyOneSelected()
if not addon: if not addon:
return return
@ -819,7 +820,7 @@ class AddonsDialog(QDialog):
else: else:
showWarning(_("Add-on was not downloaded from AnkiWeb.")) showWarning(_("Add-on was not downloaded from AnkiWeb."))
def onViewFiles(self): def onViewFiles(self) -> None:
# if nothing selected, open top level folder # if nothing selected, open top level folder
selected = self.selectedAddons() selected = self.selectedAddons()
if not selected: if not selected:
@ -833,7 +834,7 @@ class AddonsDialog(QDialog):
path = self.mgr.addonsFolder(addon) path = self.mgr.addonsFolder(addon)
openFolder(path) openFolder(path)
def onDelete(self): def onDelete(self) -> None:
selected = self.selectedAddons() selected = self.selectedAddons()
if not selected: if not selected:
return return
@ -852,19 +853,19 @@ class AddonsDialog(QDialog):
self.form.addonList.clearSelection() self.form.addonList.clearSelection()
self.redrawAddons() self.redrawAddons()
def onGetAddons(self): def onGetAddons(self) -> None:
obj = GetAddons(self) obj = GetAddons(self)
if obj.ids: if obj.ids:
download_addons(self, self.mgr, obj.ids, self.after_downloading) download_addons(self, self.mgr, obj.ids, self.after_downloading)
def after_downloading(self, log: List[DownloadLogEntry]): def after_downloading(self, log: List[DownloadLogEntry]) -> None:
self.redrawAddons() self.redrawAddons()
if log: if log:
show_log_to_user(self, log) show_log_to_user(self, log)
else: else:
tooltip(_("No updates available.")) tooltip(_("No updates available."))
def onInstallFiles(self, paths: Optional[List[str]] = None): def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
if not paths: if not paths:
key = _("Packaged Anki Add-on") + " (*{})".format(self.mgr.ext) key = _("Packaged Anki Add-on") + " (*{})".format(self.mgr.ext)
paths = getFile( paths = getFile(
@ -876,12 +877,13 @@ class AddonsDialog(QDialog):
installAddonPackages(self.mgr, paths, parent=self) installAddonPackages(self.mgr, paths, parent=self)
self.redrawAddons() self.redrawAddons()
return None
def check_for_updates(self): def check_for_updates(self) -> None:
tooltip(_("Checking...")) tooltip(_("Checking..."))
check_and_prompt_for_updates(self, self.mgr, self.after_downloading) check_and_prompt_for_updates(self, self.mgr, self.after_downloading)
def onConfig(self): def onConfig(self) -> None:
addon = self.onlyOneSelected() addon = self.onlyOneSelected()
if not addon: if not addon:
return return
@ -906,7 +908,7 @@ class AddonsDialog(QDialog):
class GetAddons(QDialog): class GetAddons(QDialog):
def __init__(self, dlg): def __init__(self, dlg) -> None:
QDialog.__init__(self, dlg) QDialog.__init__(self, dlg)
self.addonsDlg = dlg self.addonsDlg = dlg
self.mgr = dlg.mgr self.mgr = dlg.mgr
@ -922,10 +924,10 @@ class GetAddons(QDialog):
self.exec_() self.exec_()
saveGeom(self, "getaddons") saveGeom(self, "getaddons")
def onBrowse(self): def onBrowse(self) -> None:
openLink(aqt.appShared + "addons/2.1") openLink(aqt.appShared + "addons/2.1")
def accept(self): def accept(self) -> None:
# get codes # get codes
try: try:
ids = [int(n) for n in self.form.code.text().split()] ids = [int(n) for n in self.form.code.text().split()]
@ -1091,11 +1093,11 @@ class DownloaderInstaller(QObject):
% dict(a=len(self.log) + 1, b=len(self.ids), kb=self.dl_bytes / 1024) % dict(a=len(self.log) + 1, b=len(self.ids), kb=self.dl_bytes / 1024)
) )
def _download_all(self): def _download_all(self) -> None:
for id in self.ids: for id in self.ids:
self.log.append(download_and_install_addon(self.mgr, self.client, id)) self.log.append(download_and_install_addon(self.mgr, self.client, id))
def _download_done(self, future): def _download_done(self, future: Future) -> None:
self.mgr.mw.progress.finish() self.mgr.mw.progress.finish()
# qt gets confused if on_done() opens new windows while the progress # qt gets confused if on_done() opens new windows while the progress
# modal is still cleaning up # modal is still cleaning up
@ -1168,8 +1170,8 @@ def check_and_prompt_for_updates(
parent: QWidget, parent: QWidget,
mgr: AddonManager, mgr: AddonManager,
on_done: Callable[[List[DownloadLogEntry]], None], on_done: Callable[[List[DownloadLogEntry]], None],
): ) -> None:
def on_updates_received(client: HttpClient, items: List[Dict]): def on_updates_received(client: HttpClient, items: List[Dict]) -> None:
handle_update_info(parent, mgr, client, items, on_done) handle_update_info(parent, mgr, client, items, on_done)
check_for_updates(mgr, on_updates_received) check_for_updates(mgr, on_updates_received)
@ -1177,13 +1179,13 @@ def check_and_prompt_for_updates(
def check_for_updates( def check_for_updates(
mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None] mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None]
): ) -> None:
client = HttpClient() client = HttpClient()
def check(): def check() -> List[Dict]:
return fetch_update_info(client, mgr.ankiweb_addons()) return fetch_update_info(client, mgr.ankiweb_addons())
def update_info_received(future: Future): def update_info_received(future: Future) -> None:
# if syncing/in profile screen, defer message delivery # if syncing/in profile screen, defer message delivery
if not mgr.mw.col: if not mgr.mw.col:
mgr.mw.progress.timer( mgr.mw.progress.timer(
@ -1276,7 +1278,7 @@ def prompt_to_update(
class ConfigEditor(QDialog): class ConfigEditor(QDialog):
def __init__(self, dlg, addon, conf): def __init__(self, dlg, addon, conf) -> None:
super().__init__(dlg) super().__init__(dlg)
self.addon = addon self.addon = addon
self.conf = conf self.conf = conf
@ -1298,24 +1300,24 @@ class ConfigEditor(QDialog):
) )
self.show() self.show()
def onRestoreDefaults(self): def onRestoreDefaults(self) -> None:
default_conf = self.mgr.addonConfigDefaults(self.addon) default_conf = self.mgr.addonConfigDefaults(self.addon)
self.updateText(default_conf) self.updateText(default_conf)
tooltip(_("Restored defaults"), parent=self) tooltip(_("Restored defaults"), parent=self)
def setupFonts(self): def setupFonts(self) -> None:
font_mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) font_mono = QFontDatabase.systemFont(QFontDatabase.FixedFont)
font_mono.setPointSize(font_mono.pointSize() + 1) font_mono.setPointSize(font_mono.pointSize() + 1)
self.form.editor.setFont(font_mono) self.form.editor.setFont(font_mono)
def updateHelp(self): def updateHelp(self) -> None:
txt = self.mgr.addonConfigHelp(self.addon) txt = self.mgr.addonConfigHelp(self.addon)
if txt: if txt:
self.form.label.setText(txt) self.form.label.setText(txt)
else: else:
self.form.scrollArea.setVisible(False) self.form.scrollArea.setVisible(False)
def updateText(self, conf): def updateText(self, conf: Dict[str, Any]) -> None:
text = json.dumps( text = json.dumps(
conf, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": "), conf, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": "),
) )
@ -1324,15 +1326,15 @@ class ConfigEditor(QDialog):
if isMac: if isMac:
self.form.editor.repaint() self.form.editor.repaint()
def onClose(self): def onClose(self) -> None:
saveGeom(self, "addonconf") saveGeom(self, "addonconf")
saveSplitter(self.form.splitter, "addonconf") saveSplitter(self.form.splitter, "addonconf")
def reject(self): def reject(self) -> None:
self.onClose() self.onClose()
super().reject() super().reject()
def accept(self): def accept(self) -> None:
txt = self.form.editor.toPlainText() txt = self.form.editor.toPlainText()
txt = gui_hooks.addon_config_editor_will_save_json(txt) txt = gui_hooks.addon_config_editor_will_save_json(txt)
try: try: