mirror of
https://github.com/python/cpython.git
synced 2024-11-28 12:31:14 +08:00
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.
This commit is contained in:
parent
4eae1e5342
commit
1dd9510977
@ -13,6 +13,7 @@ import inspect
|
|||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
test_tools.skip_if_missing('clinic')
|
test_tools.skip_if_missing('clinic')
|
||||||
@ -21,6 +22,13 @@ with test_tools.imports_under_tool('clinic'):
|
|||||||
from clinic import DSLParser
|
from clinic import DSLParser
|
||||||
|
|
||||||
|
|
||||||
|
def default_namespace():
|
||||||
|
ns = types.SimpleNamespace()
|
||||||
|
ns.force = False
|
||||||
|
ns.limited_capi = clinic.DEFAULT_LIMITED_CAPI
|
||||||
|
return ns
|
||||||
|
|
||||||
|
|
||||||
def _make_clinic(*, filename='clinic_tests'):
|
def _make_clinic(*, filename='clinic_tests'):
|
||||||
clang = clinic.CLanguage(None)
|
clang = clinic.CLanguage(None)
|
||||||
c = clinic.Clinic(clang, filename=filename)
|
c = clinic.Clinic(clang, filename=filename)
|
||||||
@ -52,6 +60,11 @@ def _expect_failure(tc, parser, code, errmsg, *, filename=None, lineno=None,
|
|||||||
return cm.exception
|
return cm.exception
|
||||||
|
|
||||||
|
|
||||||
|
class MockClinic:
|
||||||
|
def __init__(self):
|
||||||
|
self.limited_capi = clinic.DEFAULT_LIMITED_CAPI
|
||||||
|
|
||||||
|
|
||||||
class ClinicWholeFileTest(TestCase):
|
class ClinicWholeFileTest(TestCase):
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
@ -691,8 +704,9 @@ class ParseFileUnitTest(TestCase):
|
|||||||
self, *, filename, expected_error, verify=True, output=None
|
self, *, filename, expected_error, verify=True, output=None
|
||||||
):
|
):
|
||||||
errmsg = re.escape(dedent(expected_error).strip())
|
errmsg = re.escape(dedent(expected_error).strip())
|
||||||
|
ns = default_namespace()
|
||||||
with self.assertRaisesRegex(clinic.ClinicError, errmsg):
|
with self.assertRaisesRegex(clinic.ClinicError, errmsg):
|
||||||
clinic.parse_file(filename)
|
clinic.parse_file(filename, ns=ns)
|
||||||
|
|
||||||
def test_parse_file_no_extension(self) -> None:
|
def test_parse_file_no_extension(self) -> None:
|
||||||
self.expect_parsing_failure(
|
self.expect_parsing_failure(
|
||||||
@ -832,8 +846,9 @@ class ClinicBlockParserTest(TestCase):
|
|||||||
|
|
||||||
blocks = list(clinic.BlockParser(input, language))
|
blocks = list(clinic.BlockParser(input, language))
|
||||||
writer = clinic.BlockPrinter(language)
|
writer = clinic.BlockPrinter(language)
|
||||||
|
mock_clinic = MockClinic()
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
writer.print_block(block)
|
writer.print_block(block, clinic=mock_clinic)
|
||||||
output = writer.f.getvalue()
|
output = writer.f.getvalue()
|
||||||
assert output == input, "output != input!\n\noutput " + repr(output) + "\n\n input " + repr(input)
|
assert output == input, "output != input!\n\noutput " + repr(output) + "\n\n input " + repr(input)
|
||||||
|
|
||||||
@ -3508,6 +3523,27 @@ class ClinicFunctionalTest(unittest.TestCase):
|
|||||||
self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")
|
self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import _testclinic_limited
|
||||||
|
except ImportError:
|
||||||
|
_testclinic_limited = None
|
||||||
|
|
||||||
|
@unittest.skipIf(_testclinic_limited is None, "_testclinic_limited is missing")
|
||||||
|
class LimitedCAPIFunctionalTest(unittest.TestCase):
|
||||||
|
locals().update((name, getattr(_testclinic_limited, name))
|
||||||
|
for name in dir(_testclinic_limited) if name.startswith('test_'))
|
||||||
|
|
||||||
|
def test_my_int_func(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_testclinic_limited.my_int_func()
|
||||||
|
self.assertEqual(_testclinic_limited.my_int_func(3), 3)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_testclinic_limited.my_int_func(1.0)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_testclinic_limited.my_int_func("xyz")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PermutationTests(unittest.TestCase):
|
class PermutationTests(unittest.TestCase):
|
||||||
"""Test permutation support functions."""
|
"""Test permutation support functions."""
|
||||||
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
:ref:`Argument Clinic <howto-clinic>` now has a partial support of the
|
||||||
|
:ref:`Limited API <limited-c-api>`. Patch by Victor Stinner.
|
@ -161,6 +161,7 @@
|
|||||||
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
|
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
|
||||||
@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
|
@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
|
||||||
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
|
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
|
||||||
|
@MODULE__TESTCLINIC_TRUE@_testclinic_limited _testclinic_limited.c
|
||||||
|
|
||||||
# Some testing modules MUST be built as shared libraries.
|
# Some testing modules MUST be built as shared libraries.
|
||||||
*shared*
|
*shared*
|
||||||
|
69
Modules/_testclinic_limited.c
Normal file
69
Modules/_testclinic_limited.c
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// For now, only limited C API 3.13 is supported
|
||||||
|
#define Py_LIMITED_API 0x030d0000
|
||||||
|
|
||||||
|
/* Always enable assertions */
|
||||||
|
#undef NDEBUG
|
||||||
|
|
||||||
|
#include "Python.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include "clinic/_testclinic_limited.c.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
module _testclinic_limited
|
||||||
|
[clinic start generated code]*/
|
||||||
|
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=dd408149a4fc0dbb]*/
|
||||||
|
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
test_empty_function
|
||||||
|
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
test_empty_function_impl(PyObject *module)
|
||||||
|
/*[clinic end generated code: output=0f8aeb3ddced55cb input=0dd7048651ad4ae4]*/
|
||||||
|
{
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
my_int_func -> int
|
||||||
|
|
||||||
|
arg: int
|
||||||
|
/
|
||||||
|
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_int_func_impl(PyObject *module, int arg)
|
||||||
|
/*[clinic end generated code: output=761cd54582f10e4f input=16eb8bba71d82740]*/
|
||||||
|
{
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyMethodDef tester_methods[] = {
|
||||||
|
TEST_EMPTY_FUNCTION_METHODDEF
|
||||||
|
MY_INT_FUNC_METHODDEF
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct PyModuleDef _testclinic_module = {
|
||||||
|
PyModuleDef_HEAD_INIT,
|
||||||
|
.m_name = "_testclinic_limited",
|
||||||
|
.m_size = 0,
|
||||||
|
.m_methods = tester_methods,
|
||||||
|
};
|
||||||
|
|
||||||
|
PyMODINIT_FUNC
|
||||||
|
PyInit__testclinic_limited(void)
|
||||||
|
{
|
||||||
|
PyObject *m = PyModule_Create(&_testclinic_module);
|
||||||
|
if (m == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
53
Modules/clinic/_testclinic_limited.c.h
generated
Normal file
53
Modules/clinic/_testclinic_limited.c.h
generated
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*[clinic input]
|
||||||
|
preserve
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
PyDoc_STRVAR(test_empty_function__doc__,
|
||||||
|
"test_empty_function($module, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n");
|
||||||
|
|
||||||
|
#define TEST_EMPTY_FUNCTION_METHODDEF \
|
||||||
|
{"test_empty_function", (PyCFunction)test_empty_function, METH_NOARGS, test_empty_function__doc__},
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
test_empty_function_impl(PyObject *module);
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
test_empty_function(PyObject *module, PyObject *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
return test_empty_function_impl(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyDoc_STRVAR(my_int_func__doc__,
|
||||||
|
"my_int_func($module, arg, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n");
|
||||||
|
|
||||||
|
#define MY_INT_FUNC_METHODDEF \
|
||||||
|
{"my_int_func", (PyCFunction)my_int_func, METH_O, my_int_func__doc__},
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_int_func_impl(PyObject *module, int arg);
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
my_int_func(PyObject *module, PyObject *arg_)
|
||||||
|
{
|
||||||
|
PyObject *return_value = NULL;
|
||||||
|
int arg;
|
||||||
|
int _return_value;
|
||||||
|
|
||||||
|
arg = PyLong_AsInt(arg_);
|
||||||
|
if (arg == -1 && PyErr_Occurred()) {
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
_return_value = my_int_func_impl(module, arg);
|
||||||
|
if ((_return_value == -1) && PyErr_Occurred()) {
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
return_value = PyLong_FromLong((long)_return_value);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
return return_value;
|
||||||
|
}
|
||||||
|
/*[clinic end generated code: output=07e2e8ed6923cd16 input=a9049054013a1b77]*/
|
@ -28,6 +28,7 @@ IGNORE = {
|
|||||||
'_testbuffer',
|
'_testbuffer',
|
||||||
'_testcapi',
|
'_testcapi',
|
||||||
'_testclinic',
|
'_testclinic',
|
||||||
|
'_testclinic_limited',
|
||||||
'_testconsole',
|
'_testconsole',
|
||||||
'_testimportmultiple',
|
'_testimportmultiple',
|
||||||
'_testinternalcapi',
|
'_testinternalcapi',
|
||||||
|
@ -63,6 +63,7 @@ from typing import (
|
|||||||
|
|
||||||
version = '1'
|
version = '1'
|
||||||
|
|
||||||
|
DEFAULT_LIMITED_CAPI = False
|
||||||
NO_VARARG = "PY_SSIZE_T_MAX"
|
NO_VARARG = "PY_SSIZE_T_MAX"
|
||||||
CLINIC_PREFIX = "__clinic_"
|
CLINIC_PREFIX = "__clinic_"
|
||||||
CLINIC_PREFIXED_ARGS = {
|
CLINIC_PREFIXED_ARGS = {
|
||||||
@ -1360,7 +1361,21 @@ class CLanguage(Language):
|
|||||||
vararg
|
vararg
|
||||||
)
|
)
|
||||||
nargs = f"Py_MIN(nargs, {max_pos})" if max_pos else "0"
|
nargs = f"Py_MIN(nargs, {max_pos})" if max_pos else "0"
|
||||||
if not new_or_init:
|
|
||||||
|
if clinic.limited_capi:
|
||||||
|
# positional-or-keyword arguments
|
||||||
|
flags = "METH_VARARGS|METH_KEYWORDS"
|
||||||
|
|
||||||
|
parser_prototype = self.PARSER_PROTOTYPE_KEYWORD
|
||||||
|
parser_code = [normalize_snippet("""
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "{format_units}:{name}", _keywords,
|
||||||
|
{parse_arguments}))
|
||||||
|
goto exit;
|
||||||
|
""", indent=4)]
|
||||||
|
argname_fmt = 'args[%d]'
|
||||||
|
declarations = ""
|
||||||
|
|
||||||
|
elif not new_or_init:
|
||||||
flags = "METH_FASTCALL|METH_KEYWORDS"
|
flags = "METH_FASTCALL|METH_KEYWORDS"
|
||||||
parser_prototype = self.PARSER_PROTOTYPE_FASTCALL_KEYWORDS
|
parser_prototype = self.PARSER_PROTOTYPE_FASTCALL_KEYWORDS
|
||||||
argname_fmt = 'args[%d]'
|
argname_fmt = 'args[%d]'
|
||||||
@ -2111,7 +2126,8 @@ class BlockPrinter:
|
|||||||
self,
|
self,
|
||||||
block: Block,
|
block: Block,
|
||||||
*,
|
*,
|
||||||
core_includes: bool = False
|
core_includes: bool = False,
|
||||||
|
clinic: Clinic | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
input = block.input
|
input = block.input
|
||||||
output = block.output
|
output = block.output
|
||||||
@ -2140,7 +2156,11 @@ class BlockPrinter:
|
|||||||
write("\n")
|
write("\n")
|
||||||
|
|
||||||
output = ''
|
output = ''
|
||||||
if core_includes:
|
if clinic:
|
||||||
|
limited_capi = clinic.limited_capi
|
||||||
|
else:
|
||||||
|
limited_capi = DEFAULT_LIMITED_CAPI
|
||||||
|
if core_includes and not limited_capi:
|
||||||
output += textwrap.dedent("""
|
output += textwrap.dedent("""
|
||||||
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
|
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
|
||||||
# include "pycore_gc.h" // PyGC_Head
|
# include "pycore_gc.h" // PyGC_Head
|
||||||
@ -2344,6 +2364,7 @@ impl_definition block
|
|||||||
*,
|
*,
|
||||||
filename: str,
|
filename: str,
|
||||||
verify: bool = True,
|
verify: bool = True,
|
||||||
|
limited_capi: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
# maps strings to Parser objects.
|
# maps strings to Parser objects.
|
||||||
# (instantiated from the "parsers" global.)
|
# (instantiated from the "parsers" global.)
|
||||||
@ -2353,6 +2374,7 @@ impl_definition block
|
|||||||
fail("Custom printers are broken right now")
|
fail("Custom printers are broken right now")
|
||||||
self.printer = printer or BlockPrinter(language)
|
self.printer = printer or BlockPrinter(language)
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
self.limited_capi = limited_capi
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.modules: ModuleDict = {}
|
self.modules: ModuleDict = {}
|
||||||
self.classes: ClassDict = {}
|
self.classes: ClassDict = {}
|
||||||
@ -2450,7 +2472,7 @@ impl_definition block
|
|||||||
self.parsers[dsl_name] = parsers[dsl_name](self)
|
self.parsers[dsl_name] = parsers[dsl_name](self)
|
||||||
parser = self.parsers[dsl_name]
|
parser = self.parsers[dsl_name]
|
||||||
parser.parse(block)
|
parser.parse(block)
|
||||||
printer.print_block(block)
|
printer.print_block(block, clinic=self)
|
||||||
|
|
||||||
# these are destinations not buffers
|
# these are destinations not buffers
|
||||||
for name, destination in self.destinations.items():
|
for name, destination in self.destinations.items():
|
||||||
@ -2465,7 +2487,7 @@ impl_definition block
|
|||||||
block.input = "dump " + name + "\n"
|
block.input = "dump " + name + "\n"
|
||||||
warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.")
|
warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.")
|
||||||
printer.write("\n")
|
printer.write("\n")
|
||||||
printer.print_block(block)
|
printer.print_block(block, clinic=self)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if destination.type == 'file':
|
if destination.type == 'file':
|
||||||
@ -2490,7 +2512,7 @@ impl_definition block
|
|||||||
|
|
||||||
block.input = 'preserve\n'
|
block.input = 'preserve\n'
|
||||||
printer_2 = BlockPrinter(self.language)
|
printer_2 = BlockPrinter(self.language)
|
||||||
printer_2.print_block(block, core_includes=True)
|
printer_2.print_block(block, core_includes=True, clinic=self)
|
||||||
write_file(destination.filename, printer_2.f.getvalue())
|
write_file(destination.filename, printer_2.f.getvalue())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -2536,9 +2558,15 @@ impl_definition block
|
|||||||
def parse_file(
|
def parse_file(
|
||||||
filename: str,
|
filename: str,
|
||||||
*,
|
*,
|
||||||
verify: bool = True,
|
ns: argparse.Namespace,
|
||||||
output: str | None = None
|
output: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
verify = not ns.force
|
||||||
|
limited_capi = ns.limited_capi
|
||||||
|
# XXX Temporary solution
|
||||||
|
if os.path.basename(filename) == '_testclinic_limited.c':
|
||||||
|
print(f"{filename} uses limited C API")
|
||||||
|
limited_capi = True
|
||||||
if not output:
|
if not output:
|
||||||
output = filename
|
output = filename
|
||||||
|
|
||||||
@ -2560,7 +2588,10 @@ def parse_file(
|
|||||||
return
|
return
|
||||||
|
|
||||||
assert isinstance(language, CLanguage)
|
assert isinstance(language, CLanguage)
|
||||||
clinic = Clinic(language, verify=verify, filename=filename)
|
clinic = Clinic(language,
|
||||||
|
verify=verify,
|
||||||
|
filename=filename,
|
||||||
|
limited_capi=limited_capi)
|
||||||
cooked = clinic.parse(raw)
|
cooked = clinic.parse(raw)
|
||||||
|
|
||||||
write_file(output, cooked)
|
write_file(output, cooked)
|
||||||
@ -5987,6 +6018,8 @@ For more information see https://docs.python.org/3/howto/clinic.html""")
|
|||||||
cmdline.add_argument("--exclude", type=str, action="append",
|
cmdline.add_argument("--exclude", type=str, action="append",
|
||||||
help=("a file to exclude in --make mode; "
|
help=("a file to exclude in --make mode; "
|
||||||
"can be given multiple times"))
|
"can be given multiple times"))
|
||||||
|
cmdline.add_argument("--limited", dest="limited_capi", action='store_true',
|
||||||
|
help="use the Limited C API")
|
||||||
cmdline.add_argument("filename", metavar="FILE", type=str, nargs="*",
|
cmdline.add_argument("filename", metavar="FILE", type=str, nargs="*",
|
||||||
help="the list of files to process")
|
help="the list of files to process")
|
||||||
return cmdline
|
return cmdline
|
||||||
@ -6077,7 +6110,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
|
|||||||
continue
|
continue
|
||||||
if ns.verbose:
|
if ns.verbose:
|
||||||
print(path)
|
print(path)
|
||||||
parse_file(path, verify=not ns.force)
|
parse_file(path, ns=ns)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not ns.filename:
|
if not ns.filename:
|
||||||
@ -6089,7 +6122,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: argparse.Namespace) -> None:
|
|||||||
for filename in ns.filename:
|
for filename in ns.filename:
|
||||||
if ns.verbose:
|
if ns.verbose:
|
||||||
print(filename)
|
print(filename)
|
||||||
parse_file(filename, output=ns.output, verify=not ns.force)
|
parse_file(filename, output=ns.output, ns=ns)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> NoReturn:
|
def main(argv: list[str] | None = None) -> NoReturn:
|
||||||
|
Loading…
Reference in New Issue
Block a user