• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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