Skip to content

Commit 99e2c5e

Browse files
gh-144545: Improve handling of default values in Argument Clinic (GH-146016)
* Add the c_init_default attribute which is used to initialize the C variable if the default is not explicitly provided. * Add the c_default_init() method which is used to derive c_default from default if c_default is not explicitly provided. * Explicit c_default and py_default are now almost always have precedence over the generated value. * Add support for bytes literals as default values. * Improve support for str literals as default values (support non-ASCII and non-printable characters and special characters like backslash or quotes). * Fix support for str and bytes literals containing trigraphs, "/*" and "*/". * Improve support for default values in converters "char" and "int(accept={str})". * Converter "int(accept={str})" now requires 1-character string instead of integer as default value. * Add support for non-None default values in converter "Py_buffer": NULL, str and bytes literals. * Improve error handling for invalid default values. * Rename Null to NullType for consistency.
1 parent 4f5e798 commit 99e2c5e

File tree

22 files changed

+528
-181
lines changed

22 files changed

+528
-181
lines changed

Lib/test/clinic.test.c

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -530,19 +530,19 @@ test_char_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
530530
{
531531
PyObject *return_value = NULL;
532532
char a = 'A';
533-
char b = '\x07';
534-
char c = '\x08';
533+
char b = '\a';
534+
char c = '\b';
535535
char d = '\t';
536536
char e = '\n';
537-
char f = '\x0b';
538-
char g = '\x0c';
537+
char f = '\v';
538+
char g = '\f';
539539
char h = '\r';
540540
char i = '"';
541541
char j = '\'';
542542
char k = '?';
543543
char l = '\\';
544-
char m = '\x00';
545-
char n = '\xff';
544+
char m = '\0';
545+
char n = '\377';
546546

547547
if (!_PyArg_CheckPositional("test_char_converter", nargs, 0, 14)) {
548548
goto exit;
@@ -936,7 +936,7 @@ static PyObject *
936936
test_char_converter_impl(PyObject *module, char a, char b, char c, char d,
937937
char e, char f, char g, char h, char i, char j,
938938
char k, char l, char m, char n)
939-
/*[clinic end generated code: output=ff11e203248582df input=e42330417a44feac]*/
939+
/*[clinic end generated code: output=6503d15448e1d4c4 input=e42330417a44feac]*/
940940

941941

942942
/*[clinic input]
@@ -1192,14 +1192,14 @@ test_int_converter
11921192
11931193
a: int = 12
11941194
b: int(accept={int}) = 34
1195-
c: int(accept={str}) = 45
1195+
c: int(accept={str}) = '-'
11961196
d: int(type='myenum') = 67
11971197
/
11981198
11991199
[clinic start generated code]*/
12001200

12011201
PyDoc_STRVAR(test_int_converter__doc__,
1202-
"test_int_converter($module, a=12, b=34, c=45, d=67, /)\n"
1202+
"test_int_converter($module, a=12, b=34, c=\'-\', d=67, /)\n"
12031203
"--\n"
12041204
"\n");
12051205

@@ -1215,7 +1215,7 @@ test_int_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
12151215
PyObject *return_value = NULL;
12161216
int a = 12;
12171217
int b = 34;
1218-
int c = 45;
1218+
int c = '-';
12191219
myenum d = 67;
12201220

12211221
if (!_PyArg_CheckPositional("test_int_converter", nargs, 0, 4)) {
@@ -1266,7 +1266,7 @@ test_int_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
12661266

12671267
static PyObject *
12681268
test_int_converter_impl(PyObject *module, int a, int b, int c, myenum d)
1269-
/*[clinic end generated code: output=fbcfb7554688663d input=d20541fc1ca0553e]*/
1269+
/*[clinic end generated code: output=d5357b563bdb8789 input=5d8f4eb5899b24de]*/
12701270

12711271

12721272
/*[clinic input]

Lib/test/test_clinic.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,187 @@ def test_param_with_continuations(self):
10811081
p = function.parameters['follow_symlinks']
10821082
self.assertEqual(True, p.default)
10831083

1084+
def test_param_default_none(self):
1085+
function = self.parse_function(r"""
1086+
module test
1087+
test.func
1088+
obj: object = None
1089+
str: str(accept={str, NoneType}) = None
1090+
buf: Py_buffer(accept={str, buffer, NoneType}) = None
1091+
""")
1092+
p = function.parameters['obj']
1093+
self.assertIs(p.default, None)
1094+
self.assertEqual(p.converter.py_default, 'None')
1095+
self.assertEqual(p.converter.c_default, 'Py_None')
1096+
1097+
p = function.parameters['str']
1098+
self.assertIs(p.default, None)
1099+
self.assertEqual(p.converter.py_default, 'None')
1100+
self.assertEqual(p.converter.c_default, 'NULL')
1101+
1102+
p = function.parameters['buf']
1103+
self.assertIs(p.default, None)
1104+
self.assertEqual(p.converter.py_default, 'None')
1105+
self.assertEqual(p.converter.c_default, '{NULL, NULL}')
1106+
1107+
def test_param_default_null(self):
1108+
function = self.parse_function(r"""
1109+
module test
1110+
test.func
1111+
obj: object = NULL
1112+
str: str = NULL
1113+
buf: Py_buffer = NULL
1114+
fsencoded: unicode_fs_encoded = NULL
1115+
fsdecoded: unicode_fs_decoded = NULL
1116+
""")
1117+
p = function.parameters['obj']
1118+
self.assertIs(p.default, NULL)
1119+
self.assertEqual(p.converter.py_default, '<unrepresentable>')
1120+
self.assertEqual(p.converter.c_default, 'NULL')
1121+
1122+
p = function.parameters['str']
1123+
self.assertIs(p.default, NULL)
1124+
self.assertEqual(p.converter.py_default, '<unrepresentable>')
1125+
self.assertEqual(p.converter.c_default, 'NULL')
1126+
1127+
p = function.parameters['buf']
1128+
self.assertIs(p.default, NULL)
1129+
self.assertEqual(p.converter.py_default, '<unrepresentable>')
1130+
self.assertEqual(p.converter.c_default, '{NULL, NULL}')
1131+
1132+
p = function.parameters['fsencoded']
1133+
self.assertIs(p.default, NULL)
1134+
self.assertEqual(p.converter.py_default, '<unrepresentable>')
1135+
self.assertEqual(p.converter.c_default, 'NULL')
1136+
1137+
p = function.parameters['fsdecoded']
1138+
self.assertIs(p.default, NULL)
1139+
self.assertEqual(p.converter.py_default, '<unrepresentable>')
1140+
self.assertEqual(p.converter.c_default, 'NULL')
1141+
1142+
def test_param_default_str_literal(self):
1143+
function = self.parse_function(r"""
1144+
module test
1145+
test.func
1146+
str: str = ' \t\n\r\v\f\xa0'
1147+
buf: Py_buffer(accept={str, buffer}) = ' \t\n\r\v\f\xa0'
1148+
""")
1149+
p = function.parameters['str']
1150+
self.assertEqual(p.default, ' \t\n\r\v\f\xa0')
1151+
self.assertEqual(p.converter.py_default, r"' \t\n\r\x0b\x0c\xa0'")
1152+
self.assertEqual(p.converter.c_default, r'" \t\n\r\v\f\u00a0"')
1153+
1154+
p = function.parameters['buf']
1155+
self.assertEqual(p.default, ' \t\n\r\v\f\xa0')
1156+
self.assertEqual(p.converter.py_default, r"' \t\n\r\x0b\x0c\xa0'")
1157+
self.assertEqual(p.converter.c_default,
1158+
r'{.buf = " \t\n\r\v\f\302\240", .obj = NULL, .len = 8}')
1159+
1160+
def test_param_default_bytes_literal(self):
1161+
function = self.parse_function(r"""
1162+
module test
1163+
test.func
1164+
str: str(accept={robuffer}) = b' \t\n\r\v\f\xa0'
1165+
buf: Py_buffer = b' \t\n\r\v\f\xa0'
1166+
""")
1167+
p = function.parameters['str']
1168+
self.assertEqual(p.default, b' \t\n\r\v\f\xa0')
1169+
self.assertEqual(p.converter.py_default, r"b' \t\n\r\x0b\x0c\xa0'")
1170+
self.assertEqual(p.converter.c_default, r'" \t\n\r\v\f\240"')
1171+
1172+
p = function.parameters['buf']
1173+
self.assertEqual(p.default, b' \t\n\r\v\f\xa0')
1174+
self.assertEqual(p.converter.py_default, r"b' \t\n\r\x0b\x0c\xa0'")
1175+
self.assertEqual(p.converter.c_default,
1176+
r'{.buf = " \t\n\r\v\f\240", .obj = NULL, .len = 7}')
1177+
1178+
def test_param_default_byte_literal(self):
1179+
function = self.parse_function(r"""
1180+
module test
1181+
test.func
1182+
zero: char = b'\0'
1183+
one: char = b'\1'
1184+
lf: char = b'\n'
1185+
nbsp: char = b'\xa0'
1186+
""")
1187+
p = function.parameters['zero']
1188+
self.assertEqual(p.default, b'\0')
1189+
self.assertEqual(p.converter.py_default, r"b'\x00'")
1190+
self.assertEqual(p.converter.c_default, r"'\0'")
1191+
1192+
p = function.parameters['one']
1193+
self.assertEqual(p.default, b'\1')
1194+
self.assertEqual(p.converter.py_default, r"b'\x01'")
1195+
self.assertEqual(p.converter.c_default, r"'\001'")
1196+
1197+
p = function.parameters['lf']
1198+
self.assertEqual(p.default, b'\n')
1199+
self.assertEqual(p.converter.py_default, r"b'\n'")
1200+
self.assertEqual(p.converter.c_default, r"'\n'")
1201+
1202+
p = function.parameters['nbsp']
1203+
self.assertEqual(p.default, b'\xa0')
1204+
self.assertEqual(p.converter.py_default, r"b'\xa0'")
1205+
self.assertEqual(p.converter.c_default, r"'\240'")
1206+
1207+
def test_param_default_unicode_char(self):
1208+
function = self.parse_function(r"""
1209+
module test
1210+
test.func
1211+
zero: int(accept={str}) = '\0'
1212+
one: int(accept={str}) = '\1'
1213+
lf: int(accept={str}) = '\n'
1214+
nbsp: int(accept={str}) = '\xa0'
1215+
snake: int(accept={str}) = '\U0001f40d'
1216+
""")
1217+
p = function.parameters['zero']
1218+
self.assertEqual(p.default, '\0')
1219+
self.assertEqual(p.converter.py_default, r"'\x00'")
1220+
self.assertEqual(p.converter.c_default, '0')
1221+
1222+
p = function.parameters['one']
1223+
self.assertEqual(p.default, '\1')
1224+
self.assertEqual(p.converter.py_default, r"'\x01'")
1225+
self.assertEqual(p.converter.c_default, '0x01')
1226+
1227+
p = function.parameters['lf']
1228+
self.assertEqual(p.default, '\n')
1229+
self.assertEqual(p.converter.py_default, r"'\n'")
1230+
self.assertEqual(p.converter.c_default, r"'\n'")
1231+
1232+
p = function.parameters['nbsp']
1233+
self.assertEqual(p.default, '\xa0')
1234+
self.assertEqual(p.converter.py_default, r"'\xa0'")
1235+
self.assertEqual(p.converter.c_default, '0xa0')
1236+
1237+
p = function.parameters['snake']
1238+
self.assertEqual(p.default, '\U0001f40d')
1239+
self.assertEqual(p.converter.py_default, "'\U0001f40d'")
1240+
self.assertEqual(p.converter.c_default, '0x1f40d')
1241+
1242+
def test_param_default_bool(self):
1243+
function = self.parse_function(r"""
1244+
module test
1245+
test.func
1246+
bool: bool = True
1247+
intbool: bool(accept={int}) = True
1248+
intbool2: bool(accept={int}) = 2
1249+
""")
1250+
p = function.parameters['bool']
1251+
self.assertIs(p.default, True)
1252+
self.assertEqual(p.converter.py_default, 'True')
1253+
self.assertEqual(p.converter.c_default, '1')
1254+
1255+
p = function.parameters['intbool']
1256+
self.assertIs(p.default, True)
1257+
self.assertEqual(p.converter.py_default, 'True')
1258+
self.assertEqual(p.converter.c_default, '1')
1259+
1260+
p = function.parameters['intbool2']
1261+
self.assertEqual(p.default, 2)
1262+
self.assertEqual(p.converter.py_default, '2')
1263+
self.assertEqual(p.converter.c_default, '2')
1264+
10841265
def test_param_default_expr_named_constant(self):
10851266
function = self.parse_function("""
10861267
module os
@@ -4432,6 +4613,56 @@ def test_format_escape(self):
44324613
out = libclinic.format_escape(line)
44334614
self.assertEqual(out, expected)
44344615

4616+
def test_c_bytes_repr(self):
4617+
c_bytes_repr = libclinic.c_bytes_repr
4618+
self.assertEqual(c_bytes_repr(b''), '""')
4619+
self.assertEqual(c_bytes_repr(b'abc'), '"abc"')
4620+
self.assertEqual(c_bytes_repr(b'\a\b\f\n\r\t\v'), r'"\a\b\f\n\r\t\v"')
4621+
self.assertEqual(c_bytes_repr(b' \0\x7f'), r'" \000\177"')
4622+
self.assertEqual(c_bytes_repr(b'"'), r'"\""')
4623+
self.assertEqual(c_bytes_repr(b"'"), r'''"'"''')
4624+
self.assertEqual(c_bytes_repr(b'\\'), r'"\\"')
4625+
self.assertEqual(c_bytes_repr(b'??/'), r'"?\?/"')
4626+
self.assertEqual(c_bytes_repr(b'???/'), r'"?\?\?/"')
4627+
self.assertEqual(c_bytes_repr(b'/*****/ /*/ */*'), r'"/\*****\/ /\*\/ *\/\*"')
4628+
self.assertEqual(c_bytes_repr(b'\xa0'), r'"\240"')
4629+
self.assertEqual(c_bytes_repr(b'\xff'), r'"\377"')
4630+
4631+
def test_c_str_repr(self):
4632+
c_str_repr = libclinic.c_str_repr
4633+
self.assertEqual(c_str_repr(''), '""')
4634+
self.assertEqual(c_str_repr('abc'), '"abc"')
4635+
self.assertEqual(c_str_repr('\a\b\f\n\r\t\v'), r'"\a\b\f\n\r\t\v"')
4636+
self.assertEqual(c_str_repr(' \0\x7f'), r'" \000\177"')
4637+
self.assertEqual(c_str_repr('"'), r'"\""')
4638+
self.assertEqual(c_str_repr("'"), r'''"'"''')
4639+
self.assertEqual(c_str_repr('\\'), r'"\\"')
4640+
self.assertEqual(c_str_repr('??/'), r'"?\?/"')
4641+
self.assertEqual(c_str_repr('???/'), r'"?\?\?/"')
4642+
self.assertEqual(c_str_repr('/*****/ /*/ */*'), r'"/\*****\/ /\*\/ *\/\*"')
4643+
self.assertEqual(c_str_repr('\xa0'), r'"\u00a0"')
4644+
self.assertEqual(c_str_repr('\xff'), r'"\u00ff"')
4645+
self.assertEqual(c_str_repr('\u20ac'), r'"\u20ac"')
4646+
self.assertEqual(c_str_repr('\U0001f40d'), r'"\U0001f40d"')
4647+
4648+
def test_c_unichar_repr(self):
4649+
c_unichar_repr = libclinic.c_unichar_repr
4650+
self.assertEqual(c_unichar_repr('a'), "'a'")
4651+
self.assertEqual(c_unichar_repr('\n'), r"'\n'")
4652+
self.assertEqual(c_unichar_repr('\b'), r"'\b'")
4653+
self.assertEqual(c_unichar_repr('\0'), '0')
4654+
self.assertEqual(c_unichar_repr('\1'), '0x01')
4655+
self.assertEqual(c_unichar_repr('\x7f'), '0x7f')
4656+
self.assertEqual(c_unichar_repr(' '), "' '")
4657+
self.assertEqual(c_unichar_repr('"'), """'"'""")
4658+
self.assertEqual(c_unichar_repr("'"), r"'\''")
4659+
self.assertEqual(c_unichar_repr('\\'), r"'\\'")
4660+
self.assertEqual(c_unichar_repr('?'), "'?'")
4661+
self.assertEqual(c_unichar_repr('\xa0'), '0xa0')
4662+
self.assertEqual(c_unichar_repr('\xff'), '0xff')
4663+
self.assertEqual(c_unichar_repr('\u20ac'), '0x20ac')
4664+
self.assertEqual(c_unichar_repr('\U0001f40d'), '0x1f40d')
4665+
44354666
def test_indent_all_lines(self):
44364667
# Blank lines are expected to be unchanged.
44374668
self.assertEqual(libclinic.indent_all_lines("", prefix="bar"), "")

Modules/_datetimemodule.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6395,7 +6395,7 @@ datetime_str(PyObject *op)
63956395
/*[clinic input]
63966396
datetime.datetime.isoformat
63976397
6398-
sep: int(accept={str}, c_default="'T'", py_default="'T'") = ord('T')
6398+
sep: int(accept={str}) = 'T'
63996399
timespec: str(c_default="NULL") = 'auto'
64006400
64016401
Return the time formatted according to ISO.
@@ -6417,7 +6417,7 @@ terms of the time to include. Valid options are 'auto', 'hours',
64176417
static PyObject *
64186418
datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep,
64196419
const char *timespec)
6420-
/*[clinic end generated code: output=9b6ce1383189b0bf input=2fa2512172ccf5d5]*/
6420+
/*[clinic end generated code: output=9b6ce1383189b0bf input=db935a57fa697c5e]*/
64216421
{
64226422
char buffer[100];
64236423

Modules/_testclinic.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,14 @@ int_converter
334334
335335
a: int = 12
336336
b: int(accept={int}) = 34
337-
c: int(accept={str}) = 45
337+
c: int(accept={str}) = '-'
338338
/
339339
340340
[clinic start generated code]*/
341341

342342
static PyObject *
343343
int_converter_impl(PyObject *module, int a, int b, int c)
344-
/*[clinic end generated code: output=8e56b59be7d0c306 input=a1dbc6344853db7a]*/
344+
/*[clinic end generated code: output=8e56b59be7d0c306 input=9a306d4dc907e339]*/
345345
{
346346
RETURN_PACKED_ARGS(3, PyLong_FromLong, long, a, b, c);
347347
}
@@ -1365,14 +1365,15 @@ clone_f2_impl(PyObject *module, const char *path)
13651365
class custom_t_converter(CConverter):
13661366
type = 'custom_t'
13671367
converter = 'custom_converter'
1368+
c_init_default = "<placeholder>" # overridden in pre_render(()
13681369
13691370
def pre_render(self):
13701371
self.c_default = f'''{{
13711372
.name = "{self.function.name}",
13721373
}}'''
13731374
13741375
[python start generated code]*/
1375-
/*[python end generated code: output=da39a3ee5e6b4b0d input=b2fb801e99a06bf6]*/
1376+
/*[python end generated code: output=da39a3ee5e6b4b0d input=78fe84e5ecc0481b]*/
13761377

13771378

13781379
/*[clinic input]

0 commit comments

Comments
 (0)