From d4d33061506d12b66bd2d31cfe7ae016ea38be05 Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Sun, 10 May 2026 14:17:16 +0000 Subject: [PATCH] fix(workspace): inject plugins_registry into sys.modules before loading adapters (closes #296) Plugin adapters in molecule-skill-* repos do: from plugins_registry.builtins import AgentskillsAdaptor as Adaptor But _load_module_from_path() used exec_module() with a fresh module namespace that did NOT have plugins_registry or its submodules in sys.modules, causing: ModuleNotFoundError: No module named 'plugins_registry' Fix: before exec_module(), import and register plugins_registry + all three submodules (builtins, protocol, raw_drop) in sys.modules so adapter imports resolve correctly. Follows the Option 1 recommendation from issue #296. Also adds test_resolve_plugin.py verifying the fix for both the AgentskillsAdaptor import and the full InstallContext/resolve/protocol import. Closes #296. Co-Authored-By: Claude Opus 4.7 --- workspace/plugins_registry/__init__.py | 16 +++++ .../plugins_registry/test_resolve_plugin.py | 60 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 workspace/plugins_registry/test_resolve_plugin.py diff --git a/workspace/plugins_registry/__init__.py b/workspace/plugins_registry/__init__.py index 363f26fe..33f8ceb3 100644 --- a/workspace/plugins_registry/__init__.py +++ b/workspace/plugins_registry/__init__.py @@ -51,6 +51,22 @@ class AdaptorSource: def _load_module_from_path(module_name: str, path: Path): """Import a Python file by absolute path. Returns the module or None on failure.""" + # Ensure the plugins_registry package and its submodules are importable in the + # fresh module namespace created by module_from_spec(). Plugin adapters + # (molecule-skill-*/adapters/*.py) use "from plugins_registry.builtins import ..." + # which requires plugins_registry and its submodules to already be in sys.modules. + # We import and register them before exec_module so the plugin's own + # from ... import statements resolve correctly. + import sys + import plugins_registry + sys.modules.setdefault("plugins_registry", plugins_registry) + for _sub in ("builtins", "protocol", "raw_drop"): + try: + sub = importlib.import_module(f"plugins_registry.{_sub}") + sys.modules.setdefault(f"plugins_registry.{_sub}", sub) + except Exception: + # Submodule may not exist in all versions; skip if absent. + pass spec = importlib.util.spec_from_file_location(module_name, path) if spec is None or spec.loader is None: return None diff --git a/workspace/plugins_registry/test_resolve_plugin.py b/workspace/plugins_registry/test_resolve_plugin.py new file mode 100644 index 00000000..07cf2e26 --- /dev/null +++ b/workspace/plugins_registry/test_resolve_plugin.py @@ -0,0 +1,60 @@ +"""Tests for _load_module_from_path sys.modules injection fix (issue #296). + +Verifies that plugin adapters using "from plugins_registry.builtins import ..." +can be loaded via _load_module_from_path() without ModuleNotFoundError. +""" +import sys +import tempfile +import os +from pathlib import Path + +# Ensure the plugins_registry package is importable +import plugins_registry + +from plugins_registry import _load_module_from_path + + +def test_load_adapter_with_plugins_registry_import(): + """Plugin adapter using 'from plugins_registry.builtins import ...' loads cleanly.""" + # Write a temp adapter file that does the exact import from the bug report. + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, dir=tempfile.gettempdir() + ) as f: + f.write("from plugins_registry.builtins import AgentskillsAdaptor as Adaptor\n") + f.write("assert Adaptor is not None\n") + adapter_path = Path(f.name) + + try: + module = _load_module_from_path("test_adapter", adapter_path) + assert module is not None, "module should load without error" + assert hasattr(module, "Adaptor"), "module should expose Adaptor" + finally: + os.unlink(adapter_path) + + +def test_load_adapter_with_full_plugins_registry_import(): + """Plugin adapter using 'from plugins_registry import ...' loads cleanly.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, dir=tempfile.gettempdir() + ) as f: + f.write("from plugins_registry import InstallContext, resolve\n") + f.write("from plugins_registry.protocol import PluginAdaptor\n") + f.write("assert InstallContext is not None\n") + f.write("assert resolve is not None\n") + f.write("assert PluginAdaptor is not None\n") + adapter_path = Path(f.name) + + try: + module = _load_module_from_path("test_adapter_full", adapter_path) + assert module is not None, "module should load without error" + assert hasattr(module, "InstallContext"), "module should expose InstallContext" + assert hasattr(module, "resolve"), "module should expose resolve" + assert hasattr(module, "PluginAdaptor"), "module should expose PluginAdaptor" + finally: + os.unlink(adapter_path) + + +if __name__ == "__main__": + test_load_adapter_with_plugins_registry_import() + test_load_adapter_with_full_plugins_registry_import() + print("ALL TESTS PASS")