From cb1d5de3d7004f8b5b1c95bdfa78ad12cda150fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Thu, 9 Jan 2025 18:39:10 +0100 Subject: [PATCH] fix: plugin enable/disable/enable If we enable, then disable, then enable a plugin again within the same call to Tutor, then the plugin module is not imported properly the second time. This is because it remains in the import cache. We discovered this while implementing a long-running web app for Tutor. --- .../20250109_184104_regis_plugin_unload.md | 1 + tutor/plugins/v1.py | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelog.d/20250109_184104_regis_plugin_unload.md diff --git a/changelog.d/20250109_184104_regis_plugin_unload.md b/changelog.d/20250109_184104_regis_plugin_unload.md new file mode 100644 index 0000000000..390bf434bb --- /dev/null +++ b/changelog.d/20250109_184104_regis_plugin_unload.md @@ -0,0 +1 @@ +- [Bugfix] Properly reload a plugin module on enable/disable/enable. This is an edge case that should not have affected anyone. (by @regisb) diff --git a/tutor/plugins/v1.py b/tutor/plugins/v1.py index 88982b6f67..d4a74388d3 100644 --- a/tutor/plugins/v1.py +++ b/tutor/plugins/v1.py @@ -1,10 +1,12 @@ import importlib.util import os from glob import glob +import sys import importlib_metadata from tutor import hooks +from tutor.types import Config from .base import PLUGINS_ROOT @@ -71,8 +73,34 @@ def discover_package(entrypoint: importlib_metadata.EntryPoint) -> None: dist_version = entrypoint.dist.version if entrypoint.dist else "Unknown" hooks.Filters.PLUGINS_INFO.add_item((name, dist_version)) - # Import module on enable @hooks.Actions.PLUGIN_LOADED.add() def load(plugin_name: str) -> None: + """ + Import module on enable. + """ if name == plugin_name: importlib.import_module(entrypoint.value) + + # Remove module from cache on disable + @hooks.Actions.PLUGIN_UNLOADED.add() + def unload(plugin_name: str, _root: str, _config: Config) -> None: + """ + Remove plugin module from import cache on disable. + + This is necessary in one particular use case: when a plugin is enabled, + disabled, and enabled again -- all within the same call to Tutor. In such a + case, the following happens: + + 1. plugin enabled: the plugin module is imported. It is automatically added by + Python to the import cache. + 2. plugin disabled: action and filter callbacks are removed, but the module + remains in the import cache. + 3. plugin enabled again: the plugin module is imported. But because it's in the + import cache, the module instructions are not executed again. + + This is not supposed to happen when we run Tutor normally from the CLI. But when + running a long-lived process, such as a web app, where a plugin might be enabled + and disabled multiple times, this becomes an issue. + """ + if name == plugin_name and entrypoint.value in sys.modules: + sys.modules.pop(entrypoint.value)