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  )