1# Copyright 2023 The Bazel Authors. All rights reserved. 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"""Common code used by truth.""" 16 17load("@bazel_skylib//lib:types.bzl", "types") 18 19def mkmethod(self, method): 20 """Bind a struct as the first arg to a function. 21 22 This is loosely equivalent to creating a bound method of a class. 23 """ 24 return lambda *args, **kwargs: method(self, *args, **kwargs) 25 26def repr_with_type(value): 27 return "<{} {}>".format(type(value), repr(value)) 28 29def _informative_str(value): 30 value_str = str(value) 31 if not value_str: 32 return "<empty string ∅>" 33 elif "\n" in value_str: 34 return '"""{}""" <sans triple-quotes; note newlines and whitespace>'.format(value_str) 35 elif value_str != value_str.strip(): 36 return '"{}" <sans quotes; note whitespace within>'.format(value_str) 37 else: 38 return value_str 39 40def enumerate_list_as_lines(values, prefix = "", format_value = None): 41 """Format a list of values in a human-friendly list. 42 43 Args: 44 values: ([`list`]) the values to display, one per line. 45 prefix: ([`str`]) prefix to add before each line item. 46 format_value: optional callable to convert each value to a string. 47 If not specified, then an appropriate converter will be inferred 48 based on the values. If specified, then the callable must accept 49 1 positional arg and return a string. 50 51 Returns: 52 [`str`]; the values formatted as a human-friendly list. 53 """ 54 if not values: 55 return "{}<empty>".format(prefix) 56 57 if format_value == None: 58 format_value = guess_format_value(values) 59 60 # Subtract 1 because we start at 0; i.e. length 10 prints 0 to 9 61 max_i_width = len(str(len(values) - 1)) 62 63 return "\n".join([ 64 "{prefix}{ipad}{i}: {value}".format( 65 prefix = prefix, 66 ipad = " " * (max_i_width - len(str(i))), 67 i = i, 68 value = format_value(v), 69 ) 70 for i, v in enumerate(values) 71 ]) 72 73def guess_format_value(values): 74 """Guess an appropriate human-friendly formatter to use with the value. 75 76 Args: 77 values: The object to pick a formatter for. 78 79 Returns: 80 callable that accepts the value. 81 """ 82 found_types = {} 83 for value in values: 84 found_types[type(value)] = None 85 if len(found_types) > 1: 86 return repr_with_type 87 found_types = found_types.keys() 88 if len(found_types) != 1: 89 return repr_with_type 90 elif found_types[0] in ("string", "File"): 91 # For strings: omit the extra quotes and escaping. Just noise. 92 # For Files: they include <TYPE path> already 93 return _informative_str 94 else: 95 return repr_with_type 96 97def maybe_sorted(container, allow_sorting = True): 98 """Attempts to return the values of `container` in sorted order, if possible. 99 100 Args: 101 container: ([`list`] | (or other object convertible to list)) 102 allow_sorting: ([`bool`]) whether to sort even if it can be sorted. This 103 is primarily so that callers can avoid boilerplate when they have 104 a "should it be sorted" arg, but also always convert to a list. 105 106 Returns: 107 A list, in sorted order if possible, otherwise in the original order. 108 This *may* be the same object as given as input. 109 """ 110 container = to_list(container) 111 if not allow_sorting: 112 return container 113 114 if all([_is_sortable(v) for v in container]): 115 return sorted(container) 116 else: 117 return container 118 119def _is_sortable(obj): 120 return ( 121 types.is_string(obj) or types.is_int(obj) or types.is_none(obj) or 122 types.is_bool(obj) 123 ) 124 125def to_list(obj): 126 """Attempt to convert the object to a list, else error. 127 128 NOTE: This only supports objects that are typically understood as 129 lists, not any iterable. Types like `dict` and `str` are iterable, 130 but will be rejected. 131 132 Args: 133 obj: ([`list`] | [`depset`]) The object to convert to a list. 134 135 Returns: 136 [`list`] of the object 137 """ 138 if types.is_string(obj): 139 fail("Cannot pass string to to_list(): {}".format(obj)) 140 elif types.is_list(obj): 141 return obj 142 elif types.is_depset(obj): 143 return obj.to_list() 144 else: 145 fail("Unable to convert to list: {}".format(repr_with_type(obj))) 146