Skip to content

Commit 747fed5

Browse files
authored
Fix keep_model_order dependency ordering and reduce unnecessary model_rebuild() calls (#2661)
* Implement stable topological sorting and model reordering based on dependencies * Fix model ordering dependency resolution and update test target Python version * Refactor model ordering logic for improved dependency resolution and determinism * Enhance model ordering logic to improve dependency resolution and add tests for TypeAlias cycles * Optimize dependency resolution in model ordering by refining edge processing logic * Add tests for keep_model_order behavior and update type alias references * Improve model ordering by enhancing dependency resolution and ensuring stable ordering
1 parent f55e5d1 commit 747fed5

11 files changed

Lines changed: 585 additions & 58 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Graph utilities used by parsers.
2+
3+
This module intentionally contains only generic graph algorithms (no DataModel
4+
or schema-specific logic), so it can be reused across parsers without creating
5+
dependency cycles.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from collections.abc import Callable, Hashable
11+
from heapq import heappop, heappush
12+
from typing import TypeVar
13+
14+
TNode = TypeVar("TNode", bound=Hashable)
15+
16+
17+
def stable_toposort(
18+
nodes: list[TNode],
19+
edges: dict[TNode, set[TNode]],
20+
*,
21+
key: Callable[[TNode], int],
22+
) -> list[TNode]:
23+
"""Stable topological sort; breaks ties by `key`.
24+
25+
The `edges` mapping is an adjacency list where `edges[u]` contains all `v`
26+
such that `u -> v` (i.e., `u` must come before `v`).
27+
28+
If a cycle is detected, any remaining nodes are appended in `key` order for
29+
determinism.
30+
"""
31+
node_set = set(nodes)
32+
order_index = {node: index for index, node in enumerate(nodes)}
33+
indegree: dict[TNode, int] = dict.fromkeys(nodes, 0)
34+
outgoing: dict[TNode, set[TNode]] = {n: set() for n in nodes}
35+
36+
for source in node_set & edges.keys():
37+
destinations = edges[source]
38+
new_destinations = destinations & node_set - outgoing[source]
39+
outgoing[source].update(new_destinations)
40+
for destination in new_destinations:
41+
indegree[destination] += 1
42+
43+
outgoing_sorted = {
44+
node: sorted(neighbors, key=lambda neighbor: (key(neighbor), order_index[neighbor]))
45+
for node, neighbors in outgoing.items()
46+
}
47+
48+
ready: list[tuple[int, int, TNode]] = []
49+
for node in nodes:
50+
if indegree[node] == 0:
51+
heappush(ready, (key(node), order_index[node], node))
52+
53+
result: list[TNode] = []
54+
while ready:
55+
_, _, node = heappop(ready)
56+
result.append(node)
57+
for neighbor in outgoing_sorted[node]:
58+
indegree[neighbor] -= 1
59+
if indegree[neighbor] == 0:
60+
heappush(ready, (key(neighbor), order_index[neighbor], neighbor))
61+
62+
remaining = sorted(
63+
[node for node in nodes if node not in result],
64+
key=lambda node: (key(node), order_index[node]),
65+
)
66+
result.extend(remaining)
67+
return result

0 commit comments

Comments
 (0)