Skip to content

Commit 37e0bfd

Browse files
chore: support plural subgraphs: form, pull spec v0.8.1 (#10)
* test: support plural subgraphs: form, pull spec v0.8.1 Bump the spec submodule from v0.8.0 to v0.8.1 to pick up fixture 019 (subgraph-two-level-nesting), the regression coverage that exercises namespace and parent_states stacking at depth 2. Extend the conformance harness to handle the plural `subgraphs:` map at a fixture's top level. Subgraphs there can reference each other, so they're compiled in dependency order via iterative resolution — each pass picks up subgraphs whose referenced names are already in the registry, and the loop errors on cycles or unresolvable names. Singular `subgraph:` handling (used by 006, 011, 013) is unchanged. Engine itself needs no changes; it already supports arbitrary nesting depth through descend_into_subgraph recursion. Fixture 019 passes on the first run after the harness extension. * test: harden plural subgraphs harness against fixture errors Two defensive checks added per PR review feedback: 1. _compile_subgraphs_map now raises ValueError if an entry's `name` field is present and disagrees with the dict key, or if the name already exists in the registry (collision with the singular subgraph: form or duplicate plural entry). Both are fixture authoring bugs the harness should surface, not paper over. 2. _unsupported_directive now also walks every entry under the plural `subgraphs:` map. A future fixture with fan_out or a flaky directive inside a nested subgraph will skip cleanly instead of failing noisily.
1 parent c332657 commit 37e0bfd

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

tests/conformance/test_conformance.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,70 @@ def scan(graph: Any) -> str | None:
100100
return hit
101101
if (hit := scan(spec.get("subgraph"))) is not None:
102102
return hit
103+
for sub_spec in spec.get("subgraphs", {}).values():
104+
if (hit := scan(sub_spec)) is not None:
105+
return hit
103106
return None
104107

105108

109+
def _subgraph_dependencies(sub_spec: dict[str, Any]) -> set[str]:
110+
"""Names of subgraphs referenced by `subgraph: <name>` directives in
111+
this subgraph's nodes. Used to order plural-form compilation so an
112+
inner subgraph compiles before any subgraph that references it."""
113+
deps: set[str] = set()
114+
nodes = sub_spec.get("nodes")
115+
if not isinstance(nodes, dict):
116+
return deps
117+
for node_spec in cast("dict[str, Any]", nodes).values():
118+
if isinstance(node_spec, dict) and "subgraph" in cast("dict[str, Any]", node_spec):
119+
ref = cast("dict[str, Any]", node_spec)["subgraph"]
120+
if isinstance(ref, str):
121+
deps.add(ref)
122+
return deps
123+
124+
125+
def _compile_subgraphs_map(
126+
subgraphs_spec: dict[str, dict[str, Any]],
127+
registry: dict[str, Any],
128+
) -> None:
129+
"""Compile the plural `subgraphs:` map into ``registry``.
130+
131+
Iterates until every subgraph is compiled, picking entries whose
132+
referenced subgraphs are already in the registry. This keeps the
133+
fixture format order-independent — fixture 019 happens to list inner
134+
before middle, but the harness shouldn't depend on that.
135+
"""
136+
pending: dict[str, dict[str, Any]] = dict(subgraphs_spec)
137+
while pending:
138+
progress = False
139+
for name, sub_spec in list(pending.items()):
140+
deps = _subgraph_dependencies(sub_spec)
141+
if not deps.issubset(registry):
142+
continue
143+
# Plural form omits the `name:` field (the dict key IS the name);
144+
# synthesize it for build_graph's existing singular-form lookup.
145+
# Validate against fixture authoring errors first.
146+
existing_name = sub_spec.get("name")
147+
if existing_name is not None and existing_name != name:
148+
raise ValueError(f"subgraph dict key {name!r} does not match name field {existing_name!r}")
149+
if name in registry:
150+
raise ValueError(
151+
f"subgraph name {name!r} is already registered "
152+
f"(collision with singular subgraph: form or duplicate plural entry)"
153+
)
154+
sub_with_name = {**sub_spec, "name": name}
155+
sub_built = build_graph(
156+
sub_with_name,
157+
subgraphs=registry,
158+
model_name=f"{name.title()}State",
159+
)
160+
registry[name] = sub_built.builder.compile()
161+
del pending[name]
162+
progress = True
163+
if not progress:
164+
raise RuntimeError(f"unresolvable subgraph dependencies in subgraphs: map: {sorted(pending)}")
165+
166+
106167
@pytest.mark.parametrize("fixture_path", _STANDARD_RUNTIME_FIXTURES, ids=_fixture_id)
107168
async def test_runtime_fixture(fixture_path: Path) -> None:
108169
spec = _load(fixture_path)
@@ -113,13 +174,17 @@ async def test_runtime_fixture(fixture_path: Path) -> None:
113174
if (hit := _unsupported_directive(spec)) is not None:
114175
pytest.skip(f"{fixture_path.stem}: unsupported node directive {hit}")
115176

116-
# Subgraph fixtures (006, 011, 013) declare an inner subgraph that the
117-
# outer graph references by name.
177+
# Subgraph fixtures (006, 011, 013) declare an inner subgraph via the
178+
# singular `subgraph:` key. Fixture 019 introduces the plural `subgraphs:`
179+
# map for two-level nesting; subgraphs there can reference each other,
180+
# so they're compiled in dependency order.
118181
subgraphs: dict[str, Any] = {}
119182
if "subgraph" in spec:
120183
sub_spec = spec["subgraph"]
121184
sub_built = build_graph(sub_spec, model_name=f"{sub_spec['name'].title()}State")
122185
subgraphs[sub_spec["name"]] = sub_built.builder.compile()
186+
if "subgraphs" in spec:
187+
_compile_subgraphs_map(spec["subgraphs"], subgraphs)
123188

124189
built = build_graph(spec, subgraphs=subgraphs)
125190
compiled = built.builder.compile()

0 commit comments

Comments
 (0)