Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pyc
*.zip
.idea
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
# 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.

AnkiWeb: https://ankiweb.net/shared/info/348783334 (may be an older version).
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
- 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)
```
121 changes: 73 additions & 48 deletions AddonReloader.py → __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -38,78 +38,103 @@
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)
self.choice = QComboBox()
self.choice.addItems(modules.keys())
self.layout.addWidget(self.choice)

buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Ok)
buttons.addButton(QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
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", "")
try:
module = __import__(modname)
except:
continue #skip broken modules
addon_names = mw.addonManager.allAddons()
for addon_name in addon_names:
module_name = addon_name.replace(".py", "")
try:
tmp = module.addon_reloader_before
module = importlib.import_module(module_name)
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)
new_action.setShortcut(QKeySequence("Ctrl+R"))

def reload_the_addon():
# Call before and after functions 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
except AttributeError:
before = lambda: None
try:
after = modules[choice].addon_reloader_after
except:
except AttributeError:
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)