gh-113317: Clean up Argument Clinic global namespace (#113414)

Split up clinic.py by establishing libclinic as a support package for
Argument Clinic. Get rid of clinic.py globals by either making them
class members, or by putting them into libclinic.

- Move INCLUDE_COMMENT_COLUMN to BlockPrinter
- Move NO_VARARG to CLanguage
- Move formatting helpers to libclinic
- Move some constants to libclinic (and annotate them as Final)
This commit is contained in:
Erlend E. Aasland 2023-12-23 01:37:39 +01:00 committed by GitHub
parent 9c3ddf31a3
commit c3f92f6a75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 135 deletions

View file

@ -16,6 +16,7 @@
test_tools.skip_if_missing('clinic')
with test_tools.imports_under_tool('clinic'):
import libclinic
import clinic
from clinic import DSLParser
@ -3761,19 +3762,19 @@ def test_normalize_snippet(self):
actual = clinic.normalize_snippet(snippet, indent=indent)
self.assertEqual(actual, expected)
def test_quoted_for_c_string(self):
def test_escaped_docstring(self):
dataset = (
# input, expected
(r"abc", r"abc"),
(r"\abc", r"\\abc"),
(r"\a\bc", r"\\a\\bc"),
(r"\a\\bc", r"\\a\\\\bc"),
(r'"abc"', r'\"abc\"'),
(r"'a'", r"\'a\'"),
(r"abc", r'"abc"'),
(r"\abc", r'"\\abc"'),
(r"\a\bc", r'"\\a\\bc"'),
(r"\a\\bc", r'"\\a\\\\bc"'),
(r'"abc"', r'"\"abc\""'),
(r"'a'", r'"\'a\'"'),
)
for line, expected in dataset:
with self.subTest(line=line, expected=expected):
out = clinic.quoted_for_c_string(line)
out = libclinic.docstring_for_c_string(line)
self.assertEqual(out, expected)
def test_format_escape(self):
@ -3784,7 +3785,7 @@ def test_format_escape(self):
def test_indent_all_lines(self):
# Blank lines are expected to be unchanged.
self.assertEqual(clinic.indent_all_lines("", prefix="bar"), "")
self.assertEqual(libclinic.indent_all_lines("", prefix="bar"), "")
lines = (
"one\n"
@ -3794,7 +3795,7 @@ def test_indent_all_lines(self):
"barone\n"
"bartwo"
)
out = clinic.indent_all_lines(lines, prefix="bar")
out = libclinic.indent_all_lines(lines, prefix="bar")
self.assertEqual(out, expected)
# If last line is empty, expect it to be unchanged.
@ -3810,12 +3811,12 @@ def test_indent_all_lines(self):
"bartwo\n"
""
)
out = clinic.indent_all_lines(lines, prefix="bar")
out = libclinic.indent_all_lines(lines, prefix="bar")
self.assertEqual(out, expected)
def test_suffix_all_lines(self):
# Blank lines are expected to be unchanged.
self.assertEqual(clinic.suffix_all_lines("", suffix="foo"), "")
self.assertEqual(libclinic.suffix_all_lines("", suffix="foo"), "")
lines = (
"one\n"
@ -3825,7 +3826,7 @@ def test_suffix_all_lines(self):
"onefoo\n"
"twofoo"
)
out = clinic.suffix_all_lines(lines, suffix="foo")
out = libclinic.suffix_all_lines(lines, suffix="foo")
self.assertEqual(out, expected)
# If last line is empty, expect it to be unchanged.
@ -3841,7 +3842,7 @@ def test_suffix_all_lines(self):
"twofoo\n"
""
)
out = clinic.suffix_all_lines(lines, suffix="foo")
out = libclinic.suffix_all_lines(lines, suffix="foo")
self.assertEqual(out, expected)

View file

