Skip to content

Commit 85aab00

Browse files
pbwheelclaude
authored andcommitted
fix: support dict and list[dict] values in TOML renderers
Both _render_toml_value functions (codex_home_config and config_loader) crashed with "unsupported TOML value type: dict" when user configs contained TOML array-of-tables (e.g. [[skills.config]]). Now list[dict] renders as [[section]] blocks and dict renders as inline tables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 212c2b6 commit 85aab00

4 files changed

Lines changed: 180 additions & 6 deletions

File tree

lib/agents/config_loader_runtime/defaults_runtime/rendering_runtime/service.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,35 @@ def _render_toml_mapping(
3939
mapping: dict[str, object],
4040
*,
4141
emit_header: bool,
42+
is_array: bool = False,
4243
) -> None:
4344
scalar_items: list[tuple[str, object]] = []
4445
table_items: list[tuple[str, dict[str, object]]] = []
46+
array_items: list[tuple[str, list[dict[str, object]]]] = []
4547
for key, value in mapping.items():
4648
if value is None:
4749
continue
4850
if isinstance(value, dict):
4951
if not value:
5052
continue
5153
table_items.append((key, value))
54+
elif isinstance(value, list) and value and all(isinstance(item, dict) for item in value):
55+
array_items.append((key, value))
5256
else:
5357
scalar_items.append((key, value))
5458

55-
if emit_header and (scalar_items or not table_items):
59+
if emit_header and (is_array or scalar_items or not table_items and not array_items):
5660
if lines:
5761
lines.append('')
58-
lines.append(f'[{_render_toml_path(path)}]')
62+
header = f'[[{_render_toml_path(path)}]]' if is_array else f'[{_render_toml_path(path)}]'
63+
lines.append(header)
5964
for key, value in scalar_items:
6065
lines.append(f'{_render_toml_key(key)} = {_render_toml_value(value)}')
6166
for key, value in table_items:
6267
_render_toml_mapping(lines, (*path, key), value, emit_header=True)
68+
for key, items in array_items:
69+
for item in items:
70+
_render_toml_mapping(lines, (*path, key), item, emit_header=True, is_array=True)
6371

6472

6573
def _render_toml_path(path: tuple[str, ...]) -> str:
@@ -79,6 +87,14 @@ def _render_toml_value(value: object) -> str:
7987
return json.dumps(value, ensure_ascii=False)
8088
if isinstance(value, list):
8189
return '[' + ', '.join(_render_toml_value(item) for item in value) + ']'
90+
if isinstance(value, dict):
91+
if not value:
92+
return '{}'
93+
pairs = ', '.join(
94+
f'{_render_toml_key(k)} = {_render_toml_value(v)}'
95+
for k, v in value.items()
96+
)
97+
return '{ ' + pairs + ' }'
8298
raise TypeError(f'unsupported TOML value type: {type(value).__name__}')
8399

84100

lib/provider_profiles/codex_home_config.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -640,31 +640,38 @@ def _render_toml_document(payload: dict[str, object]) -> str:
640640
return f'{rendered}\n' if rendered else ''
641641

642642

643-
def _render_toml_sections(payload: dict[str, object], *, path: tuple[str, ...]) -> list[str]:
643+
def _render_toml_sections(payload: dict[str, object], *, path: tuple[str, ...] = (), is_array: bool = False) -> list[str]:
644644
scalar_lines: list[str] = []
645645
child_sections: list[str] = []
646646
child_tables: list[tuple[str, dict[str, object]]] = []
647+
array_tables: list[tuple[str, list[dict[str, object]]]] = []
647648
for raw_key, value in payload.items():
648649
key = str(raw_key)
649650
if value is None:
650651
continue
651652
if isinstance(value, dict):
652653
child_tables.append((key, value))
653654
continue
655+
if isinstance(value, list) and value and all(isinstance(item, dict) for item in value):
656+
array_tables.append((key, value))
657+
continue
654658
scalar_lines.append(f'{_render_toml_key(key)} = {_render_toml_value(value)}')
655659

