cpython/Tools/clinic/libclinic/formatting.py

224 lines
6.5 KiB
Python

"""A collection of string formatting helpers."""
import functools
import textwrap
from typing import Final
from libclinic import ClinicError
SIG_END_MARKER: Final = "--"
def docstring_for_c_string(docstring: str) -> str:
lines = []
# Turn docstring into a properly quoted C string.
for line in docstring.split("\n"):
lines.append('"')
lines.append(_quoted_for_c_string(line))
lines.append('\\n"\n')
if lines[-2] == SIG_END_MARKER:
# If we only have a signature, add the blank line that the
# __text_signature__ getter expects to be there.
lines.append('"\\n"')
else:
lines.pop()
lines.append('"')
return "".join(lines)
def _quoted_for_c_string(text: str) -> str:
"""Helper for docstring_for_c_string()."""
for old, new in (
("\\", "\\\\"), # must be first!
('"', '\\"'),
("'", "\\'"),
):
text = text.replace(old, new)
return text
def c_repr(text: str) -> str:
return '"' + text + '"'
def wrapped_c_string_literal(
text: str,
*,
width: int = 72,
suffix: str = "",
initial_indent: int = 0,
subsequent_indent: int = 4
) -> str:
wrapped = textwrap.wrap(
text,
width=width,
replace_whitespace=False,
drop_whitespace=False,
break_on_hyphens=False,
)
separator = c_repr(suffix + "\n" + subsequent_indent * " ")
return initial_indent * " " + c_repr(separator.join(wrapped))
def _add_prefix_and_suffix(text: str, *, prefix: str = "", suffix: str = "") -> str:
"""Return 'text' with 'prefix' prepended and 'suffix' appended to all lines.
If the last line is empty, it remains unchanged.
If text is blank, return text unchanged.
(textwrap.indent only adds to non-blank lines.)
"""
*split, last = text.split("\n")
lines = [prefix + line + suffix + "\n" for line in split]
if last:
lines.append(prefix + last + suffix)
return "".join(lines)
def indent_all_lines(text: str, prefix: str) -> str:
return _add_prefix_and_suffix(text, prefix=prefix)
def suffix_all_lines(text: str, suffix: str) -> str:
return _add_prefix_and_suffix(text, suffix=suffix)
def pprint_words(items: list[str]) -> str:
if len(items) <= 2:
return " and ".join(items)
return ", ".join(items[:-1]) + " and " + items[-1]
def _strip_leading_and_trailing_blank_lines(text: str) -> str:
lines = text.rstrip().split("\n")
while lines:
line = lines[0]
if line.strip():
break
del lines[0]
return "\n".join(lines)
@functools.lru_cache()
def normalize_snippet(text: str, *, indent: int = 0) -> str:
"""
Reformats 'text':
* removes leading and trailing blank lines
* ensures that it does not end with a newline
* dedents so the first nonwhite character on any line is at column "indent"
"""
text = _strip_leading_and_trailing_blank_lines(text)
text = textwrap.dedent(text)
if indent:
text = textwrap.indent(text, " " * indent)
return text
def format_escape(text: str) -> str:
# double up curly-braces, this string will be used
# as part of a format_map() template later
text = text.replace("{", "{{")
text = text.replace("}", "}}")
return text
def wrap_declarations(text: str, length: int = 78) -> str:
"""
A simple-minded text wrapper for C function declarations.
It views a declaration line as looking like this:
xxxxxxxx(xxxxxxxxx,xxxxxxxxx)
If called with length=30, it would wrap that line into
xxxxxxxx(xxxxxxxxx,
xxxxxxxxx)
(If the declaration has zero or one parameters, this
function won't wrap it.)
If this doesn't work properly, it's probably better to
start from scratch with a more sophisticated algorithm,
rather than try and improve/debug this dumb little function.
"""
lines = []
for line in text.split("\n"):
prefix, _, after_l_paren = line.partition("(")
if not after_l_paren:
lines.append(line)
continue
in_paren, _, after_r_paren = after_l_paren.partition(")")
if not _:
lines.append(line)
continue
if "," not in in_paren:
lines.append(line)
continue
parameters = [x.strip() + ", " for x in in_paren.split(",")]
prefix += "("
if len(prefix) < length:
spaces = " " * len(prefix)
else:
spaces = " " * 4
while parameters:
line = prefix
first = True
while parameters:
if not first and (len(line) + len(parameters[0]) > length):
break
line += parameters.pop(0)
first = False
if not parameters:
line = line.rstrip(", ") + ")" + after_r_paren
lines.append(line.rstrip())
prefix = spaces
return "\n".join(lines)
def linear_format(text: str, **kwargs: str) -> str:
"""
Perform str.format-like substitution, except:
* The strings substituted must be on lines by
themselves. (This line is the "source line".)
* If the substitution text is empty, the source line
is removed in the output.
* If the field is not recognized, the original line
is passed unmodified through to the output.
* If the substitution text is not empty:
* Each line of the substituted text is indented
by the indent of the source line.
* A newline will be added to the end.
"""
lines = []
for line in text.split("\n"):
indent, curly, trailing = line.partition("{")
if not curly:
lines.extend([line, "\n"])
continue
name, curly, trailing = trailing.partition("}")
if not curly or name not in kwargs:
lines.extend([line, "\n"])
continue
if trailing:
raise ClinicError(
f"Text found after '{{{name}}}' block marker! "
"It must be on a line by itself."
)
if indent.strip():
raise ClinicError(
f"Non-whitespace characters found before '{{{name}}}' block marker! "
"It must be on a line by itself."
)
value = kwargs[name]
if not value:
continue
stripped = [line.rstrip() for line in value.split("\n")]
value = textwrap.indent("\n".join(stripped), indent)
lines.extend([value, "\n"])
return "".join(lines[:-1])