From 07c1ff53245cdc3e335a752ee51f726793d6b27d Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 27 Aug 2019 21:02:38 -0400 Subject: [PATCH 1/8] Port to Anki v2.1, loosen before/after hook restrictions --- .gitignore | 1 + README.md | 4 +- AddonReloader.py => __init__.py | 112 +++++++++++++++++++------------- 3 files changed, 71 insertions(+), 46 deletions(-) rename AddonReloader.py => __init__.py (55%) diff --git a/.gitignore b/.gitignore index f2dc75e..811c33e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc *.zip +.idea \ No newline at end of file diff --git a/README.md b/README.md index 9fcdf52..89300a1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ Anki addon to reload other single-file addons (under certain conditions). See the comments at the top of AddonReloader.py for details. -For Anki 2.0 only. +~~For Anki 2.0 only.~~ Now working with v2.1. -AnkiWeb: https://ankiweb.net/shared/info/348783334 (may be an older version). +~~AnkiWeb: https://ankiweb.net/shared/info/348783334 (may be an older version).~~ diff --git a/AddonReloader.py b/__init__.py similarity index 55% rename from AddonReloader.py rename to __init__.py index f2705a1..3bd91f3 100644 --- a/AddonReloader.py +++ b/__init__.py @@ -8,10 +8,10 @@ It can help speed up addon development, but should be used with caution, as unexpected results can occur. -To qualify, the target addon must contain the function - addon_reloader_before() - this is allowed to do nothing. -(addon_reloader_teardown() still works but "before" is preferred.) -The function addon_reloader_after() is optional. +The target addon can contain the functions: + +- addon_reloader_before() - optional, run before reload like a cleanup +- addon_reloader_after() - optional, run after reload Selecting "Reload addon..." from the "Tools" menu offers a choice of eligible addons. After reloading an addon from this menu, a new option appears: @@ -38,13 +38,16 @@ See my KanjiVocab addon for an example. """ +import types +import importlib + from aqt import mw -from PyQt4.QtCore import Qt, SIGNAL -from PyQt4.QtGui import * +from aqt.qt import * + class AddonChooser(QDialog): - def __init__(self, mw, modules): - QDialog.__init__(self, mw, Qt.Window) + def __init__(self, modules): + super().__init__() self.setWindowTitle("Reload addon") self.layout = QVBoxLayout(self) @@ -59,57 +62,78 @@ def __init__(self, mw, modules): buttons.rejected.connect(self.reject) self.layout.addWidget(buttons) -def chooseAddon(): - global actionRepeat + +def choose_addon(): + global action_repeat modules = {} - filenames = mw.addonManager.files() - for filename in filenames: - modname = filename.replace(".py", "") + addon_names = mw.addonManager.allAddons() + for addon_name in addon_names: + module_name = addon_name.replace(".py", "") try: - module = __import__(modname) + module = __import__(module_name) except: - continue #skip broken modules - try: - tmp = module.addon_reloader_before - except: - try: - tmp = module.addon_reloader_teardown - except: - continue #skip modules that don't have either function - modules[modname] = module + # Skip broken modules + continue + modules[module_name] = module - chooser = AddonChooser(mw, modules) + chooser = AddonChooser(modules) response = chooser.exec_() choice = chooser.choice.currentText() if response == QDialog.Rejected: return - if actionRepeat is not None: - mw.form.menuTools.removeAction(actionRepeat) - actionRepeat = None + if action_repeat is not None: + mw.form.menuTools.removeAction(action_repeat) + action_repeat = None if choice != "": - newAction = QAction("Reload " + choice, mw) - newAction.setShortcut(_("Ctrl+R")) - def reloadTheAddon(): - #take "before" in preference to "teardown", but must have one + new_action = QAction("Reload " + choice, mw) + + def reload_the_addon(): + # Call "before" if present try: before = modules[choice].addon_reloader_before except: - before = modules[choice].addon_reloader_teardown - #take "after" if present, otherwise make it do nothing + before = lambda: None + # Take "after" if present try: after = modules[choice].addon_reloader_after except: after = lambda: None - #execute the reloading + # Execute the reloading before() - reload(modules[choice]) + reload_package(modules[choice]) after() - mw.connect(newAction, SIGNAL("triggered()"), reloadTheAddon) - mw.form.menuTools.addAction(newAction) - actionRepeat = newAction - reloadTheAddon() - -actionRepeat = None -actionChoose = QAction("Reload addon...", mw) -mw.connect(actionChoose, SIGNAL("triggered()"), chooseAddon) -mw.form.menuTools.addAction(actionChoose) + new_action.triggered.connect(reload_the_addon) + mw.form.menuTools.addAction(new_action) + action_repeat = new_action + reload_the_addon() + + +def reload_package(package): + """ + Recursively reload all package's child modules + :param package: package imported via __import__() + """ + assert(hasattr(package, "__package__")) + fn = package.__file__ + fn_dir = os.path.dirname(fn) + os.sep + module_visit = {fn} + del fn + + def reload_recursive_ex(module): + importlib.reload(module) + + for module_child in vars(module).values(): + if isinstance(module_child, types.ModuleType): + fn_child = getattr(module_child, "__file__", None) + if (fn_child is not None) and fn_child.startswith(fn_dir): + if fn_child not in module_visit: + module_visit.add(fn_child) + reload_recursive_ex(module_child) + + return reload_recursive_ex(package) + + +action_repeat = None +action_choose = QAction("Reload addon...", mw) +action_choose.triggered.connect(choose_addon) +mw.form.menuTools.addAction(action_choose) From c58447a213878b0c22c49db1d278c1d787d8aad6 Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 27 Aug 2019 21:04:45 -0400 Subject: [PATCH 2/8] Clarify readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 89300a1..496c141 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # AddonReloader -Anki addon to reload other single-file addons (under certain conditions). See the comments at the top of AddonReloader.py for details. +Anki addon to reload other addons. See the comments at the top of __init__.py for details. -~~For Anki 2.0 only.~~ Now working with v2.1. +Now working with v2.1. -~~AnkiWeb: https://ankiweb.net/shared/info/348783334 (may be an older version).~~ +Original AnkiWeb: AnkiWeb: https://ankiweb.net/shared/info/348783334. From 67389873429f4c00c3e446cd1757f35f28893854 Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 27 Aug 2019 21:05:38 -0400 Subject: [PATCH 3/8] Fix markdown typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 496c141..2cf8758 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AddonReloader -Anki addon to reload other addons. See the comments at the top of __init__.py for details. +Anki addon to reload other addons. See the comments at the top of \_\_init\_\_.py for details. Now working with v2.1. From b25a46b0f5ca8239d673a246cf91d67f69084b6e Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 27 Aug 2019 21:13:31 -0400 Subject: [PATCH 4/8] Add current issues to readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2cf8758..d715541 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,11 @@ Anki addon to reload other addons. See the comments at the top of \_\_init\_\_.p Now working with v2.1. Original AnkiWeb: AnkiWeb: https://ankiweb.net/shared/info/348783334. + +## Current Issues + +- Seems to require reloading addons twice for effect to take place +- Creates new listing in Anki "Tools" menu for addon on reload + - Possible fixes: + - Add a before/after hook in the desired addon + - Look for global addon menu item data we can reference without knowing anything about the addon \ No newline at end of file From 8466c3ba0c4e2bd50df3dc4692fc160ba6b6053e Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Sat, 7 Sep 2019 14:54:36 -0400 Subject: [PATCH 5/8] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d715541..2ea1b88 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,15 @@ Now working with v2.1. Original AnkiWeb: AnkiWeb: https://ankiweb.net/shared/info/348783334. +## Notes + +- If you do a git pull between reloads, need to restart anki +- The folder of the addon must not contain dashes or anything that wouldn't work in a normal import statement + - e.g. anki_LL, not anki-LL + ## Current Issues -- Seems to require reloading addons twice for effect to take place - Creates new listing in Anki "Tools" menu for addon on reload - Possible fixes: - Add a before/after hook in the desired addon - - Look for global addon menu item data we can reference without knowing anything about the addon \ No newline at end of file + - Look for global addon menu item data we can reference without knowing anything about the addon From fe6d1c0b4a9eb0381dc9d13df575b1223348900e Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 17 Sep 2019 14:04:15 -0400 Subject: [PATCH 6/8] Fix Ctrl+R keyboard shortcut for reload _() translation function was not the function to use here. QKeySequence is what recognizes shortcuts. --- __init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/__init__.py b/__init__.py index 3bd91f3..fb4b92b 100644 --- a/__init__.py +++ b/__init__.py @@ -86,6 +86,7 @@ def choose_addon(): action_repeat = None if choice != "": new_action = QAction("Reload " + choice, mw) + new_action.setShortcut(QKeySequence("Ctrl+R")) def reload_the_addon(): # Call "before" if present From e54fe87fea0854365470e94e4e20533319c04bce Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 17 Sep 2019 15:02:06 -0400 Subject: [PATCH 7/8] Provide help for the duplicate Tools menu item problem If reloading an addon that creates a Tools menu item on init, there will be duplicate items. This has to be handled on a case-by-case basis since not all addons are initialized the same way or even have menu items. Tell users how to avoid this if they wish to do so. --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2ea1b88..2699868 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ Anki addon to reload other addons. See the comments at the top of \_\_init\_\_.p Now working with v2.1. -Original AnkiWeb: AnkiWeb: https://ankiweb.net/shared/info/348783334. +Original AnkiWeb: https://ankiweb.net/shared/info/348783334. ## Notes - If you do a git pull between reloads, need to restart anki - The folder of the addon must not contain dashes or anything that wouldn't work in a normal import statement - e.g. anki_LL, not anki-LL - -## Current Issues - -- Creates new listing in Anki "Tools" menu for addon on reload - - Possible fixes: - - Add a before/after hook in the desired addon - - Look for global addon menu item data we can reference without knowing anything about the addon +- Without modification, if your addon creates an action item in the Anki Tools menu, reloading it will create a duplicate item for each reload. + - Solved with an `addon_reloader_before` method in the addon's \_\_init\_\_.py: + - ```python + for action in mw.form.menuTools.actions(): + if action.text() == ADDON_ACTION_NAME: + mw.form.menuTools.removeAction(action) + ``` From 0c92f9788468d3d41e1cc22b2766ea6b1cce1a72 Mon Sep 17 00:00:00 2001 From: Connor Finley Date: Tue, 17 Sep 2019 16:02:11 -0400 Subject: [PATCH 8/8] Clean up comments, exceptions --- README.md | 2 +- __init__.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2699868..da92358 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Anki addon to reload other addons. See the comments at the top of \_\_init\_\_.p Now working with v2.1. -Original AnkiWeb: https://ankiweb.net/shared/info/348783334. +AnkiWeb: https://ankiweb.net/shared/info/348783334. ## Notes diff --git a/__init__.py b/__init__.py index fb4b92b..fd7a0e9 100644 --- a/__init__.py +++ b/__init__.py @@ -49,12 +49,12 @@ class AddonChooser(QDialog): def __init__(self, modules): super().__init__() self.setWindowTitle("Reload addon") - + self.layout = QVBoxLayout(self) self.choice = QComboBox() self.choice.addItems(modules.keys()) self.layout.addWidget(self.choice) - + buttons = QDialogButtonBox() buttons.addButton(QDialogButtonBox.Ok) buttons.addButton(QDialogButtonBox.Cancel) @@ -70,7 +70,7 @@ def choose_addon(): for addon_name in addon_names: module_name = addon_name.replace(".py", "") try: - module = __import__(module_name) + module = importlib.import_module(module_name) except: # Skip broken modules continue @@ -89,20 +89,20 @@ def choose_addon(): new_action.setShortcut(QKeySequence("Ctrl+R")) def reload_the_addon(): - # Call "before" if present + # Call before and after functions if present try: before = modules[choice].addon_reloader_before - except: + except AttributeError: before = lambda: None - # Take "after" if present try: after = modules[choice].addon_reloader_after - except: + except AttributeError: after = lambda: None # Execute the reloading before() reload_package(modules[choice]) after() + new_action.triggered.connect(reload_the_addon) mw.form.menuTools.addAction(new_action) action_repeat = new_action @@ -114,7 +114,7 @@ def reload_package(package): Recursively reload all package's child modules :param package: package imported via __import__() """ - assert(hasattr(package, "__package__")) + assert hasattr(package, "__package__") fn = package.__file__ fn_dir = os.path.dirname(fn) + os.sep module_visit = {fn}