656660
sections: list[str] = []
657661
if path:
658-
header = f'[{_render_toml_path(path)}]'
659-
if scalar_lines:
662+
header = f'[[{_render_toml_path(path)}]]' if is_array else f'[{_render_toml_path(path)}]'
663+
if is_array or scalar_lines:
660664
sections.append('\n'.join([header, *scalar_lines]))
661-
elif not child_tables:
665+
elif not child_tables and not array_tables:
662666
sections.append(header)
663667
elif scalar_lines:
664668
sections.append('\n'.join(scalar_lines))
665669

666670
for key, child in child_tables:
667671
child_sections.extend(_render_toml_sections(child, path=(*path, key)))
672+
for key, items in array_tables:
673+
for item in items:
674+
child_sections.extend(_render_toml_sections(item, path=(*path, key), is_array=True))
668675
sections.extend(child_sections)
669676
return sections
670677

@@ -694,6 +701,14 @@ def _render_toml_value(value: object) -> str:
694701
return value.isoformat()
695702
if isinstance(value, (list, tuple)):
696703
return '[' + ', '.join(_render_toml_value(item) for item in value) + ']'
704+
if isinstance(value, dict):
705+
if not value:
706+
return '{}'
707+
pairs = ', '.join(
708+
f'{_render_toml_key(k)} = {_render_toml_value(v)}'
709+
for k, v in value.items()
710+
)
711+
return '{ ' + pairs + ' }'
697712
raise TypeError(f'unsupported TOML value type: {type(value).__name__}')
698713

699714

test/test_provider_profiles.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
from pathlib import Path
66
import shutil
7+
import tomllib
78

89
import pytest
910

