Repository: lektor/lektor

Issue:
TypeErrors raised by plugin hooks are sometimes silently ignored
Hi, it took a while to identify the culprit of this bug. And I honestly cannot imagine what might cause it.
But this simple plugin example shows what is going wrong:

```py
from lektor.pluginsystem import Plugin

class TrashPlugin(Plugin):
    def on_before_build(self, builder, source, **extra):
        try:
            self.custom_attr
        except AttributeError:
            self.custom_attr = 'a'
            raise TypeError('FAIL')
```

Instead of showing the callstack with the `TypeError`, lektor somehow forgets the exception and continues happily.
The only hint that something gone wrong is this warning:

> /lektor/pluginsystem.py:174: DeprecationWarning: The plugin "trash" function "on_before_build" does not accept extra_flags. It should be updated to accept `**extra` so that it will not break if new parameters are passed to it by newer versions of Lektor.
  warnings.warn(

(which is extra frustrating because ... "c'mon, its there" 😆 )

Discussion:

Patch:
diff --git a/lektor/pluginsystem.py b/lektor/pluginsystem.py
index 4258870..22058ca 100644
--- a/lektor/pluginsystem.py
+++ b/lektor/pluginsystem.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import inspect
 import os
 import sys
 import warnings
@@ -188,24 +189,31 @@ class PluginController:
         return self.env.plugins.values()
 
     def emit(self, event, **kwargs):
+        """Invoke event hook for all plugins that support it.
 
+        Any ``kwargs`` are passed to the hook methods.
+
+        Returns a dict mapping plugin ids to hook method return values.
+        """
         rv = {}
-        kwargs["extra_flags"] = process_extra_flags(self.extra_flags)
+        extra_flags = process_extra_flags(self.extra_flags)
         funcname = "on_" + event.replace("-", "_")
         for plugin in self.iter_plugins():
             handler = getattr(plugin, funcname, None)
             if handler is not None:
+                kw = {**kwargs, "extra_flags": extra_flags}
                 try:
-                    rv[plugin.id] = handler(**kwargs)
+                    inspect.signature(handler).bind(**kw)
                 except TypeError:
-                    old_style_kwargs = kwargs.copy()
-                    old_style_kwargs.pop("extra_flags")
-                    rv[plugin.id] = handler(**old_style_kwargs)
+                    del kw["extra_flags"]
+                rv[plugin.id] = handler(**kw)
+                if "extra_flags" not in kw:
                     warnings.warn(
-                        'The plugin "{}" function "{}" does not accept extra_flags. '
+                        f"The plugin {plugin.id!r} function {funcname!r} does not "
+                        "accept extra_flags. "
                         "It should be updated to accept `**extra` so that it will "
                         "not break if new parameters are passed to it by newer "
-                        "versions of Lektor.".format(plugin.id, funcname),
+                        "versions of Lektor.",
                         DeprecationWarning,
                     )
         return rv


Test patch:
diff --git a/tests/test_pluginsystem.py b/tests/test_pluginsystem.py
index c0b7f85..680907b 100644
--- a/tests/test_pluginsystem.py
+++ b/tests/test_pluginsystem.py
@@ -48,6 +48,13 @@ class DummyPlugin(Plugin):
         self.calls.append({"event": "legacy-event"})
         return "legacy-event return value"
 
+    def on_one_type_error(self, **kwargs):
+        """Raises TypeError only on the first call."""
+        self.calls.append({"event": "one-type-error"})
+        if len(self.calls) == 1:
+            raise TypeError("test")
+        return "one-type-error return value"
+
 
 @pytest.fixture(autouse=True)
 def dummy_plugin_calls(monkeypatch):
@@ -331,6 +338,13 @@ class TestPluginController:
             rv = plugin_controller.emit("legacy-event")
         assert rv == {dummy_plugin.id: "legacy-event return value"}
 
+    def test_emit_is_not_confused_by_type_error(self, plugin_controller, dummy_plugin):
+        # Excercises https://github.com/lektor/lektor/issues/1085
+        with pytest.raises(TypeError):
+            plugin_controller.emit("one-type-error")
+        rv = plugin_controller.emit("one-type-error")
+        assert rv == {dummy_plugin.id: "one-type-error return value"}
+
 
 @pytest.mark.usefixtures("dummy_plugin_distribution")
 def test_cli_integration(project, cli_runner, monkeypatch):