From 58cb634632cd4d27e1348320665bcfa010e9cbb2 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 15 Feb 2024 23:52:20 +0100 Subject: [PATCH] gh-113317: Argument Clinic: move linear_format into libclinic (#115518) --- Lib/test/test_clinic.py | 15 ++++++- Tools/clinic/clinic.py | 65 +++++----------------------- Tools/clinic/libclinic/__init__.py | 2 + Tools/clinic/libclinic/formatting.py | 50 +++++++++++++++++++++ 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index be35e80fb02c72..f5e9b11ad1cc8a 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -711,7 +711,7 @@ def fn(): class ClinicLinearFormatTest(TestCase): def _test(self, input, output, **kwargs): - computed = clinic.linear_format(input, **kwargs) + computed = libclinic.linear_format(input, **kwargs) self.assertEqual(output, computed) def test_empty_strings(self): @@ -761,6 +761,19 @@ def test_multiline_substitution(self): def """, name='bingle\nbungle\n') + def test_text_before_block_marker(self): + regex = re.escape("found before '{marker}'") + with self.assertRaisesRegex(clinic.ClinicError, regex): + libclinic.linear_format("no text before marker for you! {marker}", + marker="not allowed!") + + def test_text_after_block_marker(self): + regex = re.escape("found after '{marker}'") + with self.assertRaisesRegex(clinic.ClinicError, regex): + libclinic.linear_format("{marker} no text after marker for you!", + marker="not allowed!") + + class InertParser: def __init__(self, clinic): pass diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py index 7e657351b3f629..4925f27b2937b1 100755 --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -163,50 +163,6 @@ def ensure_legal_c_identifier(s: str) -> str: return s -def linear_format(s: 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 s.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: - fail(f"Text found after {{{name}}} block marker! " - "It must be on a line by itself.") - if indent.strip(): - fail(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]) - - class CRenderData: def __init__(self) -> None: @@ -915,7 +871,8 @@ def parser_body( """) for field in preamble, *fields, finale: lines.append(field) - return linear_format("\n".join(lines), parser_declarations=declarations) + return libclinic.linear_format("\n".join(lines), + parser_declarations=declarations) fastcall = not new_or_init limited_capi = clinic.limited_capi @@ -1570,7 +1527,7 @@ def render_option_group_parsing( {group_booleans} break; """ - s = linear_format(s, group_booleans=lines) + s = libclinic.linear_format(s, group_booleans=lines) s = s.format_map(d) out.append(s) @@ -1729,9 +1686,9 @@ def render_function( for name, destination in clinic.destination_buffers.items(): template = templates[name] if has_option_groups: - template = linear_format(template, + template = libclinic.linear_format(template, option_group_parsing=template_dict['option_group_parsing']) - template = linear_format(template, + template = libclinic.linear_format(template, declarations=template_dict['declarations'], return_conversion=template_dict['return_conversion'], initializers=template_dict['initializers'], @@ -1744,10 +1701,8 @@ def render_function( # Only generate the "exit:" label # if we have any gotos - need_exit_label = "goto exit;" in template - template = linear_format(template, - exit_label="exit:" if need_exit_label else '' - ) + label = "exit:" if "goto exit;" in template else "" + template = libclinic.linear_format(template, exit_label=label) s = template.format_map(template_dict) @@ -6125,9 +6080,9 @@ def format_docstring(self) -> str: parameters = self.format_docstring_parameters(params) signature = self.format_docstring_signature(f, params) docstring = "\n".join(lines) - return linear_format(docstring, - signature=signature, - parameters=parameters).rstrip() + return libclinic.linear_format(docstring, + signature=signature, + parameters=parameters).rstrip() def check_remaining_star(self, lineno: int | None = None) -> None: assert isinstance(self.function, Function) diff --git a/Tools/clinic/libclinic/__init__.py b/Tools/clinic/libclinic/__init__.py index 1b300b55acc21e..6237809764d9e1 100644 --- a/Tools/clinic/libclinic/__init__.py +++ b/Tools/clinic/libclinic/__init__.py @@ -9,6 +9,7 @@ docstring_for_c_string, format_escape, indent_all_lines, + linear_format, normalize_snippet, pprint_words, suffix_all_lines, @@ -33,6 +34,7 @@ "docstring_for_c_string", "format_escape", "indent_all_lines", + "linear_format", "normalize_snippet", "pprint_words", "suffix_all_lines", diff --git a/Tools/clinic/libclinic/formatting.py b/Tools/clinic/libclinic/formatting.py index 8b3ad7ba566bc8..873ece6210017a 100644 --- a/Tools/clinic/libclinic/formatting.py +++ b/Tools/clinic/libclinic/formatting.py @@ -4,6 +4,8 @@ import textwrap from typing import Final +from libclinic import ClinicError + SIG_END_MARKER: Final = "--" @@ -171,3 +173,51 @@ def wrap_declarations(text: str, length: int = 78) -> str: 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])