Skip to content

Commit 1dd9510

Browse files
authored
gh-108494: Argument Clinic partial supports of Limited C API (#108495)
Argument Clinic now has a partial support of the Limited API: * Add --limited option to clinic.c. * Add '_testclinic_limited' extension which is built with the limited C API version 3.13. * For now, hardcode in clinic.py that "_testclinic_limited.c" targets the limited C API.
1 parent 4eae1e5 commit 1dd9510

File tree

7 files changed

+208
-13
lines changed

7 files changed

+208
-13
lines changed

Lib/test/test_clinic.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import os.path
1414
import re
1515
import sys
16+
import types
1617
import unittest
1718

1819
test_tools.skip_if_missing('clinic')
@@ -21,6 +22,13 @@
2122
from clinic import DSLParser
2223

2324

25+
def default_namespace():
26+
ns = types.SimpleNamespace()
27+
ns.force = False
28+
ns.limited_capi = clinic.DEFAULT_LIMITED_CAPI
29+
return ns
30+
31+
2432
def _make_clinic(*, filename='clinic_tests'):
2533
clang = clinic.CLanguage(None)
2634
c = clinic.Clinic(clang, filename=filename)
@@ -52,6 +60,11 @@ def _expect_failure(tc, parser, code, errmsg, *, filename=None, lineno=None,
5260
return cm.exception
5361

5462

63+
class MockClinic:
64+
def __init__(self):
65+
self.limited_capi = clinic.DEFAULT_LIMITED_CAPI
66+
67+
5568
class ClinicWholeFileTest(TestCase):
5669
maxDiff = None
5770

@@ -691,8 +704,9 @@ def expect_parsing_failure(
691704
self, *, filename, expected_error, verify=True, output=None
692705
):
693706
errmsg = re.escape(dedent(expected_error).strip())
707+
ns = default_namespace()
694708
with self.assertRaisesRegex(clinic.ClinicError, errmsg):
695-
clinic.parse_file(filename)
709+
clinic.parse_file(filename, ns=ns)
696710

697711
def test_parse_file_no_extension(self) -> None:
698712
self.expect_parsing_failure(
@@ -832,8 +846,9 @@ def _test(self, input, output):
832846

833847
blocks = list(clinic.BlockParser(input, language))
834848
writer = clinic.BlockPrinter(language)
849+
mock_clinic = MockClinic()
835850
for block in blocks:
836-
writer.print_block(block)
851+
writer.print_block(block, clinic=mock_clinic)
837852
output = writer.f.getvalue()
838853
assert output == input, "output != input!\n\noutput " + repr(output) + "\n\n input " + repr(input)
839854

@@ -3508,6 +3523,27 @@ def test_depr_multi(self):
35083523
self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")
35093524

35103525

3526+
try:
3527+
import _testclinic_limited
3528+
except ImportError:
3529+
_testclinic_limited = None
3530+
3531+
@unittest.skipIf(_testclinic_limited is None, "_testclinic_limited is missing")
3532+
class LimitedCAPIFunctionalTest(unittest.TestCase):
3533+
locals().update((name, getattr(_testclinic_limited, name))
3534+
for name in dir(_testclinic_limited) if name.startswith('test_'))
3535+
3536+
def test_my_int_func(self):
3537+
with self.assertRaises(TypeError):
3538+
_testclinic_limited.my_int_func()
3539+
self.assertEqual(_testclinic_limited.my_int_func(3), 3)
3540+
with self.assertRaises(TypeError):
3541+
_testclinic_limited.my_int_func(1.0)
3542+
with self.assertRaises(TypeError):
3543+
_testclinic_limited.my_int_func("xyz")
3544+
3545+
3546+
35113547
class PermutationTests(unittest.TestCase):
35123548
"""Test permutation support functions."""
35133549

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:ref:`Argument Clinic <howto-clinic>` now has a partial support of the
2+
:ref:`Limited API <limited-c-api>`. Patch by Victor Stinner.

Modules/Setup.stdlib.in

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@
161161
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
162162
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyos.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c
163163
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
164+
@MODULE__TESTCLINIC_TRUE@_testclinic_limited _testclinic_limited.c
164165

165166
# Some testing modules MUST be built as shared libraries.
166167
*shared*

Modules/_testclinic_limited.c

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// For now, only limited C API 3.13 is supported
2+
#define Py_LIMITED_API 0x030d0000
3+
4+
/* Always enable assertions */
5+
#undef NDEBUG
6+
7+
#include "Python.h"
8+
9+
10+
#include "clinic/_testclinic_limited.c.h"
11+
12+
13+
/*[clinic input]
14+
module _testclinic_limited
15+
[clinic start generated code]*/
16+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=dd408149a4fc0dbb]*/
17+
18+
19+
/*[clinic input]
20+
test_empty_function
21+
22+
[clinic start generated code]*/
23+
24+
static PyObject *
25+
test_empty_function_impl(PyObject *module)
26+
/*[clinic end generated code: output=0f8aeb3ddced55cb input=0dd7048651ad4ae4]*/
27+
{
28+
Py_RETURN_NONE;
29+
}
30+
31+
32+
/*[clinic input]
33+
my_int_func -> int
34+
35+
arg: int
36+
/
37+
38+
[clinic start generated code]*/
39+
40+
static int
41+
my_int_func_impl(PyObject *module, int arg)
42+
/*[clinic end generated code: output=761cd54582f10e4f input=16eb8bba71d82740]*/
43+
{
44+
return arg;
45+
}
46+
47+
48+
static PyMethodDef tester_methods[] = {
49+
TEST_EMPTY_FUNCTION_METHODDEF
50+
MY_INT_FUNC_METHODDEF
51+
{NULL, NULL}
52+
};
53+
54+
static struct PyModuleDef _testclinic_module = {
55+
PyModuleDef_HEAD_INIT,
56+
.m_name = "_testclinic_limited",
57+
.m_size = 0,
58+
.m_methods = tester_methods,
59+
};
60+
61+
PyMODINIT_FUNC
62+
PyInit__testclinic_limited(void)
63+
{
64+
PyObject *m = PyModule_Create(&_testclinic_module);
65+
if (m == NULL) {
66+
return NULL;
67+
}
68+
return m;
69+
}

Modules/clinic/_testclinic_limited.c.h

+53
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tools/build/generate_stdlib_module_names.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
'_testbuffer',
2929
'_testcapi',
3030
'_testclinic',
31+
'_testclinic_limited',
3132
'_testconsole',
3233
'_testimportmultiple',
3334
'_testinternalcapi',

Tools/clinic/clinic.py

+44-11
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
version = '1'
6565

66+
DEFAULT_LIMITED_CAPI = False
6667
NO_VARARG = "PY_SSIZE_T_MAX"
6768
CLINIC_PREFIX = "__clinic_"
6869
CLINIC_PREFIXED_ARGS = {
@@ -1360,7 +1361,21 @@ def parser_body(
13601361
vararg
13611362
)
13621363
nargs = f"Py_MIN(nargs, {max_pos})" if max_pos else "0"
1363-
if not new_or_init:
1364+
1365+
if clinic.limited_capi:
1366+
# positional-or-keyword arguments
1367+
flags = "METH_VARARGS|METH_KEYWORDS"
1368+
1369+
parser_prototype = self.PARSER_PROTOTYPE_KEYWORD
1370+
parser_code = [normalize_snippet("""
1371+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "{format_units}:{name}", _keywords,
1372+
{parse_arguments}))
1373+
goto exit;
1374+
""", indent=4)]
1375+
argname_fmt = 'args[%d]'
1376+
declarations = ""
1377+
1378+
elif not new_or_init:
13641379
flags = "METH_FASTCALL|METH_KEYWORDS"
13651380
parser_prototype = self.PARSER_PROTOTYPE_FASTCALL_KEYWORDS
13661381
argname_fmt = 'args[%d]'
@@ -2111,7 +2126,8 @@ def print_block(
21112126
self,
21122127
block: Block,
21132128
*,
2114-
core_includes: bool = False
2129+
core_includes: bool = False,
2130+
clinic: Clinic | None = None,
21152131
) -> None:
21162132
input = block.input
21172133
output = block.output
@@ -2140,7 +2156,11 @@ def print_block(
21402156
write("\n")
21412157

21422158
output = ''
2143-
if core_includes:
2159+
if clinic:
2160+
limited_capi = clinic.limited_capi
2161+
else:
2162+
limited_capi = DEFAULT_LIMITED_CAPI
2163+
if core_includes and not limited_capi:
21442164
output += textwrap.dedent("""
21452165
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
21462166
# include "pycore_gc.h" // PyGC_Head
@@ -2344,6 +2364,7 @@ def __init__(
23442364
*,
23452365
filename: str,
23462366
verify: bool = True,
2367+
limited_capi: bool = False,
23472368
) -> None:
23482369
# maps strings to Parser objects.
23492370
# (instantiated from the "parsers" global.)
@@ -2353,6 +2374,7 @@ def __init__(
23532374
fail("Custom printers are broken right now")
23542375
self.printer = printer or BlockPrinter(language)
23552376
self.verify = verify
2377+
self.limited_capi = limited_capi
23562378
self.filename = filename
23572379
self.modules: ModuleDict = {}
23582380
self.classes: ClassDict = {}
@@ -2450,7 +2472,7 @@ def parse(self, input: str) -> str:
24502472
self.parsers[dsl_name] = parsers[dsl_name](self)
24512473
parser = self.parsers[dsl_name]
24522474
parser.parse(block)
2453-
printer.print_block(block)
2475+
printer.print_block(block, clinic=self)
24542476

24552477
# these are destinations not buffers
24562478
for name, destination in self.destinations.items():
@@ -2465,7 +2487,7 @@ def parse(self, input: str) -> str:
24652487
block.input = "dump " + name + "\n"
24662488
warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.")
24672489
printer.write("\n")
2468-
printer.print_block(block)
2490+
printer.print_block(block, clinic=self)
24692491
continue
24702492

24712493
if destination.type == 'file':
@@ -2490,7 +2512,7 @@ def parse(self, input: str) -> str:
24902512

24912513
block.input = 'preserve\n'
24922514
printer_2 = BlockPrinter(self.language)
2493-
printer_2.print_block(block, core_includes=True)
2515+
printer_2.print_block(block, core_includes=True, clinic=self)
24942516
write_file(destination.filename, printer_2.f.getvalue())
24952517
continue
24962518

@@ -2536,9 +2558,15 @@ def __repr__(self) -> str:
25362558
def parse_file(
25372559
filename: str,
25382560
*,
2539-
verify: bool = True,
2540-
output: str | None = None
2561+
ns: argparse.Namespace,
2562+
output: str | None = None,
25412563
) -> None:
2564+
verify = not ns.force
2565+
limited_capi = ns.limited_capi
2566+
# XXX Temporary solution
2567+
if os.path.basename(filename) == '_testclinic_limited.c':
2568+
print(f"{filename} uses limited C API")
2569+
limited_capi = True
25422570
if not output:
25432571
output = filename
25442572

@@ -2560,7 +2588,10 @@ def parse_file(
25602588
return
25612589

25622590
assert isinstance(language, CLanguage)
2563-
clinic = Clinic(language, verify=verify, filename=filename)
2591+
clinic = Clinic(language,
2592+
verify=verify,
2593+
filename=filename,
2594+
limited_capi=limited_capi)
25642595
cooked = clinic.parse(raw)
25652596

25662597
write_file(output, cooked)
@@ -5987,6 +6018,8 @@ def create_cli() -> argparse.ArgumentParser:
59876018
cmdline.add_argument("--exclude", type=str, action="append",
59886019
help=("a file to exclude in --make mode; "
59896020
"can be given multiple times"))
6021+
cmdline.add_argument("--limited", dest="limited_capi", action='store_true',
6022+
help="use the Limited C API")
59906023
cmdline.add_argument("filename", metavar="FILE", type=str, nargs="*",
59916024
help="the list of files to process")
59926025
return cmdline
@@ -6077,7 +6110,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
60776110
continue
60786111
if ns.verbose:
60796112
print(path)
6080-
parse_file(path, verify=not ns.force)
6113+
parse_file(path, ns=ns)
60816114
return
60826115

60836116
if not ns.filename:
@@ -6089,7 +6122,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
60896122
for filename in ns.filename:
60906123
if ns.verbose:
60916124
print(filename)
6092-
parse_file(filename, output=ns.output, verify=not ns.force)
6125+
parse_file(filename, output=ns.output, ns=ns)
60936126

60946127

60956128
def main(argv: list[str] | None = None) -> NoReturn:

0 commit comments

Comments
 (0)