go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/graph.star (about) 1 # Copyright 2018 The LUCI Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """API for manipulating the node graph.""" 16 17 _KEY_ORDER = "key" 18 _REVERSE_KEY_ORDER = "~key" 19 20 _DEFINITION_ORDER = "def" 21 _REVERSE_DEFINITION_ORDER = "~def" 22 23 _BREADTH_FIRST = "breadth" 24 _DEPTH_FIRST = "depth" 25 26 # A constructor for graph.keyset structs. 27 _keyset_ctor = __native__.genstruct("graph.keyset") 28 29 def _check_interpreter_context(): 30 """Verifies add_node and add_edge are used only from 'exec' context.""" 31 ctx = __native__.interpreter_context() 32 if ctx == "EXEC": 33 return # allowed 34 if ctx == "LOAD": 35 fail('code executed via "load" must be side-effect free, consider using "exec" instead') 36 elif ctx == "GEN": 37 fail("generators aren't allowed to modify the graph, only query it") 38 else: 39 fail("cannot modify the graph from interpreter context %s" % ctx) 40 41 def _key(*args): 42 """Returns a key with given [(kind, name)] path. 43 44 The kind in the last pair is considered the principal kind: when keys or 45 nodes are filtered by kind, they are filtered by the kind from the last 46 pair. 47 48 Args: 49 *args: even number of strings: kind1, name1, kind2, name2, ... 50 51 Returns: 52 graph.key object representing this path. 53 """ 54 return __native__.graph().key(*args) 55 56 def _keyset(*keys): 57 """Returns a struct that encapsulates a set of keys of different kinds. 58 59 Keysets are returned by rules such as luci.builder(...). Internally such 60 rules add a bunch of nodes to the graph, representing different aspects of 61 the definition. Keysets represent keys of "publicly exposed" nodes, so that 62 other nodes can connect to them. 63 64 For example, luci.builder(...) is internally represented by nodes of 3 65 kinds: 66 * luci.builder (actual builder definition) 67 * luci.builder_ref (used when the builder is treated as a builder) 68 * luci.triggerer (used when the builder is treated as a triggerer). 69 70 Other nodes, depending of what they do, sometimes want to connect to 71 `luci.builder_ref` or to `luci.triggerer`. So in general rules accept 72 keysets, and their implementations then pick a key they want via 73 `keyset.get(...)`. 74 75 Note that `keys` must all have different kinds, otherwise `get` has no way 76 to target a particular key. graph.keyset(...) fails if some keys have the 77 same kind. 78 79 The kind of the first key in the keyset is used for error messages. It 80 should be the "most representative" key (e.g. `luci.builder` in the example 81 above). 82 83 Returns: 84 A graph.keyset struct with two methods: 85 `get(kind): graph.key`: either returns a key with given kind or fails 86 with an informative message if it's not there. 87 `has(kind): bool`: returns True if there's a key with given kind in the 88 keyset. 89 """ 90 if not keys: 91 fail("bad empty keyset") 92 93 as_map = {} 94 for k in keys: 95 if k.kind in as_map: 96 fail("bad key set %s, kind %s is duplicated" % (keys, k.kind)) 97 as_map[k.kind] = k 98 99 def get(kind): 100 k = as_map.get(kind) 101 if not k: 102 fail("expecting %s, got %s" % (kind, keys[0].kind)) 103 return k 104 105 def has(kind): 106 return kind in as_map 107 108 return _keyset_ctor(get = get, has = has) 109 110 def _is_keyset(keyset): 111 """Returns True if `keyset` is graph.keyset(...) struct.""" 112 return __native__.ctor(keyset) == _keyset_ctor 113 114 def _add_node(key, props = None, idempotent = False, trace = None): 115 """Adds a node to the graph. 116 117 If such node already exists, either fails right away (if 'idempotent' is 118 false), or verifies the existing node has also been marked as idempotent and 119 has exact same props dict as being passed here. 120 121 Can be used only from code that was loaded via some exec(...). Fails if run 122 from a module being loaded via load(...). Such library-like modules must not 123 have side effects during their loading. 124 125 Also fails if used from a generator callback: at this point the graph is 126 frozen and can't be extended. 127 128 Args: 129 key: a node key, see graph.key(...). 130 props: a dict with node properties, will be frozen. 131 idempotent: True if this node can be redeclared, but only with same props. 132 trace: a stack trace to associate with the node. 133 """ 134 _check_interpreter_context() 135 __native__.graph().add_node( 136 key, 137 props or {}, 138 bool(idempotent), 139 trace or stacktrace(skip = 1), 140 ) 141 142 def _add_edge(parent, child, title = None, trace = None): 143 """Adds an edge to the graph. 144 145 Neither of the nodes have to exist yet: it is OK to declare nodes and edges 146 in arbitrary order as long as at the end of the script execution (when the 147 graph is finalized) the graph is complete. 148 149 It is OK to add the same edge (with the same title) more than once. Only 150 the trace of the first definition is recorded. 151 152 Fails if the new edge introduces a cycle. 153 154 Can be used only from code that was loaded via some exec(...). Fails if run 155 from a module being loaded via load(...). Such library-like modules must not 156 have side effects during their loading. 157 158 Also fails if used from a generator callback: at this point the graph is 159 frozen and can't be extended. 160 161 Args: 162 parent: a parent node key, see graph.key(...). 163 child: a child node key, see graph.key(...). 164 title: a title for the edge, used in error messages. 165 trace: a stack trace to associate with the edge. 166 """ 167 _check_interpreter_context() 168 __native__.graph().add_edge( 169 parent, 170 child, 171 title or "", 172 trace or stacktrace(skip = 1), 173 ) 174 175 def _node(key): 176 """Returns a node by the key or None if there's no such node. 177 178 Fails if called not from a generator callback: a graph under construction 179 can't be queried. 180 181 Args: 182 key: a node key, see graph.key(...). 183 184 Returns: 185 graph.node object representing the node. 186 """ 187 return __native__.graph().node(key) 188 189 def _children(parent, kind = None, order_by = _KEY_ORDER): 190 """Returns direct children of a node (given by its key). 191 192 Depending on 'order_by', the children are either ordered lexicographically 193 by their keys or by the order edges to them were defined. 194 195 Fails if called not from a generator callback: a graph under construction 196 can't be queried. 197 198 Args: 199 parent: a key of the parent node, see graph.key(...). 200 kind: a string with a kind of children to return or None for all. 201 order_by: one of `*_ORDER` constants, default is KEY_ORDER. 202 203 Returns: 204 List of graph.node objects. 205 """ 206 out = __native__.graph().children(parent, order_by) 207 if kind: 208 return [n for n in out if n.key.kind == kind] 209 return out 210 211 def _descendants( 212 root, 213 visitor = None, 214 order_by = _KEY_ORDER, 215 topology = _BREADTH_FIRST): 216 """Recursively visits 'root' (given by its key) and all its children. 217 218 Returns the list of all visited nodes. When visiting in breadth-first order 219 (i.e. with `topology = BREADTH_FIRST`), nodes are returned exactly in the 220 same order they were passed to `visitor` callback. When visiting in 221 depth-first order, nodes are returned sorted topologically. 222 223 Fails if called not from a generator callback: a graph under construction 224 can't be queried. 225 226 Each node is visited only once, even if it is reachable through multiple 227 paths. Note that the graph has no cycles (by construction). 228 229 The visitor callback (if not None) is called for each visited node. It 230 decides what subset of children of this node to visit. The callback always 231 sees all children, even if some of them (or all) have already been visited. 232 Visited nodes will be skipped even if the visitor returns them. 233 234 Args: 235 root: a key of the node to start the traversal from, see graph.key(...). 236 visitor: func(node: graph.node, children: []graph.node): []graph.node. 237 order_by: one of `*_ORDER` constants, default is KEY_ORDER. 238 topology: either BREADTH_FIRST or DEPTH_FIRST, default is BREADTH_FIRST. 239 240 Returns: 241 List of visited graph.node objects, starting with the root. 242 """ 243 return __native__.graph().descendants(root, visitor, order_by, topology) 244 245 def _parents(child, kind = None, order_by = _KEY_ORDER): 246 """Returns direct parents of a node (given by its key). 247 248 Depending on 'order_by', the parents are either ordered lexicographically by 249 their key or by the order edges from them were defined. 250 251 Fails if called not from a generator callback: a graph under construction 252 can't be queried. 253 254 Args: 255 child: a key of the node to find parents of, see graph.key(...). 256 kind: a string with a kind of parents to return or None for all. 257 order_by: one of `*_ORDER` constants, default is KEY_ORDER. 258 259 Returns: 260 List of graph.node objects. 261 """ 262 out = __native__.graph().parents(child, order_by) 263 if kind: 264 return [n for n in out if n.key.kind == kind] 265 return out 266 267 def _sorted_nodes(nodes, order_by = _KEY_ORDER): 268 """Returns a new sorted list of nodes. 269 270 Depending on 'order_by', the nodes are either ordered lexicographically by 271 their keys or by the order they were defined in the graph. 272 273 Args: 274 nodes: an iterable of graph.node objects. 275 order_by: one of `*_ORDER` constants, default is KEY_ORDER. 276 277 Returns: 278 List of graph.node objects. 279 """ 280 return __native__.graph().sorted_nodes(nodes, order_by) 281 282 # Public API of this module. 283 graph = struct( 284 KEY_ORDER = _KEY_ORDER, 285 REVERSE_KEY_ORDER = _REVERSE_KEY_ORDER, 286 DEFINITION_ORDER = _DEFINITION_ORDER, 287 REVERSE_DEFINITION_ORDER = _REVERSE_DEFINITION_ORDER, 288 BREADTH_FIRST = _BREADTH_FIRST, 289 DEPTH_FIRST = _DEPTH_FIRST, 290 key = _key, 291 keyset = _keyset, 292 is_keyset = _is_keyset, 293 add_node = _add_node, 294 add_edge = _add_edge, 295 node = _node, 296 children = _children, 297 descendants = _descendants, 298 parents = _parents, 299 sorted_nodes = _sorted_nodes, 300 )