1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173

infra / config / lib / args.star [blame]

# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Library containing utilities for handling args in libraries."""

def _sentinel(tag):
    return struct(**{tag: tag})

# A sentinel value indicating a value that will be computed based off of some
# other values
COMPUTE = _sentinel("__compute__")

# A sentinel value to be used as a default so that it can be distinguished
# between an unspecified value and an explicit value of None
DEFAULT = _sentinel("__default__")

# A sentinel value that can be passed as the merge argument to
# get_value/get_value_from_kwargs to have the provided value merged as a list.
MERGE_LIST = _sentinel("__merge_list__")

# A sentinel value that can be passed as the merge argument to
# get_value/get_value_from_kwargs to have the provided value merged as a dict.
MERGE_DICT = _sentinel("__merge_dict__")

_IGNORE_DEFAULT_ATTR = "_ignore_default"

def defaults(extends = None, **vars):
    """Define a structure provides a group of module-level defaults.

    Args:
      extends: A struct containing `lucicfg.var` attributes to add to the
        resulting struct.
      **vars: Defaults to define. Each entry results in a `lucicfg.var` attribute
        being added to the resulting struct. The name of the attribute is the
        keyword and the default value of the `lucicfg.var` is the keyword's value.

    Returns:
      A struct containing `lucicfg.var` attributes providing the module level
      defaults and the following methods:
      * get_value(name, value, merge=None) - Gets the value of an argument. The
        behavior of the function depends on the value of the `merge` argument:
        * None (default) - If `value` is not `DEFAULT`, `value` is returned.
          Otherwise, the module-level default for `name` is returned.
        * MERGE_LIST - If `value` is wrapped using `ignore_defaults`, the result
          of calling `listify(value.value)` is returned. Otherwise, the result
          of calling `listify` with the module-level default for `name` and
          `value` is returned.
        * MERGE_DICT - If `value` is wrapped using `ignore_defaults`,
          `value.value` is returned. Otherwise, the returned value will be the
          module-level default for `name` updated with `value`.
      * get_value_from_kwargs(name, kwargs, merge=None) - Gets the value of a keyword
        argument.
      * set(**kwargs) - Sets module-level defaults. For each keyword, sets the
        module-level default with the keyword as the name to the value of the
        keyword.
    """
    methods = ["get_value", "get_value_from_kwargs", "set"]
    for m in methods:
        if m in vars:
            fail("{!r} can't be used as the name of a default: it is a method"
                .format(a))

    vars = {k: lucicfg.var(default = v) for k, v in vars.items()}
    for a in dir(extends):
        if a not in methods:
            vars[a] = getattr(extends, a)

    def get_value(name, value, merge = None):
        default = vars[name].get()
        ignore_default, value = _should_ignore_default(value)

        if not merge:
            if ignore_default:
                fail("attribute {!r} does not merge with the default, ignore_defaults cannot be used".format(name))
            if value != DEFAULT:
                return value
            return default

        if merge == MERGE_DICT:
            if value == DEFAULT:
                value = {}
            if value and type(value) != type({}):
                fail("attribute {!r} requires a dict value or None, got {!r}".format(name, value))
            value = value or {}
            if ignore_default:
                return value
            new_value = default or {}
            new_value.update(value)
            return new_value

        if merge == MERGE_LIST:
            if value == DEFAULT:
                value = []
            if ignore_default:
                return listify(value)
            return listify(default, value)

        fail("unknown merge value: {}".format(merge))

    def get_value_from_kwargs(name, kwargs, merge = None):
        return get_value(name, kwargs.get(name, DEFAULT), merge = merge)

    def set(**kwargs):
        for k, v in kwargs.items():
            vars[k].set(v)

    return struct(
        get_value = get_value,
        get_value_from_kwargs = get_value_from_kwargs,
        set = set,
        **vars
    )

def ignore_default(value):
    """Wraps a value to ignore defaults for the argument.

    For arguments that merge the provided value with a module-level default,
    this provides a way to explicitly set an exact value. It is an error to use
    this for attributes that don't merge with the module-level default.
    """
    return struct(
        _ignore_default = True,
        value = value,
    )

def _should_ignore_default(value):
    ignore_default = getattr(value, _IGNORE_DEFAULT_ATTR, False)
    if ignore_default:
        value = value.value
    return ignore_default, value

def listify(*args):
    """Create a single list from multiple arguments.

    Each argument can be either a single element or a list of elements. A single
    element will appear as an element in the resulting list iff it is non-None.
    A list of elements will have all non-None elements appear in the resulting
    list.

    Args:
      *args: The arguments to merge into a list.

    Returns:
      A list composed of the pased in arguments.
    """
    wrap_in_ignore_default = False
    l = []
    for a in args:
        should_ignore_default, a = _should_ignore_default(a)
        if should_ignore_default:
            wrap_in_ignore_default = True
        if type(a) != type([]):
            a = [a]
        for e in a:
            should_ignore_default, val = _should_ignore_default(e)
            if should_ignore_default:
                wrap_in_ignore_default = True
            if val != None:
                l.append(val)
    if wrap_in_ignore_default:
        return ignore_default(l)
    return l

args = struct(
    COMPUTE = COMPUTE,
    DEFAULT = DEFAULT,
    MERGE_LIST = MERGE_LIST,
    MERGE_DICT = MERGE_DICT,
    defaults = defaults,
    ignore_default = ignore_default,
    listify = listify,
)