aboutsummaryrefslogtreecommitdiff
path: root/lib/private/truth_common.bzl
blob: c7e6b60f43ac95fbe053faa4494b7d1bdd0f46b4 (plain)
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
"""Common code used by truth."""

load("@bazel_skylib//lib:types.bzl", "types")

def mkmethod(self, method):
    """Bind a struct as the first arg to a function.

    This is loosely equivalent to creating a bound method of a class.
    """
    return lambda *args, **kwargs: method(self, *args, **kwargs)

def repr_with_type(value):
    return "<{} {}>".format(type(value), repr(value))

def _informative_str(value):
    value_str = str(value)
    if not value_str:
        return "<empty string ∅>"
    elif value_str != value_str.strip():
        return '"{}" <sans quotes; note whitespace within>'.format(value_str)
    else:
        return value_str

def enumerate_list_as_lines(values, prefix = "", format_value = None):
    """Format a list of values in a human-friendly list.

    Args:
        values: ([`list`]) the values to display, one per line.
        prefix: ([`str`]) prefix to add before each line item.
        format_value: optional callable to convert each value to a string.
            If not specified, then an appropriate converter will be inferred
            based on the values. If specified, then the callable must accept
            1 positional arg and return a string.

    Returns:
        [`str`]; the values formatted as a human-friendly list.
    """
    if not values:
        return "{}<empty>".format(prefix)

    if format_value == None:
        format_value = guess_format_value(values)

    # Subtract 1 because we start at 0; i.e. length 10 prints 0 to 9
    max_i_width = len(str(len(values) - 1))

    return "\n".join([
        "{prefix}{ipad}{i}: {value}".format(
            prefix = prefix,
            ipad = " " * (max_i_width - len(str(i))),
            i = i,
            value = format_value(v),
        )
        for i, v in enumerate(values)
    ])

def guess_format_value(values):
    """Guess an appropriate human-friendly formatter to use with the value.

    Args:
        values: The object to pick a formatter for.

    Returns:
        callable that accepts the value.
    """
    found_types = {}
    for value in values:
        found_types[type(value)] = None
        if len(found_types) > 1:
            return repr_with_type
    found_types = found_types.keys()
    if len(found_types) != 1:
        return repr_with_type
    elif found_types[0] in ("string", "File"):
        # For strings: omit the extra quotes and escaping. Just noise.
        # For Files: they include <TYPE path> already
        return _informative_str
    else:
        return repr_with_type

def maybe_sorted(container, allow_sorting = True):
    """Attempts to return the values of `container` in sorted order, if possible.

    Args:
        container: ([`list`] | (or other object convertible to list))
        allow_sorting: ([`bool`]) whether to sort even if it can be sorted. This
            is primarly so that callers can avoid boilerplate when they have
            a "should it be sorted" arg, but also always convert to a list.

    Returns:
        A list, in sorted order if possible, otherwise in the original order.
        This *may* be the same object as given as input.
    """
    container = to_list(container)
    if not allow_sorting:
        return container

    if all([_is_sortable(v) for v in container]):
        return sorted(container)
    else:
        return container

def _is_sortable(obj):
    return (
        types.is_string(obj) or types.is_int(obj) or types.is_none(obj) or
        types.is_bool(obj)
    )

def to_list(obj):
    """Attempt to convert the object to a list, else error.

    NOTE: This only supports objects that are typically understood as
    lists, not any iterable. Types like `dict` and `str` are iterable,
    but will be rejected.

    Args:
        obj: ([`list`] | [`depset`]) The object to convert to a list.

    Returns:
        [`list`] of the object
    """
    if types.is_string(obj):
        fail("Cannot pass string to to_list(): {}".format(obj))
    elif types.is_list(obj):
        return obj
    elif types.is_depset(obj):
        return obj.to_list()
    else:
        fail("Unable to convert to list: {}".format(repr_with_type(obj)))