cpython/Tools/clinic/libclinic/formatting.py

"""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])