@ -50,6 +50,11 @@
overload,
)
# Local imports.
import libclinic
# TODO:
#
# soon:
@ -61,23 +66,6 @@
# and keyword-only
#
NO_VARARG = "PY_SSIZE_T_MAX"
CLINIC_PREFIX = "__clinic_"
CLINIC_PREFIXED_ARGS = {
"_keywords",
"_parser",
"args",
"argsbuf",
"fastargs",
"kwargs",
"kwnames",
"nargs",
"noptargs",
"return_value",
}
# '#include "header.h" // reason': column of '//' comment
INCLUDE_COMMENT_COLUMN = 35
# match '#define Py_LIMITED_API'
LIMITED_CAPI_REGEX = re.compile(r'#define +Py_LIMITED_API')
@ -103,8 +91,6 @@ def __repr__(self) -> str:
NULL = Null()
sig_end_marker = '--'
TemplateDict = dict[str, str]
@ -179,33 +165,6 @@ def fail(
warn_or_fail(*args, filename=filename, line_number=line_number, fail=True)
def quoted_for_c_string(s: str) -> str:
for old, new in (
('\\', '\\\\'), # must be first!
('"', '\\"'),
("'", "\\'"),
):
s = s.replace(old, new)
return s
def c_repr(s: str) -> str:
return '"' + s + '"'
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 = '"' + suffix + '\n' + subsequent_indent * ' ' + '"'
return initial_indent * ' ' + '"' + separator.join(wrapped) + '"'
is_legal_c_identifier = re.compile('^[A-Za-z_][A-Za-z0-9_]*$').match
def is_legal_py_identifier(s: str) -> bool:
@ -251,7 +210,6 @@ def linear_format(s: str, **kwargs: str) -> str:
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('{')
@ -281,34 +239,6 @@ def linear_format(s: str, **kwargs: str) -> str:
return "".join(lines[:-1])
def _add_prefix_and_suffix(s: str, prefix: str = "", suffix: str = "") -> str:
"""
Return 's', with 'prefix' prepended and 'suffix' appended to all lines.
If the last line is empty, it remains unchanged.
If s is blank, returns s unchanged.
(textwrap.indent only adds to non-blank lines.)
"""
*split, last = s.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(s: str, prefix: str) -> str:
return _add_prefix_and_suffix(s, prefix=prefix)
def suffix_all_lines(s: str, suffix: str) -> str:
return _add_prefix_and_suffix(s, suffix=suffix)
def pprint_words(items: list[str]) -> str:
if len(items) <= 2:
return " and ".join(items)
else:
return ", ".join(items[:-1]) + " and " + items[-1]
class CRenderData:
def __init__(self) -> None:
@ -710,6 +640,8 @@ class CLanguage(Language):
stop_line = "[{dsl_name} start generated code]*/"
checksum_line = "/*[{dsl_name} end generated code: {arguments}]*/"
NO_VARARG: Final[str] = "PY_SSIZE_T_MAX"
PARSER_PROTOTYPE_KEYWORD: Final[str] = normalize_snippet("""
static PyObject *
{c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs)
@ -863,7 +795,7 @@ def compiler_deprecated_warning(
code = self.COMPILER_DEPRECATION_WARNING_PROTOTYPE.format(
major=minversion[0],
minor=minversion[1],
message=c_repr(message),
message=libclinic.c_repr(message),
)
return normalize_snippet(code)
@ -894,7 +826,7 @@ def deprecate_positional_use(
params.values(), key=attrgetter("deprecated_positional")
):
names = [repr(p.name) for p in group]
pstr = pprint_words(names)
pstr = libclinic.pprint_words(names)
if len(names) == 1:
message += (
f" Parameter {pstr} will become a keyword-only parameter "
@ -913,8 +845,8 @@ def deprecate_positional_use(
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
condition=condition,
errcheck="",
message=wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
message=libclinic.wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
)
return normalize_snippet(code, indent=4)
@ -963,7 +895,7 @@ def deprecate_keyword_use(
else:
condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
names = [repr(p.name) for p in params.values()]
pstr = pprint_words(names)
pstr = libclinic.pprint_words(names)
pl = 's' if len(params) != 1 else ''
message = (
f"Passing keyword argument{pl} {pstr} to "
@ -974,7 +906,7 @@ def deprecate_keyword_use(
params.values(), key=attrgetter("deprecated_keyword")
):
names = [repr(p.name) for p in group]
pstr = pprint_words(names)
pstr = libclinic.pprint_words(names)
pl = 's' if len(names) != 1 else ''
message += (
f" Parameter{pl} {pstr} will become positional-only "
@ -996,31 +928,11 @@ def deprecate_keyword_use(
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
condition=condition,
errcheck=errcheck,
message=wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
message=libclinic.wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
)
return normalize_snippet(code, indent=4)
def docstring_for_c_string(
self,
f: Function
) -> str:
lines = []
# turn docstring into a properly quoted C string
for line in f.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 output_templates(
self,
f: Function,
@ -1049,7 +961,7 @@ def output_templates(
and not f.critical_section)
new_or_init = f.kind.new_or_init
vararg: int | str = NO_VARARG
vararg: int | str = self.NO_VARARG
pos_only = min_pos = max_pos = min_kw_only = pseudo_args = 0
for i, p in enumerate(parameters, 1):
if p.is_keyword_only():
@ -1057,12 +969,12 @@ def output_templates(
if not p.is_optional():
min_kw_only = i - max_pos
elif p.is_vararg():
if vararg != NO_VARARG:
if vararg != self.NO_VARARG:
fail("Too many var args")
pseudo_args += 1
vararg = i - 1
else:
if vararg == NO_VARARG:
if vararg == self.NO_VARARG:
max_pos = i
if p.is_positional_only():
pos_only = i
@ -1271,7 +1183,7 @@ def parser_body(
argname_fmt = 'PyTuple_GET_ITEM(args, %d)'
left_args = f"{nargs} - {max_pos}"
max_args = NO_VARARG if (vararg != NO_VARARG) else max_pos
max_args = self.NO_VARARG if (vararg != self.NO_VARARG) else max_pos
if limited_capi:
parser_code = []
if nargs != 'nargs':
@ -1296,7 +1208,7 @@ def parser_body(
}}}}
""",
indent=4))
if max_args != NO_VARARG:
if max_args != self.NO_VARARG:
pl = '' if max_args == 1 else 's'
parser_code.append(normalize_snippet(f"""
if ({nargs} > {max_args}) {{{{
@ -1393,13 +1305,16 @@ def parser_body(
if p.deprecated_keyword:
deprecated_keywords[i] = p
has_optional_kw = (max(pos_only, min_pos) + min_kw_only < len(converters) - int(vararg != NO_VARARG))
has_optional_kw = (
max(pos_only, min_pos) + min_kw_only
< len(converters) - int(vararg != self.NO_VARARG)
)
if limited_capi:
parser_code = None
fastcall = False
else:
if vararg == NO_VARARG:
if vararg == self.NO_VARARG:
clinic.add_include('pycore_modsupport.h',
'_PyArg_UnpackKeywords()')
args_declaration = "_PyArg_UnpackKeywords", "%s, %s, %s" % (
@ -1499,7 +1414,7 @@ def parser_body(
else:
label = 'skip_optional_kwonly'
first_opt = max_pos + min_kw_only
if vararg != NO_VARARG:
if vararg != self.NO_VARARG:
first_opt += 1
if i == first_opt:
add_label = label
@ -1897,8 +1812,7 @@ def render_function(
template_dict['methoddef_name'] = f.c_basename.upper() + "_METHODDEF"
template_dict['c_basename'] = f.c_basename
template_dict['docstring'] = self.docstring_for_c_string(f)
template_dict['docstring'] = libclinic.docstring_for_c_string(f.docstring)
template_dict['self_name'] = template_dict['self_type'] = template_dict['self_type_check'] = ''
template_dict['target_critical_section'] = ', '.join(f.target_critical_section)
for converter in converters:
@ -1976,9 +1890,9 @@ def render_function(
s = wrap_declarations(s)
if clinic.line_prefix:
s = indent_all_lines(s, clinic.line_prefix)
s = libclinic.indent_all_lines(s, clinic.line_prefix)
if clinic.line_suffix:
s = suffix_all_lines(s, clinic.line_suffix)
s = libclinic.suffix_all_lines(s, clinic.line_suffix)
destination.append(s)
@ -2263,6 +2177,9 @@ class BlockPrinter:
language: Language
f: io.StringIO = dc.field(default_factory=io.StringIO)
# '#include "header.h" // reason': column of '//' comment
INCLUDE_COMMENT_COLUMN: Final[int] = 35
def print_block(
self,
block: Block,
@ -2318,7 +2235,7 @@ def print_block(
line = f'#include "{include.filename}"'
if include.reason:
comment = f'// {include.reason}\n'
line = line.ljust(INCLUDE_COMMENT_COLUMN - 1) + comment
line = line.ljust(self.INCLUDE_COMMENT_COLUMN - 1) + comment
output += line
if current_condition:
@ -3406,7 +3323,7 @@ def parse_argument(self, args: list[str]) -> None:
args.append(self.converter)
if self.encoding:
args.append(c_repr(self.encoding))
args.append(libclinic.c_repr(self.encoding))
elif self.subclass_of:
args.append(self.subclass_of)
@ -3584,8 +3501,8 @@ def set_template_dict(self, template_dict: TemplateDict) -> None:
@property
def parser_name(self) -> str:
if self.name in CLINIC_PREFIXED_ARGS: # bpo-39741
return CLINIC_PREFIX + self.name
if self.name in libclinic.CLINIC_PREFIXED_ARGS: # bpo-39741
return libclinic.CLINIC_PREFIX + self.name
else:
return self.name
@ -5867,7 +5784,7 @@ def bad_node(self, node: ast.AST) -> None:
if isinstance(value, (bool, NoneType)):
c_default = "Py_" + py_default
elif isinstance(value, str):
c_default = c_repr(value)
c_default = libclinic.c_repr(value)
else:
c_default = py_default
@ -6312,7 +6229,7 @@ def add_parameter(text: str) -> None:
# lines.append(f.return_converter.py_default)
if not f.docstring_only:
lines.append("\n" + sig_end_marker + "\n")
lines.append("\n" + libclinic.SIG_END_MARKER + "\n")
signature_line = "".join(lines)

View file

@ -0,0 +1,40 @@
from typing import Final
from .formatting import (
c_repr,
docstring_for_c_string,
indent_all_lines,
pprint_words,
suffix_all_lines,
wrapped_c_string_literal,
SIG_END_MARKER,
)
__all__ = [
# Formatting helpers
"c_repr",
"docstring_for_c_string",
"indent_all_lines",
"pprint_words",
"suffix_all_lines",
"wrapped_c_string_literal",
"SIG_END_MARKER",
]
CLINIC_PREFIX: Final = "__clinic_"
CLINIC_PREFIXED_ARGS: Final = frozenset(
{
"_keywords",
"_parser",
"args",
"argsbuf",
"fastargs",
"kwargs",
"kwnames",
"nargs",
"noptargs",
"return_value",
}
)

View file

@ -0,0 +1,92 @@
"""A collection of string formatting helpers."""
import textwrap
from typing import Final
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]