Skip to content

Commit 58cb634

Browse files
gh-113317: Argument Clinic: move linear_format into libclinic (#115518)
1 parent fd2bb4b commit 58cb634

File tree

4 files changed

+76
-56
lines changed

4 files changed

+76
-56
lines changed

Lib/test/test_clinic.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ def fn():
711711

712712
class ClinicLinearFormatTest(TestCase):
713713
def _test(self, input, output, **kwargs):
714-
computed = clinic.linear_format(input, **kwargs)
714+
computed = libclinic.linear_format(input, **kwargs)
715715
self.assertEqual(output, computed)
716716

717717
def test_empty_strings(self):
@@ -761,6 +761,19 @@ def test_multiline_substitution(self):
761761
def
762762
""", name='bingle\nbungle\n')
763763

764+
def test_text_before_block_marker(self):
765+
regex = re.escape("found before '{marker}'")
766+
with self.assertRaisesRegex(clinic.ClinicError, regex):
767+
libclinic.linear_format("no text before marker for you! {marker}",
768+
marker="not allowed!")
769+
770+
def test_text_after_block_marker(self):
771+
regex = re.escape("found after '{marker}'")
772+
with self.assertRaisesRegex(clinic.ClinicError, regex):
773+
libclinic.linear_format("{marker} no text after marker for you!",
774+
marker="not allowed!")
775+
776+
764777
class InertParser:
765778
def __init__(self, clinic):
766779
pass

Tools/clinic/clinic.py

+10-55
Original file line numberDiff line numberDiff line change
@@ -163,50 +163,6 @@ def ensure_legal_c_identifier(s: str) -> str:
163163
return s
164164

165165

166-
def linear_format(s: str, **kwargs: str) -> str:
167-
"""
168-
Perform str.format-like substitution, except:
169-
* The strings substituted must be on lines by
170-
themselves. (This line is the "source line".)
171-
* If the substitution text is empty, the source line
172-
is removed in the output.
173-
* If the field is not recognized, the original line
174-
is passed unmodified through to the output.
175-
* If the substitution text is not empty:
176-
* Each line of the substituted text is indented
177-
by the indent of the source line.
178-
* A newline will be added to the end.
179-
"""
180-
lines = []
181-
for line in s.split('\n'):
182-
indent, curly, trailing = line.partition('{')
183-
if not curly:
184-
lines.extend([line, "\n"])
185-
continue
186-
187-
name, curly, trailing = trailing.partition('}')
188-
if not curly or name not in kwargs:
189-
lines.extend([line, "\n"])
190-
continue
191-
192-
if trailing:
193-
fail(f"Text found after {{{name}}} block marker! "
194-
"It must be on a line by itself.")
195-
if indent.strip():
196-
fail(f"Non-whitespace characters found before {{{name}}} block marker! "
197-
"It must be on a line by itself.")
198-
199-
value = kwargs[name]
200-
if not value:
201-
continue
202-
203-
stripped = [line.rstrip() for line in value.split("\n")]
204-
value = textwrap.indent("\n".join(stripped), indent)
205-
lines.extend([value, "\n"])
206-
207-
return "".join(lines[:-1])
208-
209-
210166
class CRenderData:
211167
def __init__(self) -> None:
212168

@@ -915,7 +871,8 @@ def parser_body(
915871
""")
916872
for field in preamble, *fields, finale:
917873
lines.append(field)
918-
return linear_format("\n".join(lines), parser_declarations=declarations)
874+
return libclinic.linear_format("\n".join(lines),
875+
parser_declarations=declarations)
919876

920877
fastcall = not new_or_init
921878
limited_capi = clinic.limited_capi
@@ -1570,7 +1527,7 @@ def render_option_group_parsing(
15701527
{group_booleans}
15711528
break;
15721529
"""
1573-
s = linear_format(s, group_booleans=lines)
1530+
s = libclinic.linear_format(s, group_booleans=lines)
15741531
s = s.format_map(d)
15751532
out.append(s)
15761533

@@ -1729,9 +1686,9 @@ def render_function(
17291686
for name, destination in clinic.destination_buffers.items():
17301687
template = templates[name]
17311688
if has_option_groups:
1732-
template = linear_format(template,
1689+
template = libclinic.linear_format(template,
17331690
option_group_parsing=template_dict['option_group_parsing'])
1734-
template = linear_format(template,
1691+
template = libclinic.linear_format(template,
17351692
declarations=template_dict['declarations'],
17361693
return_conversion=template_dict['return_conversion'],
17371694
initializers=template_dict['initializers'],
@@ -1744,10 +1701,8 @@ def render_function(
17441701

17451702
# Only generate the "exit:" label
17461703
# if we have any gotos
1747-
need_exit_label = "goto exit;" in template
1748-
template = linear_format(template,
1749-
exit_label="exit:" if need_exit_label else ''
1750-
)
1704+
label = "exit:" if "goto exit;" in template else ""
1705+
template = libclinic.linear_format(template, exit_label=label)
17511706

17521707
s = template.format_map(template_dict)
17531708

@@ -6125,9 +6080,9 @@ def format_docstring(self) -> str:
61256080
parameters = self.format_docstring_parameters(params)
61266081
signature = self.format_docstring_signature(f, params)
61276082
docstring = "\n".join(lines)
6128-
return linear_format(docstring,
6129-
signature=signature,
6130-
parameters=parameters).rstrip()
6083+
return libclinic.linear_format(docstring,
6084+
signature=signature,
6085+
parameters=parameters).rstrip()
61316086

61326087
def check_remaining_star(self, lineno: int | None = None) -> None:
61336088
assert isinstance(self.function, Function)

Tools/clinic/libclinic/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
docstring_for_c_string,
1010
format_escape,
1111
indent_all_lines,
12+
linear_format,
1213
normalize_snippet,
1314
pprint_words,
1415
suffix_all_lines,
@@ -33,6 +34,7 @@
3334
"docstring_for_c_string",
3435
"format_escape",
3536
"indent_all_lines",
37+
"linear_format",
3638
"normalize_snippet",
3739
"pprint_words",
3840
"suffix_all_lines",

Tools/clinic/libclinic/formatting.py

+50
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import textwrap
55
from typing import Final
66

7+
from libclinic import ClinicError
8+
79

810
SIG_END_MARKER: Final = "--"
911

@@ -171,3 +173,51 @@ def wrap_declarations(text: str, length: int = 78) -> str:
171173
lines.append(line.rstrip())
172174
prefix = spaces
173175
return "\n".join(lines)
176+
177+
178+
def 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])

0 commit comments

Comments
 (0)