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")