core/config: write LF strings as TOML multi-line literals

This commit is contained in:
2026-07-24 17:21:50 +08:00
parent da4d3d097a
commit 559a3410bc
2 changed files with 46 additions and 4 deletions
+22 -4
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, Any, cast
import msgspec import msgspec
import tomlkit import tomlkit
@@ -370,6 +370,24 @@ class ConfigStore:
self._document[key] = tomlkit.table() self._document[key] = tomlkit.table()
return cast("MutableMapping[str, object]", self._document[key]) return cast("MutableMapping[str, object]", self._document[key])
@staticmethod
def _encode_toml_value(value: object) -> object:
"""Encode builtins for tomlkit; strings with LF become multi-line literals."""
if isinstance(value, str):
if "\n" in value:
return tomlkit.string(value, multiline=True)
return value
if isinstance(value, list):
arr: object = tomlkit.array()
append = cast("Any", arr).append
for item in cast("list[object]", value):
append(ConfigStore._encode_toml_value(item))
return arr
if isinstance(value, dict):
# Nested dicts at section level are rare; providers use dedicated helpers.
return ConfigStore._to_inline_table(cast("Mapping[str, object]", value))
return value
def _sync_section(self, key: str, data: object) -> None: def _sync_section(self, key: str, data: object) -> None:
raw: dict[str, object] = msgspec.to_builtins(data) raw: dict[str, object] = msgspec.to_builtins(data)
if not raw: if not raw:
@@ -381,7 +399,7 @@ class ConfigStore:
if k not in raw: if k not in raw:
del section[k] del section[k]
for k, v in raw.items(): for k, v in raw.items():
section[k] = v section[k] = self._encode_toml_value(v)
def _sync_database_section(self) -> None: def _sync_database_section(self) -> None:
"""Sync ``[database]`` to the document.""" """Sync ``[database]`` to the document."""
@@ -403,7 +421,7 @@ class ConfigStore:
if isinstance(value, dict): if isinstance(value, dict):
table[key] = ConfigStore._to_inline_table(cast("Mapping[str, object]", value)) table[key] = ConfigStore._to_inline_table(cast("Mapping[str, object]", value))
else: else:
table[key] = value table[key] = ConfigStore._encode_toml_value(value)
return table return table
@staticmethod @staticmethod
@@ -427,7 +445,7 @@ class ConfigStore:
elif isinstance(value, dict): elif isinstance(value, dict):
entry[key] = ConfigStore._to_inline_table(cast("Mapping[str, object]", value)) entry[key] = ConfigStore._to_inline_table(cast("Mapping[str, object]", value))
else: else:
entry[key] = value entry[key] = ConfigStore._encode_toml_value(value)
return entry return entry
def _sync_providers_section(self) -> None: def _sync_providers_section(self) -> None:
+24
View File
@@ -282,6 +282,30 @@ def test_write_models_as_inline_tables(tmp_path: Path) -> None:
assert again.providers["local"].models["gpt-test"].cost_factor == 2 assert again.providers["local"].models["gpt-test"].cost_factor == 2
def test_write_lf_strings_as_multiline(tmp_path: Path) -> None:
"""Strings containing LF are written as TOML multi-line strings (\"\"\"\"\"\")."""
from plyngent.config import AgentConfig
path = tmp_path / "ml-string.toml"
config = ConfigStore(path=path, document=tomlkit.document())
# AgentConfig has no public setter; exercise the TOML encoder via write path.
config._agent = AgentConfig(
system_prompt="Line one.\nLine two.",
tool_directives="Use tools.\nBe careful.",
)
config.write()
text = path.read_text(encoding="utf-8")
assert 'system_prompt = """' in text
assert "Line one." in text and "Line two." in text
assert 'tool_directives = """' in text
# Escaped single-line form should not appear for these values.
assert 'system_prompt = "Line one.\\nLine two."' not in text
again = plyngent.config.load(path)
assert again.agent_config.system_prompt == "Line one.\nLine two."
assert again.agent_config.tool_directives == "Use tools.\nBe careful."
def test_update_config() -> None: def test_update_config() -> None:
file = Path(__file__).parent / "plyngent-edit-2.toml" file = Path(__file__).parent / "plyngent-edit-2.toml"
_ = shutil.copy(Path(__file__).parent / "plyngent-valid.toml", file) _ = shutil.copy(Path(__file__).parent / "plyngent-valid.toml", file)