diff --git a/pylib/Makefile b/pylib/Makefile index d9b3d7bea..f519323a5 100644 --- a/pylib/Makefile +++ b/pylib/Makefile @@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules RUNARGS := .SUFFIXES: -BLACKARGS := -t py36 anki tests setup.py --exclude='backend_pb2|buildinfo' +BLACKARGS := -t py36 anki tests setup.py tools/*.py --exclude='backend_pb2|buildinfo' ISORTARGS := anki tests setup.py $(shell mkdir -p .build ../dist) diff --git a/pylib/anki/hooks_gen.py b/pylib/anki/hooks_gen.py new file mode 100644 index 000000000..5283b6097 --- /dev/null +++ b/pylib/anki/hooks_gen.py @@ -0,0 +1,129 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +Code for generating parts of hooks.py +""" + +import re +from dataclasses import dataclass +from operator import attrgetter +from typing import List, Optional + + +@dataclass +class Hook: + # the name of the hook. _filter or _hook is appending automatically. + name: str + # string of the typed arguments passed to the callback, eg + # "kind: str, val: int" + cb_args: str = "" + # string of the return type. if set, hook is a filter. + return_type: Optional[str] = None + # if add-ons may be relying on the legacy hook name, add it here + legacy_hook: Optional[str] = None + + def callable(self) -> str: + "Convert args into a Callable." + types = [] + for arg in self.cb_args.split(","): + if not arg: + continue + (name, type) = arg.split(":") + types.append(type.strip()) + types_str = ", ".join(types) + return f"Callable[[{types_str}], {self.return_type or 'None'}]" + + def arg_names(self) -> List[str]: + names = [] + for arg in self.cb_args.split(","): + if not arg: + continue + (name, type) = arg.split(":") + names.append(name.strip()) + return names + + def full_name(self) -> str: + return f"{self.name}_{self.kind()}" + + def kind(self) -> str: + if self.return_type is not None: + return "filter" + else: + return "hook" + + def list_code(self) -> str: + return f"""\ +{self.full_name()}: List[{self.callable()}] = [] +""" + + def fire_code(self) -> str: + if self.return_type is not None: + # filter + return self.filter_fire_code() + else: + # hook + return self.hook_fire_code() + + def hook_fire_code(self) -> str: + arg_names = self.arg_names() + out = f"""\ +def run_{self.full_name()}({self.cb_args}) -> None: + for hook in {self.full_name()}: + try: + hook({", ".join(arg_names)}) + except: + # if the hook fails, remove it + {self.full_name()}.remove(hook) + raise +""" + if self.legacy_hook: + args = ", ".join([f'"{self.legacy_hook}"'] + arg_names) + out += f"""\ + # legacy support + runHook({args}) +""" + return out + "\n\n" + + def filter_fire_code(self) -> str: + arg_names = self.arg_names() + out = f"""\ +def run_{self.full_name()}({self.cb_args}) -> {self.return_type}: + for filter in {self.full_name()}: + try: + {arg_names[0]} = filter({", ".join(arg_names)}) + except: + # if the hook fails, remove it + {self.full_name()}.remove(filter) + raise +""" + if self.legacy_hook: + args = ", ".join([f'"{self.legacy_hook}"'] + arg_names) + out += f"""\ + # legacy support + runFilter({args}) +""" + + out += f"""\ + return {arg_names[0]} +""" + return out + "\n\n" + + +def update_file(path: str, hooks: List[Hook]): + hooks.sort(key=attrgetter("name")) + code = "" + for hook in hooks: + code += hook.list_code() + code += "\n\n" + for hook in hooks: + code += hook.fire_code() + + orig = open(path).read() + new = re.sub( + "(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n", + f"# @@AUTOGEN@@\n\n{code}# @@AUTOGEN@@\n", + orig, + ) + + open(path, "wb").write(new.encode("utf8")) diff --git a/pylib/tools/genhooks.py b/pylib/tools/genhooks.py index b3f43a48c..a9cbcb163 100644 --- a/pylib/tools/genhooks.py +++ b/pylib/tools/genhooks.py @@ -11,109 +11,7 @@ To add a new hook: """ import os -import re -from dataclasses import dataclass -from operator import attrgetter -from typing import Optional, List - - -@dataclass -class Hook: - # the name of the hook. _filter or _hook is appending automatically. - name: str - # string of the typed arguments passed to the callback, eg - # "kind: str, val: int" - cb_args: str = "" - # string of the return type. if set, hook is a filter. - return_type: Optional[str] = None - # if add-ons may be relying on the legacy hook name, add it here - legacy_hook: Optional[str] = None - - def callable(self) -> str: - "Convert args into a Callable." - types = [] - for arg in self.cb_args.split(","): - if not arg: - continue - (name, type) = arg.split(":") - types.append(type.strip()) - types_str = ", ".join(types) - return f"Callable[[{types_str}], {self.return_type or 'None'}]" - - def arg_names(self) -> List[str]: - names = [] - for arg in self.cb_args.split(","): - if not arg: - continue - (name, type) = arg.split(":") - names.append(name.strip()) - return names - - def full_name(self) -> str: - return f"{self.name}_{self.kind()}" - - def kind(self) -> str: - if self.return_type is not None: - return "filter" - else: - return "hook" - - def list_code(self) -> str: - return f"""\ -{self.full_name()}: List[{self.callable()}] = [] -""" - - def fire_code(self) -> str: - if self.return_type is not None: - # filter - return self.filter_fire_code() - else: - # hook - return self.hook_fire_code() - - def hook_fire_code(self) -> str: - arg_names = self.arg_names() - out = f"""\ -def run_{self.full_name()}({self.cb_args}) -> None: - for hook in {self.full_name()}: - try: - hook({", ".join(arg_names)}) - except: - # if the hook fails, remove it - {self.full_name()}.remove(hook) - raise -""" - if self.legacy_hook: - args = ", ".join([f'"{self.legacy_hook}"'] + arg_names) - out += f"""\ - # legacy support - runHook({args}) -""" - return out + "\n\n" - - def filter_fire_code(self) -> str: - arg_names = self.arg_names() - out = f"""\ -def run_{self.full_name()}({self.cb_args}) -> {self.return_type}: - for filter in {self.full_name()}: - try: - {arg_names[0]} = filter({", ".join(arg_names)}) - except: - # if the hook fails, remove it - {self.full_name()}.remove(filter) - raise -""" - if self.legacy_hook: - args = ", ".join([f'"{self.legacy_hook}"'] + arg_names) - out += f"""\ - # legacy support - runFilter({args}) -""" - - out += f"""\ - return {arg_names[0]} -""" - return out + "\n\n" +from anki.hooks_gen import Hook, update_file # Hook list ###################################################################### @@ -121,26 +19,9 @@ def run_{self.full_name()}({self.cb_args}) -> {self.return_type}: hooks = [ Hook(name="leech", cb_args="card: Card", legacy_hook="leech"), Hook(name="odue_invalid"), - Hook(name="mod_schema", cb_args="proceed: bool", return_type="bool") + Hook(name="mod_schema", cb_args="proceed: bool", return_type="bool"), ] -hooks.sort(key=attrgetter("name")) - -###################################################################### - -tools_dir = os.path.dirname(__file__) -hooks_py = os.path.join(tools_dir, "..", "anki", "hooks.py") - -code = "" -for hook in hooks: - code += hook.list_code() -code += "\n\n" -for hook in hooks: - code += hook.fire_code() - -orig = open(hooks_py).read() -new = re.sub("(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n", f"# @@AUTOGEN@@\n\n{code}# @@AUTOGEN@@\n", orig) - -open(hooks_py, "wb").write(new.encode("utf8")) - -print("Updated hooks.py") \ No newline at end of file +if __name__ == "__main__": + path = os.path.join(os.path.dirname(__file__), "..", "anki", "hooks.py") + update_file(path, hooks) diff --git a/qt/Makefile b/qt/Makefile index 31183d6c1..0791bab04 100644 --- a/qt/Makefile +++ b/qt/Makefile @@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules .SUFFIXES: -BLACKARGS := -t py36 aqt tests setup.py --exclude='aqt/forms|buildinfo' +BLACKARGS := -t py36 aqt tests setup.py tools/*.py --exclude='aqt/forms|buildinfo' ISORTARGS := aqt tests setup.py $(shell mkdir -p .build ../dist) @@ -35,7 +35,11 @@ TSDEPS := $(wildcard ts/src/*.ts) (cd ts && npm i && npm run build) @touch $@ -BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui .build/i18n aqt/buildinfo.py +.build/hooks: tools/genhooks.py + python tools/genhooks.py + @touch $@ + +BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui .build/hooks .build/i18n aqt/buildinfo.py # Checking ###################### diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py new file mode 100644 index 000000000..25a524e37 --- /dev/null +++ b/qt/aqt/gui_hooks.py @@ -0,0 +1,47 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +See pylib/anki/hooks.py +""" + +from __future__ import annotations + +from typing import Any, Callable, Dict, List # pylint: disable=unused-import + +from anki.hooks import runFilter, runHook # pylint: disable=unused-import + +# New hook/filter handling +############################################################################## +# The code in this section is automatically generated - any edits you make +# will be lost. To add new hooks, see ../tools/genhooks.py +# +# @@AUTOGEN@@ + +mpv_idle_hook: List[Callable[[], None]] = [] +mpv_will_play_hook: List[Callable[[str], None]] = [] + + +def run_mpv_idle_hook() -> None: + for hook in mpv_idle_hook: + try: + hook() + except: + # if the hook fails, remove it + mpv_idle_hook.remove(hook) + raise + + +def run_mpv_will_play_hook(file: str) -> None: + for hook in mpv_will_play_hook: + try: + hook(file) + except: + # if the hook fails, remove it + mpv_will_play_hook.remove(hook) + raise + # legacy support + runHook("mpvWillPlay", file) + + +# @@AUTOGEN@@ diff --git a/qt/aqt/main.py b/qt/aqt/main.py index ad834a61d..02ef36b69 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -30,6 +30,7 @@ from anki.hooks import addHook, runFilter, runHook from anki.lang import _, ngettext from anki.storage import Collection from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields +from aqt import gui_hooks from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * from aqt.qt import sip @@ -1158,9 +1159,10 @@ Difference to correct time: %s.""" addHook("remNotes", self.onRemNotes) hooks.odue_invalid_hook.append(self.onOdueInvalid) - addHook("mpvWillPlay", self.onMpvWillPlay) - addHook("mpvIdleHook", self.onMpvIdle) - self._activeWindowOnPlay = None + gui_hooks.mpv_will_play_hook.append(self.on_mpv_will_play) + gui_hooks.mpv_idle_hook.append(self.on_mpv_idle) + + self._activeWindowOnPlay: Optional[QWidget] = None def onOdueInvalid(self): showWarning( @@ -1175,13 +1177,13 @@ and if the problem comes up again, please ask on the support site.""" head, ext = os.path.splitext(file.lower()) return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") - def onMpvWillPlay(self, file): + def on_mpv_will_play(self, file: str) -> None: if not self._isVideo(file): return self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay - def onMpvIdle(self): + def on_mpv_idle(self) -> None: w = self._activeWindowOnPlay if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible(): w.activateWindow() diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 7f233c54c..e0d0be716 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -11,10 +11,11 @@ import threading import time from typing import Any, Callable, Dict, List, Optional, Tuple -from anki.hooks import addHook, runHook +from anki.hooks import addHook from anki.lang import _ from anki.sound import allSounds from anki.utils import isLin, isMac, isWin, tmpdir +from aqt import gui_hooks from aqt.mpv import MPV, MPVBase from aqt.qt import * from aqt.utils import restoreGeom, saveGeom, showWarning @@ -156,8 +157,8 @@ class MpvManager(MPV): def __init__(self) -> None: super().__init__(window_id=None, debug=False) - def queueFile(self, file) -> None: - runHook("mpvWillPlay", file) + def queueFile(self, file: str) -> None: + gui_hooks.run_mpv_will_play_hook(file) path = os.path.join(os.getcwd(), file) self.command("loadfile", path, "append-play") @@ -172,7 +173,7 @@ class MpvManager(MPV): self.command("seek", secs, "relative") def on_idle(self) -> None: - runHook("mpvIdleHook") + gui_hooks.run_mpv_idle_hook() def setMpvConfigBase(base) -> None: diff --git a/qt/tools/genhooks.py b/qt/tools/genhooks.py new file mode 100644 index 000000000..503b122fd --- /dev/null +++ b/qt/tools/genhooks.py @@ -0,0 +1,22 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +See pylib/tools/genhooks.py for more info. +""" + +import os + +from anki.hooks_gen import Hook, update_file + +# Hook list +###################################################################### + +hooks = [ + Hook(name="mpv_idle"), + Hook(name="mpv_will_play", cb_args="file: str", legacy_hook="mpvWillPlay"), +] + +if __name__ == "__main__": + path = os.path.join(os.path.dirname(__file__), "..", "aqt", "gui_hooks.py") + update_file(path, hooks)