@@ -2336,3 +2337,93 @@ def test_materialize_gemini_home_config_skips_memory_without_project_context(tmp
23362337
layout = materialize_gemini_home_config(target_home, source_home=source_home)
23372338

23382339
assert not (layout.gemini_dir / 'GEMINI.md').exists()
2340+
2341+
2342+
def test_render_toml_value_handles_dict_inline_table() -> None:
2343+
from provider_profiles.codex_home_config import _render_toml_value
2344+
result = _render_toml_value({'name': 'test', 'enabled': False})
2345+
assert result == '{ name = "test", enabled = false }'
2346+
2347+
2348+
def test_render_toml_value_handles_empty_dict() -> None:
2349+
from provider_profiles.codex_home_config import _render_toml_value
2350+
result = _render_toml_value({})
2351+
assert result == '{}'
2352+
2353+
2354+
def test_render_toml_value_handles_dict_in_mixed_list() -> None:
2355+
from provider_profiles.codex_home_config import _render_toml_value
2356+
result = _render_toml_value(['literal', {'name': 'test'}])
2357+
assert result == '["literal", { name = "test" }]'
2358+
2359+
2360+
def test_render_toml_sections_handles_array_of_tables() -> None:
2361+
from provider_profiles.codex_home_config import _render_toml_sections
2362+
payload = {
2363+
'skills': {
2364+
'config': [
2365+
{'path': '/a/skill.md', 'enabled': False},
2366+
{'name': 'plugin:skill', 'enabled': True},
2367+
]
2368+
}
2369+
}
2370+
sections = _render_toml_sections(payload)
2371+
rendered = '\n\n'.join(sections)
2372+
assert '[[skills.config]]' in rendered
2373+
assert 'path = "/a/skill.md"' in rendered
2374+
assert 'enabled = false' in rendered
2375+
assert 'name = "plugin:skill"' in rendered
2376+
assert 'enabled = true' in rendered
2377+
2378+
2379+
def test_render_toml_sections_handles_array_of_tables_with_only_child_tables() -> None:
2380+
from provider_profiles.codex_home_config import _render_toml_document
2381+
payload = {
2382+
'items': [
2383+
{'child': {'x': 1}},
2384+
{'child': {'x': 2}},
2385+
]
2386+
}
2387+
rendered = _render_toml_document(payload)
2388+
assert tomllib.loads(rendered) == {
2389+
'items': [
2390+
{'child': {'x': 1}},
2391+
{'child': {'x': 2}},
2392+
]
2393+
}
2394+
2395+
2396+
def test_materialize_codex_home_config_with_skills_config_array(tmp_path: Path) -> None:
2397+
source_home = tmp_path / 'codex-home'
2398+
target_home = tmp_path / 'target-home'
2399+
source_home.mkdir(parents=True, exist_ok=True)
2400+
(source_home / 'config.toml').write_text(
2401+
'model = "gpt-5.5"\n'
2402+
'\n'
2403+
'[[skills.config]]\n'
2404+
'path = "/a/skill.md"\n'
2405+
'enabled = false\n'
2406+
'\n'
2407+
'[[skills.config]]\n'
2408+
'name = "plugin:other"\n'
2409+
'enabled = true\n'
2410+
'\n'
2411+
'[features]\n'
2412+
'unified_exec = true\n',
2413+
encoding='utf-8',
2414+
)
2415+
2416+
codex_home_config.materialize_codex_home_config(
2417+
target_home,
2418+
source_home=source_home,
2419+
)
2420+
2421+
text = (target_home / 'config.toml').read_text(encoding='utf-8')
2422+
assert '[[skills.config]]' in text
2423+
assert 'path = "/a/skill.md"' in text
2424+
assert 'name = "plugin:other"' in text
2425+
parsed = tomllib.loads(text)
2426+
assert parsed['skills']['config'] == [
2427+
{'path': '/a/skill.md', 'enabled': False},
2428+
{'name': 'plugin:other', 'enabled': True},
2429+
]

test/test_v2_config_loader.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from pathlib import Path
4+
import tomllib
45

56
import pytest
67

@@ -1069,3 +1070,54 @@ def test_load_project_config_reports_actionable_error_when_hybrid_overlay_parser
10691070

10701071
with pytest.raises(ConfigValidationError, match='rich TOML config requires Python 3.11\\+'):
10711072
load_project_config(project_root)
1073+
1074+
1075+
def test_render_toml_value_service_handles_dict_inline_table() -> None:
1076+
from agents.config_loader_runtime.defaults_runtime.rendering_runtime.service import _render_toml_value
1077+
result = _render_toml_value({'key': 'val', 'count': 3})
1078+
assert result == '{ key = "val", count = 3 }'
1079+
1080+
1081+
def test_render_toml_value_service_handles_empty_dict() -> None:
1082+
from agents.config_loader_runtime.defaults_runtime.rendering_runtime.service import _render_toml_value
1083+
result = _render_toml_value({})
1084+
assert result == '{}'
1085+
1086+
1087+
def test_render_toml_value_service_handles_dict_in_mixed_list() -> None:
1088+
from agents.config_loader_runtime.defaults_runtime.rendering_runtime.service import _render_toml_value
1089+
result = _render_toml_value(['literal', {'key': 'val'}])
1090+
assert result == '["literal", { key = "val" }]'
1091+
1092+
1093+
def test_render_toml_mapping_handles_array_of_tables() -> None:
1094+
from agents.config_loader_runtime.defaults_runtime.rendering_runtime.service import _render_toml_document
1095+
payload = {
1096+
'items': [
1097+
{'name': 'first', 'value': 1},
1098+
{'name': 'second', 'value': 2},
1099+
]
1100+
}
1101+
rendered = _render_toml_document(payload)
1102+
assert '[[items]]' in rendered
1103+
assert 'name = "first"' in rendered
1104+
assert 'value = 1' in rendered
1105+
assert 'name = "second"' in rendered
1106+
assert 'value = 2' in rendered
1107+
1108+
1109+
def test_render_toml_mapping_handles_array_of_tables_with_only_child_tables() -> None:
1110+
from agents.config_loader_runtime.defaults_runtime.rendering_runtime.service import _render_toml_document
1111+
payload = {
1112+
'items': [
1113+
{'child': {'x': 1}},
1114+
{'child': {'x': 2}},
1115+
]
1116+
}
1117+
rendered = _render_toml_document(payload)
1118+
assert tomllib.loads(rendered) == {
1119+
'items': [
1120+
{'child': {'x': 1}},
1121+
{'child': {'x': 2}},
1122+
]
1123+
}

0 commit comments

Comments
 (0)