1"""A collection of string formatting helpers.""" 2 3import functools 4import textwrap 5from typing import Final 6 7from libclinic import ClinicError 8 9 10SIG_END_MARKER: Final = "--" 11 12 13def docstring_for_c_string(docstring: str) -> str: 14 lines = [] 15 # Turn docstring into a properly quoted C string. 16 for line in docstring.split("\n"): 17 lines.append('"') 18 lines.append(_quoted_for_c_string(line)) 19 lines.append('\\n"\n') 20 21 if lines[-2] == SIG_END_MARKER: 22 # If we only have a signature, add the blank line that the 23 # __text_signature__ getter expects to be there. 24 lines.append('"\\n"') 25 else: 26 lines.pop() 27 lines.append('"') 28 return "".join(lines) 29 30 31def _quoted_for_c_string(text: str) -> str: 32 """Helper for docstring_for_c_string().""" 33 for old, new in ( 34 ("\\", "\\\\"), # must be first! 35 ('"', '\\"'), 36 ("'", "\\'"), 37 ): 38 text = text.replace(old, new) 39 return text 40 41 42def c_repr(text: str) -> str: 43 return '"' + text + '"' 44 45 46def wrapped_c_string_literal( 47 text: str, 48 *, 49 width: int = 72, 50 suffix: str = "", 51 initial_indent: int = 0, 52 subsequent_indent: int = 4 53) -> str: 54 wrapped = textwrap.wrap( 55 text, 56 width=width, 57 replace_whitespace=False, 58 drop_whitespace=False, 59 break_on_hyphens=False, 60 ) 61 separator = c_repr(suffix + "\n" + subsequent_indent * " ") 62 return initial_indent * " " + c_repr(separator.join(wrapped)) 63 64 65def _add_prefix_and_suffix(text: str, *, prefix: str = "", suffix: str = "") -> str: 66 """Return 'text' with 'prefix' prepended and 'suffix' appended to all lines. 67 68 If the last line is empty, it remains unchanged. 69 If text is blank, return text unchanged. 70 71 (textwrap.indent only adds to non-blank lines.) 72 """ 73 *split, last = text.split("\n") 74 lines = [prefix + line + suffix + "\n" for line in split] 75 if last: 76 lines.append(prefix + last + suffix) 77 return "".join(lines) 78 79 80def indent_all_lines(text: str, prefix: str) -> str: 81 return _add_prefix_and_suffix(text, prefix=prefix) 82 83 84def suffix_all_lines(text: str, suffix: str) -> str: 85 return _add_prefix_and_suffix(text, suffix=suffix) 86 87 88def pprint_words(items: list[str]) -> str: 89 if len(items) <= 2: 90 return " and ".join(items) 91 return ", ".join(items[:-1]) + " and " + items[-1] 92 93 94def _strip_leading_and_trailing_blank_lines(text: str) -> str: 95 lines = text.rstrip().split("\n") 96 while lines: 97 line = lines[0] 98 if line.strip(): 99 break 100 del lines[0] 101 return "\n".join(lines) 102 103 104@functools.lru_cache() 105def normalize_snippet(text: str, *, indent: int = 0) -> str: 106 """ 107 Reformats 'text': 108 * removes leading and trailing blank lines 109 * ensures that it does not end with a newline 110 * dedents so the first nonwhite character on any line is at column "indent" 111 """ 112 text = _strip_leading_and_trailing_blank_lines(text) 113 text = textwrap.dedent(text) 114 if indent: 115 text = textwrap.indent(text, " " * indent) 116 return text 117 118 119def format_escape(text: str) -> str: 120 # double up curly-braces, this string will be used 121 # as part of a format_map() template later 122 text = text.replace("{", "{{") 123 text = text.replace("}", "}}") 124 return text 125 126 127def wrap_declarations(text: str, length: int = 78) -> str: 128 """ 129 A simple-minded text wrapper for C function declarations. 130 131 It views a declaration line as looking like this: 132 xxxxxxxx(xxxxxxxxx,xxxxxxxxx) 133 If called with length=30, it would wrap that line into 134 xxxxxxxx(xxxxxxxxx, 135 xxxxxxxxx) 136 (If the declaration has zero or one parameters, this 137 function won't wrap it.) 138 139 If this doesn't work properly, it's probably better to 140 start from scratch with a more sophisticated algorithm, 141 rather than try and improve/debug this dumb little function. 142 """ 143 lines = [] 144 for line in text.split("\n"): 145 prefix, _, after_l_paren = line.partition("(") 146 if not after_l_paren: 147 lines.append(line) 148 continue 149 in_paren, _, after_r_paren = after_l_paren.partition(")") 150 if not _: 151 lines.append(line) 152 continue 153 if "," not in in_paren: 154 lines.append(line) 155 continue 156 parameters = [x.strip() + ", " for x in in_paren.split(",")] 157 prefix += "(" 158 if len(prefix) < length: 159 spaces = " " * len(prefix) 160 else: 161 spaces = " " * 4 162 163 while parameters: 164 line = prefix 165 first = True 166 while parameters: 167 if not first and (len(line) + len(parameters[0]) > length): 168 break 169 line += parameters.pop(0) 170 first = False 171 if not parameters: 172 line = line.rstrip(", ") + ")" + after_r_paren 173 lines.append(line.rstrip()) 174 prefix = spaces 175 return "\n".join(lines) 176 177 178def linear_format(text: str, **kwargs: str) -> str: 179 """ 180 Perform str.format-like substitution, except: 181 * The strings substituted must be on lines by 182 themselves. (This line is the "source line".) 183 * If the substitution text is empty, the source line 184 is removed in the output. 185 * If the field is not recognized, the original line 186 is passed unmodified through to the output. 187 * If the substitution text is not empty: 188 * Each line of the substituted text is indented 189 by the indent of the source line. 190 * A newline will be added to the end. 191 """ 192 lines = [] 193 for line in text.split("\n"): 194 indent, curly, trailing = line.partition("{") 195 if not curly: 196 lines.extend([line, "\n"]) 197 continue 198 199 name, curly, trailing = trailing.partition("}") 200 if not curly or name not in kwargs: 201 lines.extend([line, "\n"]) 202 continue 203 204 if trailing: 205 raise ClinicError( 206 f"Text found after '{{{name}}}' block marker! " 207 "It must be on a line by itself." 208 ) 209 if indent.strip(): 210 raise ClinicError( 211 f"Non-whitespace characters found before '{{{name}}}' block marker! " 212 "It must be on a line by itself." 213 ) 214 215 value = kwargs[name] 216 if not value: 217 continue 218 219 stripped = [line.rstrip() for line in value.split("\n")] 220 value = textwrap.indent("\n".join(stripped), indent) 221 lines.extend([value, "\n"]) 222 223 return "".join(lines[:-1]) 224