fix(workspace): register plugins_registry as sys.modules shim before loading adapters
KI-296 fix: when the PyPI-installed runtime (molecule-ai-workspace-runtime 0.1.129+) ships plugins_registry as molecule_runtime.plugins_registry (a subpackage), plugin adapter files that do ``from plugins_registry import ...`` as a top-level name fail with ModuleNotFoundError because Python's import system cannot find a top-level ``plugins_registry`` package. The fix in plugins_registry/__init__.py:_load_module_from_path() registers molecule_runtime.plugins_registry as ``plugins_registry`` in sys.modules before exec'ing any plugin adapter file, so the top-level import resolves correctly in both environments: - PyPI wheel (molecule_runtime.plugins_registry → sys.modules["plugins_registry"]) - molecule-core workspace source (top-level workspace/plugins_registry already on sys.path; the setdefault is a no-op) Submodules (builtins, protocol, raw_drop) are also registered so adapters that import ``from plugins_registry.builtins import ...`` work without error. Added test_load_module_from_path_registers_plugins_registry_sys_modules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
716ec95b7d
commit
c814aa2210
@ -51,6 +51,32 @@ 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."""
|
||||
|
||||
# KI-296: Before exec'ing plugin-adapter files (which import
|
||||
# ``from plugins_registry import ...`` as a top-level name), register
|
||||
# the molecule-runtime subpackage as ``plugins_registry`` in sys.modules.
|
||||
# In the molecule-core workspace source this is already a top-level package,
|
||||
# so the setdefault is a no-op. In the PyPI-installed runtime wheel
|
||||
# (molecule-ai-workspace-runtime 0.1.129+), the package ships as
|
||||
# ``molecule_runtime.plugins_registry`` and without this shim every
|
||||
# plugin adapter would fail with ModuleNotFoundError.
|
||||
import sys as _sys
|
||||
|
||||
if "plugins_registry" not in _sys.modules:
|
||||
try:
|
||||
_mr_pr = __import__("molecule_runtime.plugins_registry", fromlist=[""])
|
||||
_sys.modules["plugins_registry"] = _mr_pr
|
||||
# Also register submodules the adapters commonly import directly.
|
||||
for _sub in ("builtins", "protocol", "raw_drop"):
|
||||
_submod = getattr(_mr_pr, _sub, None)
|
||||
if _submod is not None:
|
||||
_sys.modules[f"plugins_registry.{_sub}"] = _submod
|
||||
except ImportError:
|
||||
# molecule-runtime not installed (e.g. test environment with
|
||||
# workspace/ on sys.path directly) — skip shim; the top-level
|
||||
# workspace/plugins_registry package is already findable.
|
||||
pass
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
return None
|
||||
|
||||
@ -325,3 +325,44 @@ def test_resolve_registry_missing_module_falls_through(monkeypatch, tmp_path: Pa
|
||||
monkeypatch.setattr(pr, "_REGISTRY_ROOT", tmp_path / "empty-registry")
|
||||
_, source = pr.resolve("demo-plugin", "test_runtime", plugin_root)
|
||||
assert source == AdaptorSource.RAW_DROP
|
||||
|
||||
|
||||
def test_load_module_from_path_registers_plugins_registry_sys_modules(tmp_path: Path):
|
||||
"""KI-296: _load_module_from_path registers ``plugins_registry`` in sys.modules
|
||||
before exec'ing the adapter, so adapter files that do
|
||||
``from plugins_registry import ...`` resolve correctly when the runtime is
|
||||
installed from the PyPI wheel (where the package ships as
|
||||
``molecule_runtime.plugins_registry`` rather than a top-level ``plugins_registry``).
|
||||
"""
|
||||
import sys as _sys
|
||||
import plugins_registry as pr
|
||||
|
||||
# Create a fake adapter that imports plugins_registry at top level.
|
||||
adapter_file = tmp_path / "fake_runtime_adapter.py"
|
||||
adapter_file.write_text(
|
||||
"from plugins_registry import InstallContext # noqa: F401\n"
|
||||
"from plugins_registry.builtins import AgentskillsAdaptor as Adaptor # noqa: F401\n"
|
||||
)
|
||||
|
||||
# Evict any pre-existing sys.modules entries for the shim keys so the
|
||||
# import inside _load_module_from_path actually runs.
|
||||
_saved = {
|
||||
k: _sys.modules.pop(k, None)
|
||||
for k in (
|
||||
"plugins_registry", "plugins_registry.builtins",
|
||||
"plugins_registry.protocol", "plugins_registry.raw_drop",
|
||||
"_plugin_adaptor.test.fake_runtime",
|
||||
)
|
||||
}
|
||||
|
||||
try:
|
||||
result = pr._load_module_from_path("_plugin_adaptor.test.fake_runtime", adapter_file)
|
||||
assert result is not None, "module should load without ImportError"
|
||||
assert hasattr(result, "Adaptor"), "AgentskillsAdaptor alias should be in namespace"
|
||||
finally:
|
||||
# Restore sys.modules state.
|
||||
for k, v in _saved.items():
|
||||
if v is None:
|
||||
_sys.modules.pop(k, None)
|
||||
else:
|
||||
_sys.modules[k] = v
|
||||
|
||||
Loading…
Reference in New Issue
Block a user