Buggy add-on hooks prevent user actions

Assume this add-on code:

from anki import hooks

def on_notes_delete(collection, note_ids, foobar):
	pass

hooks.notes_will_be_deleted.append(on_notes_delete)

Note that the hook has the wrong signature because only two arguments are passed.

If the user now tries to delete notes, a Python error message “on_notes_delete() missing 1 required positional argument: ‘foobar’” pops up. No note gets deleted and the user has no idea how to fix that because the add-on that caused the problem is not mentioned in the error message.

Even the console output does not help in this case. It looks like this:

Traceback (most recent call last):
  File "concurrent.futures.thread", line 58, in run
  File "aqt.operations", line 107, in wrapped_op
  File "aqt.operations.note", line 40, in <lambda>
  File "anki.collection", line 549, in remove_notes
  File "anki.hooks_gen", line 507, in __call__
TypeError: on_notes_delete() missing 1 required positional argument: 'foobar'

The module that caused the problem is again not visible from the stack trace.

1 Like

Such errors can mostly be caught at build time.

https://addon-docs.ankiweb.net/mypy.html

1 Like

I’m afraid, I’m not with you here.

Sure the module vendor should use mypy.

But what if the module vendor is negligent enough to not use mypy or to not test that hook?

And what if the add-on suffers from bit rot and no longer works with an updated Anki version?

And what if Mallory has deliberately uploaded malicious add-on code?

Either way, the end-user is left alone with non-functional software. Deleting notes is a core feature. End-users should not be forced to disable their add-ons one by one to find the culprit that is in the way in that case, leave alone that knowledge of such debugging techniques cannot be expected from end-users.

I agree with you that in an ideal world, Anki would tell the user which add-on is to blame in such circumstances. I can’t see any technical way that could be done however, as add-ons all run in the same Python environment and can make arbitrary changes to the environment.

But the hook is called from somewhere, and at that location, it should be clear which add-on had registered that particular hook. Or do I miss something here?

I’d like to know if I’m missing something too. For example, would this work?

  1. In each hooks.some_hook.append function use inspect to get the caller filepath that would be useful to show in the error message. Since .append calls happen in add-on code the filepath would point to the add-on
  2. Change the hook list to be a list of tuples like (func, filepath)
  3. When hooks are run, you’ll have the filepath available to add to the error message, if calling the func raises an exception

Well, I found the tools/hookkslib.py file where the actual append func is defined and I can’t really understand where and how code is being called for each hook.

I suppose I (or @guido.flohr) could find out what happens by building Anki with the below print on pylib/tools/hookslib.py L88:

    def append(self, callback: {self.callable()}) -> None:
        '''{appenddoc}'''
        print(f"caller filepath: {{inspect.stack()[2].filename}}")
        self._hooks.append(callback)

It may be possible to determine the culprit in some circumstances by inspecting the stack. I’m not confident you can reliably do that across all platforms and cases (e.g. monkey patching), but if you want to at least try to handle the hooks case, a well-written PR would be welcome.