From 55c43a3b09ab89f29fd5ca60b15d137e09226b72 Mon Sep 17 00:00:00 2001 From: stephantul Date: Thu, 4 Jun 2026 10:25:50 +0200 Subject: [PATCH] chore: make functions public, remove callable paths --- src/semble/installer/agents.py | 17 ++++++----------- src/semble/installer/config.py | 4 ++-- src/semble/installer/installer.py | 16 ++++++++-------- tests/test_installer.py | 20 ++++++++++---------- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/semble/installer/agents.py b/src/semble/installer/agents.py index 2b5996d..14fb533 100644 --- a/src/semble/installer/agents.py +++ b/src/semble/installer/agents.py @@ -5,13 +5,12 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Callable, Literal +from typing import Literal _HOME = Path.home() Action = Literal["created", "updated", "unchanged", "not-found", "removed", "error", "skipped"] Mode = Literal["install", "uninstall"] -PathResolver = Callable[[], Path] SEMBLE_START = "" SEMBLE_END = "" @@ -39,7 +38,7 @@ "args": ["--from", "semble[mcp]", "semble"], } -_INSTRUCTIONS = f"""\ +INSTRUCTIONS = f"""\ {SEMBLE_START} ## Semble Code Search @@ -78,15 +77,11 @@ class McpConfig: """MCP integration config for one agent.""" - path: Path | PathResolver + path: Path key: str entry: dict[str, object] format: Literal["json", "toml"] = "json" - def resolved_path(self) -> Path: - """Return the resolved config path.""" - return self.path() if callable(self.path) else self.path - @dataclass(frozen=True) class WriteResult: @@ -110,7 +105,7 @@ class AgentTarget: def resolved_mcp_path(self) -> Path | None: """Return the resolved MCP config path, or None if MCP is unsupported.""" - return self.mcp.resolved_path() if self.mcp else None + return self.mcp.path if self.mcp else None def _opencode_mcp_path() -> Path: @@ -175,7 +170,7 @@ def _vscode_mcp_path() -> Path: display_name="Opencode", binary="opencode", config_dir=_HOME / ".config" / "opencode", - mcp=McpConfig(_opencode_mcp_path, "mcp", _OPENCODE_SERVER_CONFIG), + mcp=McpConfig(_opencode_mcp_path(), "mcp", _OPENCODE_SERVER_CONFIG), instructions_path=_HOME / ".config" / "opencode" / "AGENTS.md", subagent_path=_HOME / ".config" / "opencode" / "agents" / "semble-search.md", ), @@ -201,7 +196,7 @@ def _vscode_mcp_path() -> Path: display_name="VS Code", binary="code", config_dir=None, - mcp=McpConfig(_vscode_mcp_path, "servers", _STDIO_SERVER_CONFIG), + mcp=McpConfig(_vscode_mcp_path(), "servers", _STDIO_SERVER_CONFIG), instructions_path=None, ), AgentTarget( diff --git a/src/semble/installer/config.py b/src/semble/installer/config.py index e5608e6..9881c20 100644 --- a/src/semble/installer/config.py +++ b/src/semble/installer/config.py @@ -230,7 +230,7 @@ def _strip_toml_section(text: str, header: str) -> str: return "".join(result) -def _merge_toml_block(path: Path) -> Action: +def merge_toml_block(path: Path) -> Action: """Add (or refresh) the semble [mcp_servers.semble] table in a Codex config.toml as text.""" path.parent.mkdir(parents=True, exist_ok=True) existed = path.exists() @@ -242,7 +242,7 @@ def _merge_toml_block(path: Path) -> Action: return "created" if not existed else "updated" -def _remove_toml_block(path: Path) -> Action: +def remove_toml_block(path: Path) -> Action: """Remove the semble [mcp_servers.semble] table from a Codex config.toml, leaving the rest.""" if not path.exists(): return "not-found" diff --git a/src/semble/installer/installer.py b/src/semble/installer/installer.py index 895a6ef..8e5fba0 100644 --- a/src/semble/installer/installer.py +++ b/src/semble/installer/installer.py @@ -9,19 +9,19 @@ import questionary from semble.installer.agents import ( - _INSTRUCTIONS, AGENTS, + INSTRUCTIONS, AgentTarget, Mode, WriteResult, is_detected, ) from semble.installer.config import ( - _merge_toml_block, - _remove_toml_block, merge_json_member, + merge_toml_block, remove_json_member, remove_marked, + remove_toml_block, replace_or_append_marked, ) @@ -51,14 +51,14 @@ class _Integration: def merge_mcp(agent: AgentTarget) -> WriteResult: """Add the semble MCP entry to the agent's config.""" assert agent.mcp is not None - path = agent.mcp.resolved_path() + path = agent.mcp.path return WriteResult(path, merge_json_member(path, agent.mcp.key, "semble", agent.mcp.entry)) def remove_mcp(agent: AgentTarget) -> WriteResult: """Remove the semble MCP entry from the agent's config.""" assert agent.mcp is not None - path = agent.mcp.resolved_path() + path = agent.mcp.path return WriteResult(path, remove_json_member(path, agent.mcp.key, "semble")) @@ -66,9 +66,9 @@ def _apply_mcp(agent: AgentTarget, mode: Mode) -> WriteResult | None: """Apply or remove the MCP server integration for one agent.""" if agent.mcp is None: return None - path = agent.mcp.resolved_path() + path = agent.mcp.path if agent.mcp.format == "toml": - return WriteResult(path, _merge_toml_block(path) if mode == "install" else _remove_toml_block(path)) + return WriteResult(path, merge_toml_block(path) if mode == "install" else remove_toml_block(path)) return merge_mcp(agent) if mode == "install" else remove_mcp(agent) @@ -77,7 +77,7 @@ def _apply_instructions(agent: AgentTarget, mode: Mode) -> WriteResult | None: path = agent.instructions_path if path is None: return None - action = replace_or_append_marked(path, _INSTRUCTIONS) if mode == "install" else remove_marked(path) + action = replace_or_append_marked(path, INSTRUCTIONS) if mode == "install" else remove_marked(path) return WriteResult(path, action) diff --git a/tests/test_installer.py b/tests/test_installer.py index 11a57d9..fb27353 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -16,9 +16,9 @@ ) from semble.installer.config import ( _CODEX_MCP_HEADER, - _merge_toml_block, - _remove_toml_block, + merge_toml_block, remove_marked, + remove_toml_block, replace_or_append_marked, ) from semble.installer.installer import ( @@ -206,14 +206,14 @@ def test_codex_toml_merge_and_remove(tmp_path): """The Codex TOML helpers add/remove [mcp_servers.semble] while preserving other tables and keys.""" f = tmp_path / "config.toml" f.write_text('model = "gpt-5"\n\n[mcp_servers.other]\ncommand = "x"\n') - assert _merge_toml_block(f) == "updated" + assert merge_toml_block(f) == "updated" text = f.read_text() assert _CODEX_MCP_HEADER in text assert 'model = "gpt-5"' in text assert "[mcp_servers.other]" in text - assert _merge_toml_block(f) == "unchanged" # idempotent + assert merge_toml_block(f) == "unchanged" # idempotent - assert _remove_toml_block(f) == "removed" + assert remove_toml_block(f) == "removed" text = f.read_text() assert _CODEX_MCP_HEADER not in text assert "[mcp_servers.other]" in text # only the semble table is removed @@ -223,7 +223,7 @@ def test_codex_toml_merge_replaces_section_with_inline_comment(tmp_path): """_merge_toml_block replaces an existing semble table even when the header has a trailing comment.""" f = tmp_path / "config.toml" f.write_text('[mcp_servers.semble] # added manually\ncommand = "old"\n') - assert _merge_toml_block(f) == "updated" + assert merge_toml_block(f) == "updated" text = f.read_text() assert text.count("[mcp_servers.semble]") == 1 @@ -237,14 +237,14 @@ def test_remove_toml_not_found(tmp_path, setup, expected): f = tmp_path / "config.toml" if setup is not None: f.write_text(setup) - assert _remove_toml_block(f) == expected + assert remove_toml_block(f) == expected def test_remove_toml_deletes_file_when_only_semble(tmp_path): """_remove_toml_block unlinks the file when removing semble leaves it empty.""" f = tmp_path / "config.toml" - _merge_toml_block(f) - _remove_toml_block(f) + merge_toml_block(f) + remove_toml_block(f) assert not f.exists() @@ -265,7 +265,7 @@ def test_remove_toml_strips_sub_tables(tmp_path, content): """_remove_toml_block removes sub-tables like [mcp_servers.semble.tools.search], before or after the main header.""" f = tmp_path / "config.toml" f.write_text(content) - assert _remove_toml_block(f) == "removed" + assert remove_toml_block(f) == "removed" text = f.read_text() assert "[mcp_servers.semble]" not in text assert "[mcp_servers.semble.tools.search]" not in text