mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
fix: isolate plugin CLI import side effects
This commit is contained in:
parent
d926b487f8
commit
5c02eb4136
2 changed files with 86 additions and 13 deletions
|
|
@ -127,7 +127,13 @@ def _installed_manifest(plugin: InstalledPlugin) -> PluginManifest:
|
|||
path = Path(distribution.locate_file(relative))
|
||||
if path.is_file():
|
||||
return parse_plugin_manifest(path.read_text(encoding="utf-8"), plugin_name=plugin.name)
|
||||
return load_package_manifest(plugin.target, plugin_name=plugin.name)
|
||||
# Editable installs may not expose package data through ``locate_file``.
|
||||
# Importlib resources then has to import the package, whose ``__init__``
|
||||
# may still contain legacy registration side effects.
|
||||
from .components.component_registry import R
|
||||
|
||||
with R.preserve(allow_mutation=True):
|
||||
return load_package_manifest(plugin.target, plugin_name=plugin.name)
|
||||
|
||||
|
||||
def _plugin_details(plugin: InstalledPlugin) -> dict:
|
||||
|
|
@ -233,6 +239,7 @@ def _validate_installed(name: str) -> list[str]:
|
|||
|
||||
|
||||
def _validate_local(path: Path) -> list[str]:
|
||||
from .components.component_registry import R
|
||||
from .plugin import Backend, Plugin, _load_backend
|
||||
|
||||
project_file = path if path.name == "pyproject.toml" else path / "pyproject.toml"
|
||||
|
|
@ -249,18 +256,21 @@ def _validate_local(path: Path) -> list[str]:
|
|||
plugins = []
|
||||
sys.path.insert(0, str(source_root.resolve()))
|
||||
try:
|
||||
for name, package in entry_points.items():
|
||||
if not isinstance(name, str) or not isinstance(package, str) or ":" in package:
|
||||
raise ValueError("Local validation requires package-only manifest entry points")
|
||||
manifest_path = source_root.joinpath(*package.split(".")).joinpath(PLUGIN_MANIFEST)
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(f"Plugin manifest not found: {manifest_path}")
|
||||
manifest = parse_plugin_manifest(manifest_path.read_text(encoding="utf-8"), plugin_name=name)
|
||||
backends = tuple(
|
||||
Backend(backend_name, _load_backend(target, plugin_name=name))
|
||||
for backend_name, target in manifest.backends.items()
|
||||
)
|
||||
plugins.append(Plugin(name=name, backends=backends, config=manifest.application_defaults))
|
||||
# Match installed-plugin loading: imports may execute compatibility
|
||||
# decorators, but they must not mutate the frozen built-in template.
|
||||
with R.preserve(allow_mutation=True):
|
||||
for name, package in entry_points.items():
|
||||
if not isinstance(name, str) or not isinstance(package, str) or ":" in package:
|
||||
raise ValueError("Local validation requires package-only manifest entry points")
|
||||
manifest_path = source_root.joinpath(*package.split(".")).joinpath(PLUGIN_MANIFEST)
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(f"Plugin manifest not found: {manifest_path}")
|
||||
manifest = parse_plugin_manifest(manifest_path.read_text(encoding="utf-8"), plugin_name=name)
|
||||
backends = tuple(
|
||||
Backend(backend_name, _load_backend(target, plugin_name=name))
|
||||
for backend_name, target in manifest.backends.items()
|
||||
)
|
||||
plugins.append(Plugin(name=name, backends=backends, config=manifest.application_defaults))
|
||||
_validate_plugins(plugins)
|
||||
finally:
|
||||
sys.path.remove(str(source_root.resolve()))
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from types import SimpleNamespace
|
|||
|
||||
from reme import plugin_cli as plugin_cli_module
|
||||
from reme import reme as reme_module
|
||||
from reme.components import R
|
||||
from reme.enumeration import ComponentEnum
|
||||
|
||||
|
||||
class _FakeDistribution:
|
||||
|
|
@ -94,6 +96,34 @@ def test_show_plugin_reads_manifest_without_importing_backends(monkeypatch, tmp_
|
|||
assert "auto_fin" in output
|
||||
|
||||
|
||||
def test_show_plugin_preserves_registry_when_editable_fallback_imports_package(monkeypatch, tmp_path, capsys):
|
||||
package = tmp_path / "fallback_plugin"
|
||||
package.mkdir()
|
||||
(package / "__init__.py").write_text("from .backend import FallbackStep\n", encoding="utf-8")
|
||||
(package / "backend.py").write_text(
|
||||
"from reme.components import ComponentMixin, R\n"
|
||||
"from reme.enumeration import ComponentEnum\n"
|
||||
"@R.register('fallback_import_side_effect')\n"
|
||||
"class FallbackStep(ComponentMixin):\n"
|
||||
" component_type = ComponentEnum.STEP\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(package / "plugin.yaml").write_text(
|
||||
"""backends:
|
||||
fallback_step: fallback_plugin.backend:FallbackStep
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
entry = _FakeEntryPoint("fallback", "fallback_plugin", _FakeDistribution(tmp_path / "missing"))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
monkeypatch.setattr(plugin_cli_module.metadata, "entry_points", lambda: _FakeEntryPoints([entry]))
|
||||
|
||||
assert plugin_cli_module.plugin_cli(["show", "fallback"]) == 0
|
||||
|
||||
assert "fallback_step" in capsys.readouterr().out
|
||||
assert R.get(ComponentEnum.STEP, "fallback_import_side_effect") is None
|
||||
|
||||
|
||||
def test_install_uses_current_python_pip(monkeypatch, capsys):
|
||||
commands = []
|
||||
monkeypatch.setattr(plugin_cli_module, "_run_pip", lambda command: commands.append(command) or 0)
|
||||
|
|
@ -146,6 +176,39 @@ def test_validate_local_auto_fin_project():
|
|||
assert names == ["auto-fin"]
|
||||
|
||||
|
||||
def test_validate_local_preserves_registry_during_backend_imports(tmp_path):
|
||||
package = tmp_path / "src" / "decorated_plugin"
|
||||
package.mkdir(parents=True)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"[project]\n"
|
||||
"name = 'decorated-plugin'\n"
|
||||
"version = '0.1.0'\n"
|
||||
"[project.entry-points.'reme.plugins']\n"
|
||||
"decorated = 'decorated_plugin'\n"
|
||||
"[tool.setuptools.package-dir]\n"
|
||||
"'' = 'src'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(package / "__init__.py").write_text("", encoding="utf-8")
|
||||
(package / "backend.py").write_text(
|
||||
"from reme.components import ComponentMixin, R\n"
|
||||
"from reme.enumeration import ComponentEnum\n"
|
||||
"@R.register('local_import_side_effect')\n"
|
||||
"class DecoratedStep(ComponentMixin):\n"
|
||||
" component_type = ComponentEnum.STEP\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(package / "plugin.yaml").write_text(
|
||||
"""backends:
|
||||
decorated_step: decorated_plugin.backend:DecoratedStep
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert plugin_cli_module._validate_local(tmp_path) == ["decorated"]
|
||||
assert R.get(ComponentEnum.STEP, "local_import_side_effect") is None
|
||||
|
||||
|
||||
def test_plugin_command_errors_are_clean(monkeypatch, capsys):
|
||||
monkeypatch.setattr(plugin_cli_module.metadata, "entry_points", _FakeEntryPoints)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue