smartcomment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smartcomment/__init__.py +49 -0
- smartcomment/analysis/__init__.py +1 -0
- smartcomment/analysis/visualization/__init__.py +43 -0
- smartcomment/analysis/visualization/_utils.py +103 -0
- smartcomment/analysis/visualization/graphviz.py +201 -0
- smartcomment/analysis/visualization/pyvis.py +155 -0
- smartcomment/api/__init__.py +18 -0
- smartcomment/api/_helpers.py +440 -0
- smartcomment/api/_mutation.py +318 -0
- smartcomment/api/public.py +971 -0
- smartcomment/debugging.py +69 -0
- smartcomment/drivers/__init__.py +12 -0
- smartcomment/drivers/base.py +309 -0
- smartcomment/drivers/in_memory.py +323 -0
- smartcomment/identity/__init__.py +6 -0
- smartcomment/identity/registry.py +214 -0
- smartcomment/logging.py +80 -0
- smartcomment/runtime/__init__.py +44 -0
- smartcomment/runtime/context.py +630 -0
- smartcomment/runtime/errors.py +117 -0
- smartcomment/runtime/graph.py +520 -0
- smartcomment/runtime/network.py +1841 -0
- smartcomment/runtime/operation.py +274 -0
- smartcomment/runtime/session.py +133 -0
- smartcomment/runtime/variable.py +237 -0
- smartcomment/schema/__init__.py +1 -0
- smartcomment/schema/base.py +134 -0
- smartcomment/schema/operation.py +96 -0
- smartcomment/schema/session.py +42 -0
- smartcomment/schema/variable.py +126 -0
- smartcomment-0.1.0.dist-info/METADATA +29 -0
- smartcomment-0.1.0.dist-info/RECORD +34 -0
- smartcomment-0.1.0.dist-info/WHEEL +5 -0
- smartcomment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Runtime error types for smartcomment tracing."""
|
|
2
|
+
|
|
3
|
+
from .variable import RuntimeVariable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _diff_context(old: str, new: str, max_chars: int = 120) -> tuple[str, str]:
|
|
8
|
+
"""Return context snippets around the first character where
|
|
9
|
+
old value and new value diverge.
|
|
10
|
+
|
|
11
|
+
It locates the first differing character index, then extracts a
|
|
12
|
+
window of at most `max_chars` characters centred on that position
|
|
13
|
+
from each string. Ellipsis markers are prepended and appended when
|
|
14
|
+
the window does not cover the full string.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
old (`str`):
|
|
18
|
+
The previously recorded encoded value.
|
|
19
|
+
new (`str`):
|
|
20
|
+
The newly provided encoded value.
|
|
21
|
+
max_chars (`int`, defaults to `120`):
|
|
22
|
+
Maximum number of characters to show in each snippet.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
`tuple[str, str]`:
|
|
26
|
+
A tuple of two strings with context around the
|
|
27
|
+
first difference.
|
|
28
|
+
"""
|
|
29
|
+
diff_idx = 0
|
|
30
|
+
for i, (oc, nc) in enumerate(zip(old, new)):
|
|
31
|
+
if oc != nc:
|
|
32
|
+
diff_idx = i
|
|
33
|
+
break
|
|
34
|
+
else:
|
|
35
|
+
diff_idx = min(len(old), len(new))
|
|
36
|
+
|
|
37
|
+
half = max_chars // 2
|
|
38
|
+
start = max(0, diff_idx - half)
|
|
39
|
+
|
|
40
|
+
def _snippet(s: str) -> str:
|
|
41
|
+
end = min(len(s), start + max_chars)
|
|
42
|
+
prefix = "..." if start > 0 else ""
|
|
43
|
+
suffix = "..." if end < len(s) else ""
|
|
44
|
+
return f"{prefix}{s[start:end]}{suffix}"
|
|
45
|
+
|
|
46
|
+
return _snippet(old), _snippet(new)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TraceConsistencyError(RuntimeError):
|
|
50
|
+
"""It is raised in strict mode when a tracked value diverges from
|
|
51
|
+
its recorded state.
|
|
52
|
+
|
|
53
|
+
This typically means a value was mutated in-place without going
|
|
54
|
+
through `comment_mutation`. The error message includes both the
|
|
55
|
+
user-provided value and the existing variable node so that the
|
|
56
|
+
developer can quickly identify the inconsistency.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
user_value: Any,
|
|
63
|
+
user_value_encoded: str,
|
|
64
|
+
existing_variable: RuntimeVariable[Any],
|
|
65
|
+
identity_name: str,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Initialize the error.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
user_value (`Any`):
|
|
71
|
+
The raw Python value that the caller tried to register.
|
|
72
|
+
user_value_encoded (`str`):
|
|
73
|
+
The encoded string of the user-provided value.
|
|
74
|
+
existing_variable (`RuntimeVariable[Any]`):
|
|
75
|
+
The existing variable node stored in the graph.
|
|
76
|
+
identity_name (`str`):
|
|
77
|
+
The identity name that both values resolve to.
|
|
78
|
+
"""
|
|
79
|
+
self.user_value = user_value
|
|
80
|
+
self.user_value_encoded = user_value_encoded
|
|
81
|
+
self.existing_variable = existing_variable
|
|
82
|
+
self.identity_name = identity_name
|
|
83
|
+
|
|
84
|
+
message = self._build_message()
|
|
85
|
+
super().__init__(message)
|
|
86
|
+
|
|
87
|
+
def _build_message(self) -> str:
|
|
88
|
+
"""Build a human-readable diagnostic message."""
|
|
89
|
+
ev = self.existing_variable
|
|
90
|
+
old_snippet, new_snippet = _diff_context(ev.raw_value, self.user_value_encoded)
|
|
91
|
+
|
|
92
|
+
lines = [
|
|
93
|
+
"Untraced mutation detected in strict mode.",
|
|
94
|
+
"",
|
|
95
|
+
f" Variable name (identity) : {self.identity_name!r}",
|
|
96
|
+
f" Existing full node identifier : {ev.full_node_id}",
|
|
97
|
+
f" Existing version number : v{ev.version}",
|
|
98
|
+
"",
|
|
99
|
+
" Diff (first changed character with context):",
|
|
100
|
+
f" Recorded : {old_snippet}",
|
|
101
|
+
f" Provided : {new_snippet}",
|
|
102
|
+
"",
|
|
103
|
+
"The value bound to this identity has changed since it was last "
|
|
104
|
+
"recorded, but no `comment_mutation` call was made. In strict "
|
|
105
|
+
"mode this is treated as an error.",
|
|
106
|
+
"",
|
|
107
|
+
"To fix this, either:",
|
|
108
|
+
" 1. Use `comment_mutation(target=rv, new_value=...)` to record "
|
|
109
|
+
"the change explicitly.",
|
|
110
|
+
" 2. Disable strict mode: `comment_graph(strict=False)` or "
|
|
111
|
+
"`graph.strict = False`.",
|
|
112
|
+
]
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ExecNetworkKeyError(KeyError):
|
|
117
|
+
"""It is raised when a key error occurs in the execution network."""
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""Read-only subgraph view with query and display methods."""
|
|
2
|
+
|
|
3
|
+
from ..analysis.visualization import _VISUAL_BACKENDS
|
|
4
|
+
from .operation import RuntimeEdge, RuntimeOp
|
|
5
|
+
from .variable import RuntimeVariable
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Self,
|
|
9
|
+
Iterable,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RuntimeGraph:
|
|
14
|
+
"""Read-only view over a subgraph."""
|
|
15
|
+
|
|
16
|
+
__slots__ = (
|
|
17
|
+
"nodes", "edges", "ops",
|
|
18
|
+
"_node_ids", "_op_ids", "_edge_ids"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
nodes: list[RuntimeVariable[Any]],
|
|
24
|
+
edges: list[RuntimeEdge],
|
|
25
|
+
ops: list[RuntimeOp],
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize the runtime graph handle.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
nodes (`list[RuntimeVariable]`):
|
|
31
|
+
The list of nodes in the subgraph.
|
|
32
|
+
edges (`list[RuntimeEdge]`):
|
|
33
|
+
The list of edges in the subgraph.
|
|
34
|
+
ops (`list[RuntimeOp]`):
|
|
35
|
+
The list of operations in the subgraph.
|
|
36
|
+
"""
|
|
37
|
+
self.nodes = nodes
|
|
38
|
+
self.edges = edges
|
|
39
|
+
self.ops = ops
|
|
40
|
+
self._node_ids = frozenset(n.full_node_id for n in nodes)
|
|
41
|
+
self._op_ids = frozenset(o.op_id for o in ops)
|
|
42
|
+
self._edge_ids = frozenset(e.edge_id for e in edges)
|
|
43
|
+
|
|
44
|
+
def __contains__(self, full_node_id: str) -> bool:
|
|
45
|
+
"""Check whether a node with the given full node identifier exists.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
full_node_id (`str`):
|
|
49
|
+
The full node identifier to check.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
`bool`:
|
|
53
|
+
``True`` if the node exists in the subgraph, ``False``
|
|
54
|
+
otherwise.
|
|
55
|
+
"""
|
|
56
|
+
return full_node_id in self._node_ids
|
|
57
|
+
|
|
58
|
+
def filter_by_category(self, category: str | Iterable[str]) -> Self:
|
|
59
|
+
"""Return a sub-view containing only nodes of the given category or categories.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
category (`str | Iterable[str]`):
|
|
63
|
+
A single category string or an iterable of category strings.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
`Self`:
|
|
67
|
+
Filtered subgraph.
|
|
68
|
+
"""
|
|
69
|
+
cats = {category} if isinstance(category, str) else set(category)
|
|
70
|
+
matched = [n for n in self.nodes if n.category in cats]
|
|
71
|
+
matched_ids = {n.full_node_id for n in matched}
|
|
72
|
+
kept_edges = [
|
|
73
|
+
e for e in self.edges
|
|
74
|
+
if e.source_full_node_id in matched_ids and e.target_full_node_id in matched_ids
|
|
75
|
+
]
|
|
76
|
+
kept_op_ids = {e.op_id for e in kept_edges}
|
|
77
|
+
kept_ops = [o for o in self.ops if o.op_id in kept_op_ids]
|
|
78
|
+
return type(self)(nodes=matched, edges=kept_edges, ops=kept_ops)
|
|
79
|
+
|
|
80
|
+
def filter_by_time(
|
|
81
|
+
self,
|
|
82
|
+
start: str | None = None,
|
|
83
|
+
end: str | None = None,
|
|
84
|
+
) -> Self:
|
|
85
|
+
"""Return a sub-view of nodes within a time range (inclusive).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
start (`str | None`, optional):
|
|
89
|
+
Inclusive lower bound timestamp. If not provided,
|
|
90
|
+
it means no lower bound.
|
|
91
|
+
end (`str | None`, optional):
|
|
92
|
+
Inclusive upper bound timestamp. If not provided,
|
|
93
|
+
it means no upper bound.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
`Self`:
|
|
97
|
+
Filtered subgraph.
|
|
98
|
+
"""
|
|
99
|
+
matched = [
|
|
100
|
+
n for n in self.nodes
|
|
101
|
+
if (start is None or n.created_at >= start)
|
|
102
|
+
and (end is None or n.created_at <= end)
|
|
103
|
+
]
|
|
104
|
+
matched_ids = {n.full_node_id for n in matched}
|
|
105
|
+
kept_edges = [
|
|
106
|
+
e for e in self.edges
|
|
107
|
+
if e.source_full_node_id in matched_ids and e.target_full_node_id in matched_ids
|
|
108
|
+
]
|
|
109
|
+
kept_op_ids = {e.op_id for e in kept_edges}
|
|
110
|
+
kept_ops = [o for o in self.ops if o.op_id in kept_op_ids]
|
|
111
|
+
return type(self)(nodes=matched, edges=kept_edges, ops=kept_ops)
|
|
112
|
+
|
|
113
|
+
def time_range(self) -> tuple[str, str] | None:
|
|
114
|
+
"""Return the earliest and latest creation time among all runtime variables.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
`tuple[str, str] | None`:
|
|
118
|
+
The earliest and latest creation time of variables in the
|
|
119
|
+
runtime graph if the runtime graph is not empty.
|
|
120
|
+
"""
|
|
121
|
+
if not self.nodes:
|
|
122
|
+
return None
|
|
123
|
+
times = [n.created_at for n in self.nodes]
|
|
124
|
+
return (min(times), max(times))
|
|
125
|
+
|
|
126
|
+
def get_root_nodes(self) -> list[RuntimeVariable[Any]]:
|
|
127
|
+
"""Return nodes with zero in-degree (no incoming edges).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
`list[RuntimeVariable]`:
|
|
131
|
+
Root nodes.
|
|
132
|
+
"""
|
|
133
|
+
targets = {e.target_full_node_id for e in self.edges}
|
|
134
|
+
return [n for n in self.nodes if n.full_node_id not in targets]
|
|
135
|
+
|
|
136
|
+
def get_leaf_nodes(self) -> list[RuntimeVariable[Any]]:
|
|
137
|
+
"""Return nodes with zero out-degree (no outgoing edges).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
`list[RuntimeVariable]`:
|
|
141
|
+
Leaf nodes.
|
|
142
|
+
"""
|
|
143
|
+
sources = {e.source_full_node_id for e in self.edges}
|
|
144
|
+
return [n for n in self.nodes if n.full_node_id not in sources]
|
|
145
|
+
|
|
146
|
+
def induced_subgraph(self, full_node_ids: Iterable[str]) -> Self:
|
|
147
|
+
"""Return the induced subgraph over the given node identifiers.
|
|
148
|
+
|
|
149
|
+
Nodes not present in this graph are silently skipped.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
full_node_ids (`Iterable[str]`):
|
|
153
|
+
The full node identifiers to include.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
`Self`:
|
|
157
|
+
A sub-view containing only the specified nodes and the
|
|
158
|
+
edges whose both endpoints are in the set.
|
|
159
|
+
"""
|
|
160
|
+
id_set = set(full_node_ids)
|
|
161
|
+
matched = [n for n in self.nodes if n.full_node_id in id_set]
|
|
162
|
+
matched_ids = {n.full_node_id for n in matched}
|
|
163
|
+
kept_edges = [
|
|
164
|
+
e for e in self.edges
|
|
165
|
+
if e.source_full_node_id in matched_ids
|
|
166
|
+
and e.target_full_node_id in matched_ids
|
|
167
|
+
]
|
|
168
|
+
kept_op_ids = {e.op_id for e in kept_edges}
|
|
169
|
+
kept_ops = [o for o in self.ops if o.op_id in kept_op_ids]
|
|
170
|
+
return type(self)(nodes=matched, edges=kept_edges, ops=kept_ops)
|
|
171
|
+
|
|
172
|
+
def __and__(self, other: Self) -> Self:
|
|
173
|
+
"""Return the intersection of two graphs.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
other (`Self`):
|
|
177
|
+
The other graph.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
`Self`:
|
|
181
|
+
The intersection graph.
|
|
182
|
+
"""
|
|
183
|
+
if not isinstance(other, RuntimeGraph):
|
|
184
|
+
return NotImplemented
|
|
185
|
+
|
|
186
|
+
common_node_ids = self._node_ids & other._node_ids
|
|
187
|
+
common_edge_ids = self._edge_ids & other._edge_ids
|
|
188
|
+
|
|
189
|
+
nodes = [n for n in self.nodes if n.full_node_id in common_node_ids]
|
|
190
|
+
edges = [
|
|
191
|
+
e for e in self.edges
|
|
192
|
+
if e.edge_id in common_edge_ids
|
|
193
|
+
and e.source_full_node_id in common_node_ids
|
|
194
|
+
and e.target_full_node_id in common_node_ids
|
|
195
|
+
]
|
|
196
|
+
kept_op_ids = {e.op_id for e in edges}
|
|
197
|
+
ops = [o for o in self.ops if o.op_id in kept_op_ids]
|
|
198
|
+
|
|
199
|
+
return type(self)(nodes=nodes, edges=edges, ops=ops)
|
|
200
|
+
|
|
201
|
+
def __or__(self, other: Self) -> Self:
|
|
202
|
+
"""Return the union of two graphs.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
other (`Self`):
|
|
206
|
+
The other graph.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
`Self`:
|
|
210
|
+
The union graph.
|
|
211
|
+
"""
|
|
212
|
+
if not isinstance(other, RuntimeGraph):
|
|
213
|
+
return NotImplemented
|
|
214
|
+
|
|
215
|
+
node_map= {
|
|
216
|
+
n.full_node_id: n for n in self.nodes
|
|
217
|
+
}
|
|
218
|
+
for n in other.nodes:
|
|
219
|
+
node_map.setdefault(n.full_node_id, n)
|
|
220
|
+
|
|
221
|
+
op_map = {o.op_id: o for o in self.ops}
|
|
222
|
+
for o in other.ops:
|
|
223
|
+
op_map.setdefault(o.op_id, o)
|
|
224
|
+
|
|
225
|
+
edge_map = {e.edge_id: e for e in self.edges}
|
|
226
|
+
for e in other.edges:
|
|
227
|
+
edge_map.setdefault(e.edge_id, e)
|
|
228
|
+
|
|
229
|
+
return type(self)(
|
|
230
|
+
nodes=list(node_map.values()),
|
|
231
|
+
edges=list(edge_map.values()),
|
|
232
|
+
ops=list(op_map.values()),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def __sub__(self, other: Self) -> Self:
|
|
236
|
+
"""Return the difference.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
other (`Self`):
|
|
240
|
+
The other graph.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
`Self`:
|
|
244
|
+
The difference graph.
|
|
245
|
+
"""
|
|
246
|
+
if not isinstance(other, RuntimeGraph):
|
|
247
|
+
return NotImplemented
|
|
248
|
+
|
|
249
|
+
diff_node_ids = self._node_ids - other._node_ids
|
|
250
|
+
diff_op_ids = self._op_ids - other._op_ids
|
|
251
|
+
|
|
252
|
+
nodes = [n for n in self.nodes if n.full_node_id in diff_node_ids]
|
|
253
|
+
edges = [
|
|
254
|
+
e for e in self.edges
|
|
255
|
+
if e.op_id in diff_op_ids
|
|
256
|
+
and e.source_full_node_id in diff_node_ids
|
|
257
|
+
and e.target_full_node_id in diff_node_ids
|
|
258
|
+
]
|
|
259
|
+
kept_op_ids = {e.op_id for e in edges}
|
|
260
|
+
ops = [o for o in self.ops if o.op_id in kept_op_ids]
|
|
261
|
+
|
|
262
|
+
return type(self)(nodes=nodes, edges=edges, ops=ops)
|
|
263
|
+
|
|
264
|
+
def __xor__(self, other: Self) -> Self:
|
|
265
|
+
"""Return the symmetric difference of two graphs.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
other (`Self`):
|
|
269
|
+
The other graph.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
`Self`:
|
|
273
|
+
The symmetric-difference graph.
|
|
274
|
+
"""
|
|
275
|
+
if not isinstance(other, RuntimeGraph):
|
|
276
|
+
return NotImplemented
|
|
277
|
+
|
|
278
|
+
xor_node_ids = self._node_ids ^ other._node_ids
|
|
279
|
+
xor_op_ids = self._op_ids ^ other._op_ids
|
|
280
|
+
xor_edge_ids = self._edge_ids ^ other._edge_ids
|
|
281
|
+
|
|
282
|
+
node_map = {}
|
|
283
|
+
for n in (*self.nodes, *other.nodes):
|
|
284
|
+
if n.full_node_id in xor_node_ids:
|
|
285
|
+
node_map.setdefault(n.full_node_id, n)
|
|
286
|
+
|
|
287
|
+
edges = [
|
|
288
|
+
e for e in (*self.edges, *other.edges)
|
|
289
|
+
if e.edge_id in xor_edge_ids
|
|
290
|
+
and e.op_id in xor_op_ids
|
|
291
|
+
and e.source_full_node_id in xor_node_ids
|
|
292
|
+
and e.target_full_node_id in xor_node_ids
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
kept_op_ids = {e.op_id for e in edges}
|
|
296
|
+
op_map = {}
|
|
297
|
+
for o in (*self.ops, *other.ops):
|
|
298
|
+
if o.op_id in kept_op_ids:
|
|
299
|
+
op_map.setdefault(o.op_id, o)
|
|
300
|
+
|
|
301
|
+
return type(self)(
|
|
302
|
+
nodes=list(node_map.values()),
|
|
303
|
+
edges=edges,
|
|
304
|
+
ops=list(op_map.values()),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def __eq__(self, other: object) -> bool:
|
|
308
|
+
"""Check structural equality by node, operation, and edge identifiers."""
|
|
309
|
+
if not isinstance(other, RuntimeGraph):
|
|
310
|
+
return NotImplemented
|
|
311
|
+
return (
|
|
312
|
+
self._node_ids == other._node_ids
|
|
313
|
+
and self._op_ids == other._op_ids
|
|
314
|
+
and self._edge_ids == other._edge_ids
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def issubset(self, other: Self) -> bool:
|
|
318
|
+
"""Check whether every node, op, and edge in this graph also
|
|
319
|
+
exists in another graph.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
other (`Self`):
|
|
323
|
+
The candidate supergraph.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
`bool`:
|
|
327
|
+
``True`` if this graph is a subgraph of another graph.
|
|
328
|
+
"""
|
|
329
|
+
return (
|
|
330
|
+
self._node_ids <= other._node_ids
|
|
331
|
+
and self._op_ids <= other._op_ids
|
|
332
|
+
and self._edge_ids <= other._edge_ids
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def issuperset(self, other: Self) -> bool:
|
|
336
|
+
"""Check whether every node, operation, and edge in another graph also
|
|
337
|
+
exists in this graph.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
other (`Self`):
|
|
341
|
+
The candidate subgraph.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
`bool`:
|
|
345
|
+
``True`` if this graph is a supergraph of another graph.
|
|
346
|
+
"""
|
|
347
|
+
return (
|
|
348
|
+
self._node_ids >= other._node_ids
|
|
349
|
+
and self._op_ids >= other._op_ids
|
|
350
|
+
and self._edge_ids >= other._edge_ids
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def __le__(self, other: Self) -> bool:
|
|
354
|
+
if not isinstance(other, RuntimeGraph):
|
|
355
|
+
return NotImplemented
|
|
356
|
+
return self.issubset(other)
|
|
357
|
+
|
|
358
|
+
def __ge__(self, other: Self) -> bool:
|
|
359
|
+
if not isinstance(other, RuntimeGraph):
|
|
360
|
+
return NotImplemented
|
|
361
|
+
return self.issuperset(other)
|
|
362
|
+
|
|
363
|
+
def __lt__(self, other: Self) -> bool:
|
|
364
|
+
if not isinstance(other, RuntimeGraph):
|
|
365
|
+
return NotImplemented
|
|
366
|
+
return self != other and self.issubset(other)
|
|
367
|
+
|
|
368
|
+
def __gt__(self, other: Self) -> bool:
|
|
369
|
+
if not isinstance(other, RuntimeGraph):
|
|
370
|
+
return NotImplemented
|
|
371
|
+
return self != other and self.issuperset(other)
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def edge_count(self) -> int:
|
|
375
|
+
"""Return the number of edges in the graph."""
|
|
376
|
+
return len(self.edges)
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def op_count(self) -> int:
|
|
380
|
+
"""Return the number of operations in the graph."""
|
|
381
|
+
return len(self.ops)
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def is_empty(self) -> bool:
|
|
385
|
+
"""Check whether the graph contains no nodes."""
|
|
386
|
+
return len(self.nodes) == 0
|
|
387
|
+
|
|
388
|
+
def visualize(self, backend: str = "graphviz", **kwargs: Any) -> Any:
|
|
389
|
+
"""Render this subgraph using a visualization backend.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
backend (`str`, defaults to `"graphviz"`):
|
|
393
|
+
Visualization backend name.
|
|
394
|
+
**kwargs (`Any`):
|
|
395
|
+
Forwarded to the backend.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
`Any`:
|
|
399
|
+
The return value of the visualization backend.
|
|
400
|
+
"""
|
|
401
|
+
if backend not in _VISUAL_BACKENDS:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"'{backend}' is not a supported visualization backend. "
|
|
404
|
+
"Available visualization backends "
|
|
405
|
+
f"are {', '.join(list(_VISUAL_BACKENDS.keys()))}."
|
|
406
|
+
)
|
|
407
|
+
return _VISUAL_BACKENDS[backend](
|
|
408
|
+
self.nodes,
|
|
409
|
+
self.edges,
|
|
410
|
+
**kwargs
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def __len__(self) -> int:
|
|
414
|
+
"""Get the number of nodes in the graph."""
|
|
415
|
+
return len(self.nodes)
|
|
416
|
+
|
|
417
|
+
def __repr__(self) -> str:
|
|
418
|
+
"""Get the string representation of the graph."""
|
|
419
|
+
return (
|
|
420
|
+
f"{self.__class__.__name__}(nodes={len(self.nodes)}, "
|
|
421
|
+
f"edges={len(self.edges)}, "
|
|
422
|
+
f"ops={len(self.ops)})"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def to_xml(
|
|
426
|
+
self,
|
|
427
|
+
*,
|
|
428
|
+
include_metadata: bool = False,
|
|
429
|
+
include_variable_value: bool = True,
|
|
430
|
+
) -> str:
|
|
431
|
+
"""Serialize the subgraph to an XML-like string for large language
|
|
432
|
+
model consumption.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
include_metadata (`bool`, defaults to `False`):
|
|
436
|
+
When it is enabled, include metadata as a JSON string element.
|
|
437
|
+
include_variable_value (`bool`, defaults to `True`):
|
|
438
|
+
When it is enabled, include stored values for variable nodes.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
`str`:
|
|
442
|
+
XML representation.
|
|
443
|
+
"""
|
|
444
|
+
if not self.nodes:
|
|
445
|
+
return "<graph />"
|
|
446
|
+
|
|
447
|
+
parts = ["<graph>"]
|
|
448
|
+
parts.append(" <nodes>")
|
|
449
|
+
for n in self.nodes:
|
|
450
|
+
for line in n.to_xml(
|
|
451
|
+
include_metadata=include_metadata,
|
|
452
|
+
include_variable_value=include_variable_value,
|
|
453
|
+
).splitlines():
|
|
454
|
+
parts.append(f" {line}")
|
|
455
|
+
parts.append(" </nodes>")
|
|
456
|
+
|
|
457
|
+
if self.edges:
|
|
458
|
+
parts.append(" <edges>")
|
|
459
|
+
for e in self.edges:
|
|
460
|
+
for line in e.to_xml(include_metadata=include_metadata).splitlines():
|
|
461
|
+
parts.append(f" {line}")
|
|
462
|
+
parts.append(" </edges>")
|
|
463
|
+
|
|
464
|
+
if self.ops:
|
|
465
|
+
parts.append(" <operations>")
|
|
466
|
+
for o in self.ops:
|
|
467
|
+
for line in o.to_xml(include_metadata=include_metadata).splitlines():
|
|
468
|
+
parts.append(f" {line}")
|
|
469
|
+
parts.append(" </operations>")
|
|
470
|
+
|
|
471
|
+
parts.append("</graph>")
|
|
472
|
+
return "\n".join(parts)
|
|
473
|
+
|
|
474
|
+
def to_markdown(
|
|
475
|
+
self,
|
|
476
|
+
*,
|
|
477
|
+
include_metadata: bool = False,
|
|
478
|
+
include_variable_value: bool = True,
|
|
479
|
+
) -> str:
|
|
480
|
+
"""Serialize the subgraph to Markdown for large language model consumption.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
include_metadata (`bool`, defaults to `False`):
|
|
484
|
+
When it is enabled, include metadata as a JSON string.
|
|
485
|
+
include_variable_value (`bool`, defaults to `True`):
|
|
486
|
+
When it is enabled, include stored values for variable nodes.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
`str`:
|
|
490
|
+
Markdown representation.
|
|
491
|
+
"""
|
|
492
|
+
lines = ["## Graph"]
|
|
493
|
+
|
|
494
|
+
if not self.nodes:
|
|
495
|
+
lines.append("\n*Empty graph.*")
|
|
496
|
+
return "\n".join(lines)
|
|
497
|
+
|
|
498
|
+
lines.append(f"\n### Nodes ({len(self.nodes)})\n")
|
|
499
|
+
for n in self.nodes:
|
|
500
|
+
lines.append(
|
|
501
|
+
n.to_markdown(
|
|
502
|
+
include_metadata=include_metadata,
|
|
503
|
+
include_variable_value=include_variable_value,
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
lines.append("")
|
|
507
|
+
|
|
508
|
+
if self.edges:
|
|
509
|
+
lines.append(f"### Edges ({len(self.edges)})\n")
|
|
510
|
+
for e in self.edges:
|
|
511
|
+
lines.append(e.to_markdown(include_metadata=include_metadata))
|
|
512
|
+
lines.append("")
|
|
513
|
+
|
|
514
|
+
if self.ops:
|
|
515
|
+
lines.append(f"### Operations ({len(self.ops)})\n")
|
|
516
|
+
for o in self.ops:
|
|
517
|
+
lines.append(o.to_markdown(include_metadata=include_metadata))
|
|
518
|
+
lines.append("")
|
|
519
|
+
|
|
520
|
+
return "\n".join(lines)
|