Commit 7726ac91 authored by Larry Hastings's avatar Larry Hastings

#Issue 20456: Several improvements and bugfixes for Argument Clinic,

including correctly generating code for Clinic blocks inside C
preprocessor conditional blocks.
parent 04edd2eb
......@@ -561,8 +561,8 @@ in ``Lib/inspect.py``.
to allow full expressions like ``CONSTANT - 1``.)
Renaming the C functions generated by Argument Clinic
-----------------------------------------------------
Renaming the C functions and variables generated by Argument Clinic
-------------------------------------------------------------------
Argument Clinic automatically names the functions it generates for you.
Occasionally this may cause a problem, if the generated name collides with
......@@ -584,6 +584,25 @@ The base function would now be named ``pickler_dumper()``,
and the impl function would now be named ``pickler_dumper_impl()``.
Similarly, you may have a problem where you want to give a parameter
a specific Python name, but that name may be inconvenient in C. Argument
Clinic allows you to give a parameter different names in Python and in C,
using the same ``"as"`` syntax::
/*[clinic input]
pickle.Pickler.dump
obj: object
file as file_obj: object
protocol: object = NULL
*
fix_imports: bool = True
Here, the name used in Python (in the signature and the ``keywords``
array) would be ``file``, but the C variable would be named ``file_obj``.
You can use this to rename the ``self`` parameter too!
Converting functions using PyArg_UnpackTuple
--------------------------------------------
......@@ -1308,74 +1327,6 @@ them ``__new__`` or ``__init__`` as appropriate. Notes:
(If your function doesn't support keywords, the parsing function
generated will throw an exception if it receives any.)
The #ifdef trick
----------------------------------------------
If you're converting a function that isn't available on all platforms,
there's a trick you can use to make life a little easier. The existing
code probably looks like this::
#ifdef HAVE_FUNCTIONNAME
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */
And then in the ``PyMethodDef`` structure at the bottom you'll have::
#ifdef HAVE_FUNCTIONNAME
{'functionname', ... },
#endif /* HAVE_FUNCTIONNAME */
In this scenario, you should change the code to look like the following::
#ifdef HAVE_FUNCTIONNAME
/*[clinic input]
module.functionname
...
[clinic start generated code]*/
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */
Run Argument Clinic on the code in this state, then refresh the file in
your editor. Now you'll have the generated code, including the #define
for the ``PyMethodDef``, like so::
#ifdef HAVE_FUNCTIONNAME
/*[clinic input]
...
[clinic start generated code]*/
...
#define MODULE_FUNCTIONNAME \
{'functionname', ... },
...
/*[clinic end generated code: checksum=...]*/
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */
Change the #endif at the bottom as follows::
#else
#define MODULE_FUNCTIONNAME
#endif /* HAVE_FUNCTIONNAME */
Now you can remove the #ifdefs around the ``PyMethodDef`` structure
at the end, and replace those three lines with ``MODULE_FUNCTIONNAME``.
If the function is available, the macro turns into the ``PyMethodDef``
static value, including the trailing comma; if the function isn't
available, the macro turns into nothing. Perfect!
(This is the preferred approach for optional functions; in the future,
Argument Clinic may generate the entire ``PyMethodDef`` structure.)
Changing and redirecting Clinic's output
----------------------------------------
......@@ -1491,8 +1442,9 @@ previous configuration.
``output preset`` sets Clinic's output to one of several built-in
preset configurations, as follows:
``original``
Clinic's starting configuration.
``block``
Clinic's original starting configuration. Writes everything
immediately after the input block.
Suppress the ``parser_prototype``
and ``docstring_prototype``, write everything else to ``block``.
......@@ -1640,6 +1592,82 @@ it in a Clinic block lets Clinic use its existing checksum functionality to ensu
the file was not modified by hand before it gets overwritten.
The #ifdef trick
----------------------------------------------
If you're converting a function that isn't available on all platforms,
there's a trick you can use to make life a little easier. The existing
code probably looks like this::
#ifdef HAVE_FUNCTIONNAME
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */
And then in the ``PyMethodDef`` structure at the bottom the existing code
will have::
#ifdef HAVE_FUNCTIONNAME
{'functionname', ... },
#endif /* HAVE_FUNCTIONNAME */
In this scenario, you should enclose the body of your impl function inside the ``#ifdef``,
like so::
#ifdef HAVE_FUNCTIONNAME
/*[clinic input]
module.functionname
...
[clinic start generated code]*/
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */
Then, remove those three lines from the ``PyMethodDef`` structure,
replacing them with the macro Argument Clinic generated::
MODULE_FUNCTIONNAME_METHODDEF
(You can find the real name for this macro inside the generated code.
Or you can calculate it yourself: it's the name of your function as defined
on the first line of your block, but with periods changed to underscores,
uppercased, and ``"_METHODDEF"`` added to the end.)
Perhaps you're wondering: what if ``HAVE_FUNCTIONNAME`` isn't defined?
The ``MODULE_FUNCTIONNAME_METHODDEF`` macro won't be defined either!
Here's where Argument Clinic gets very clever. It actually detects that the
Argument Clinic block might be deactivated by the ``#ifdef``. When that
happens, it generates a little extra code that looks like this::
#ifndef MODULE_FUNCTIONNAME_METHODDEF
#define MODULE_FUNCTIONNAME_METHODDEF
#endif /* !defined(MODULE_FUNCTIONNAME_METHODDEF) */
That means the macro always works. If the function is defined, this turns
into the correct structure, including the trailing comma. If the function is
undefined, this turns into nothing.
However, this causes one ticklish problem: where should Argument Clinic put this
extra code when using the "block" output preset? It can't go in the output block,
because that could be decativated by the ``#ifdef``. (That's the whole point!)
In this situation, Argument Clinic writes the extra code to the "buffer" destination.
This may mean that you get a complaint from Argument Clinic::
Warning in file "Modules/posixmodule.c" on line 12357:
Destination buffer 'buffer' not empty at end of file, emptying.
When this happens, just open your file, find the ``dump buffer`` block that
Argument Clinic added to your file (it'll be at the very bottom), then
move it above the ``PyMethodDef`` structure where that macro is used.
Using Argument Clinic in Python files
-------------------------------------
......
......@@ -95,6 +95,20 @@ Tests
Tools/Demos
-----------
- #Issue 20456: Argument Clinic now observes the C preprocessor conditional
compilation statements of the C files it parses. When a Clinic block is
inside a conditional code, it adjusts its output to match, including
automatically generating an empty methoddef macro.
- #Issue 20456: Cloned functions in Argument Clinic now use the correct
name, not the name of the function they were cloned from, for text
strings inside generated code.
- #Issue 20456: Fixed Argument Clinic's test suite and "--converters" feature.
- #Issue 20456: Argument Clinic now allows specifying different names
for a parameter in Python and C, using "as" on the parameter line.
- Issue #20326: Argument Clinic now uses a simple, unique signature to
annotate text signatures in docstrings, resulting in fewer false
positives. "self" parameters are also explicitly marked, allowing
......
......@@ -584,7 +584,7 @@ current settings for the window object.
[clinic start generated code]*/
PyDoc_STRVAR(curses_window_addch__doc__,
"addch(self, [x, y,] ch, [attr])\n"
"addch([x, y,] ch, [attr])\n"
"Paint character ch at (y, x) with attributes attr.\n"
"\n"
" x\n"
......@@ -651,7 +651,7 @@ exit:
static PyObject *
curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1, int x, int y, PyObject *ch, int group_right_1, long attr)
/*[clinic end generated code: output=e1cdbd4f4e42fc6b input=fe7e3711d5bbf1f6]*/
/*[clinic end generated code: output=43acb91a5c98f615 input=fe7e3711d5bbf1f6]*/
{
PyCursesWindowObject *cwself = (PyCursesWindowObject *)self;
int coordinates_group = group_left_1;
......
......@@ -52,10 +52,11 @@ static PyObject *DbmError;
/*[python input]
class dbmobject_converter(self_converter):
type = "dbmobject *"
def converter_init(self):
def pre_render(self):
super().pre_render()
self.name = 'dp'
[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=8a69ac1827811128]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=6ad536357913879a]*/
static PyObject *
newdbmobject(const char *file, int flags, int mode)
......@@ -270,23 +271,21 @@ dbm.dbm.get
self: dbmobject
key: str(length=True)
[
default: object
]
default: object = None
/
Return the value for key if present, otherwise default.
[clinic start generated code]*/
PyDoc_STRVAR(dbm_dbm_get__doc__,
"get(self, key, [default])\n"
"sig=($self, key, default=None)\n"
"Return the value for key if present, otherwise default.");
#define DBM_DBM_GET_METHODDEF \
{"get", (PyCFunction)dbm_dbm_get, METH_VARARGS, dbm_dbm_get__doc__},
static PyObject *
dbm_dbm_get_impl(dbmobject *dp, const char *key, Py_ssize_clean_t key_length, int group_right_1, PyObject *default_value);
dbm_dbm_get_impl(dbmobject *dp, const char *key, Py_ssize_clean_t key_length, PyObject *default_value);
static PyObject *
dbm_dbm_get(dbmobject *dp, PyObject *args)
......@@ -294,37 +293,24 @@ dbm_dbm_get(dbmobject *dp, PyObject *args)
PyObject *return_value = NULL;
const char *key;
Py_ssize_clean_t key_length;
int group_right_1 = 0;
PyObject *default_value = NULL;
switch (PyTuple_GET_SIZE(args)) {
case 1:
if (!PyArg_ParseTuple(args, "s#:get", &key, &key_length))
goto exit;
break;
case 2:
if (!PyArg_ParseTuple(args, "s#O:get", &key, &key_length, &default_value))
goto exit;
group_right_1 = 1;
break;
default:
PyErr_SetString(PyExc_TypeError, "dbm.dbm.get requires 1 to 2 arguments");
goto exit;
}
return_value = dbm_dbm_get_impl(dp, key, key_length, group_right_1, default_value);
PyObject *default_value = Py_None;
if (!PyArg_ParseTuple(args,
"s#|O:get",
&key, &key_length, &default_value))
goto exit;
return_value = dbm_dbm_get_impl(dp, key, key_length, default_value);
exit:
return return_value;
}
static PyObject *
dbm_dbm_get_impl(dbmobject *dp, const char *key, Py_ssize_clean_t key_length, int group_right_1, PyObject *default_value)
/*[clinic end generated code: output=31d5180d6b36f1ea input=43a561dc2bd1db3b]*/
dbm_dbm_get_impl(dbmobject *dp, const char *key, Py_ssize_clean_t key_length, PyObject *default_value)
/*[clinic end generated code: output=2bbaf9a187f9b6bf input=aecf5efd2f2b1a3b]*/
{
datum dbm_key, val;
if (!group_right_1)
default_value = Py_None;
dbm_key.dptr = (char *)key;
dbm_key.dsize = key_length;
check_dbmobject_open(dp);
......
......@@ -11,49 +11,35 @@ module _opcode
_opcode.stack_effect -> int
opcode: int
[
oparg: int
]
oparg: object = None
/
Compute the stack effect of the opcode.
[clinic start generated code]*/
PyDoc_STRVAR(_opcode_stack_effect__doc__,
"stack_effect(module, opcode, [oparg])\n"
"sig=($module, opcode, oparg=None)\n"
"Compute the stack effect of the opcode.");
#define _OPCODE_STACK_EFFECT_METHODDEF \
{"stack_effect", (PyCFunction)_opcode_stack_effect, METH_VARARGS, _opcode_stack_effect__doc__},
static int
_opcode_stack_effect_impl(PyModuleDef *module, int opcode, int group_right_1, int oparg);
_opcode_stack_effect_impl(PyModuleDef *module, int opcode, PyObject *oparg);
static PyObject *
_opcode_stack_effect(PyModuleDef *module, PyObject *args)
{
PyObject *return_value = NULL;
int opcode;
int group_right_1 = 0;
int oparg = 0;
PyObject *oparg = Py_None;
int _return_value;
switch (PyTuple_GET_SIZE(args)) {
case 1:
if (!PyArg_ParseTuple(args, "i:stack_effect", &opcode))
goto exit;
break;
case 2:
if (!PyArg_ParseTuple(args, "ii:stack_effect", &opcode, &oparg))
goto exit;
group_right_1 = 1;
break;
default:
PyErr_SetString(PyExc_TypeError, "_opcode.stack_effect requires 1 to 2 arguments");
goto exit;
}
_return_value = _opcode_stack_effect_impl(module, opcode, group_right_1, oparg);
if (!PyArg_ParseTuple(args,
"i|O:stack_effect",
&opcode, &oparg))
goto exit;
_return_value = _opcode_stack_effect_impl(module, opcode, oparg);
if ((_return_value == -1) && PyErr_Occurred())
goto exit;
return_value = PyLong_FromLong((long)_return_value);
......@@ -63,23 +49,31 @@ exit:
}
static int
_opcode_stack_effect_impl(PyModuleDef *module, int opcode, int group_right_1, int oparg)
/*[clinic end generated code: output=4689140ffda2494a input=056816407c3d4284]*/
_opcode_stack_effect_impl(PyModuleDef *module, int opcode, PyObject *oparg)
/*[clinic end generated code: output=4fe636f5db87c0a9 input=2d0a9ee53c0418f5]*/
{
int effect;
int oparg_int = 0;
if (HAS_ARG(opcode)) {
if (!group_right_1) {
PyObject *i_object;
if (oparg == Py_None) {
PyErr_SetString(PyExc_ValueError,
"stack_effect: opcode requires oparg but oparg was not specified");
return -1;
}
i_object = PyNumber_Index(oparg);
if (!i_object)
return -1;
oparg_int = (int)PyLong_AsLong(oparg);
if ((oparg_int == -1) && PyErr_Occurred())
return -1;
}
else if (group_right_1) {
else if (oparg != Py_None) {
PyErr_SetString(PyExc_ValueError,
"stack_effect: opcode does not permit oparg but oparg was specified");
return -1;
}
effect = PyCompile_OpcodeStackEffect(opcode, oparg);
effect = PyCompile_OpcodeStackEffect(opcode, oparg_int);
if (effect == PY_INVALID_STACK_EFFECT) {
PyErr_SetString(PyExc_ValueError,
"invalid opcode or oparg");
......
......@@ -276,6 +276,8 @@ exit:
return return_value;
}
#if defined(HAVE_ZLIB_COPY)
PyDoc_STRVAR(zlib_Compress_copy__doc__,
"sig=($self)\n"
"Return a copy of the compression object.");
......@@ -292,6 +294,14 @@ zlib_Compress_copy(compobject *self, PyObject *Py_UNUSED(ignored))
return zlib_Compress_copy_impl(self);
}
#endif /* defined(HAVE_ZLIB_COPY) */
#ifndef ZLIB_COMPRESS_COPY_METHODDEF
#define ZLIB_COMPRESS_COPY_METHODDEF
#endif /* !defined(ZLIB_COMPRESS_COPY_METHODDEF) */
#if defined(HAVE_ZLIB_COPY)
PyDoc_STRVAR(zlib_Decompress_copy__doc__,
"sig=($self)\n"
"Return a copy of the decompression object.");
......@@ -308,6 +318,12 @@ zlib_Decompress_copy(compobject *self, PyObject *Py_UNUSED(ignored))
return zlib_Decompress_copy_impl(self);
}
#endif /* defined(HAVE_ZLIB_COPY) */
#ifndef ZLIB_DECOMPRESS_COPY_METHODDEF
#define ZLIB_DECOMPRESS_COPY_METHODDEF
#endif /* !defined(ZLIB_DECOMPRESS_COPY_METHODDEF) */
PyDoc_STRVAR(zlib_Decompress_flush__doc__,
"sig=($self, length=DEF_BUF_SIZE)\n"
"Return a bytes object containing any remaining decompressed data.\n"
......@@ -408,4 +424,4 @@ exit:
return return_value;
}
/*[clinic end generated code: output=ad23316b49faf7e6 input=a9049054013a1b77]*/
/*[clinic end generated code: output=21556008559f839c input=a9049054013a1b77]*/
......@@ -2366,21 +2366,26 @@ class path_t_converter(CConverter):
converter = 'path_converter'
def converter_init(self, *, allow_fd=False, nullable=False):
def strify(value):
return str(int(bool(value)))
# right now path_t doesn't support default values.
# to support a default value, you'll need to override initialize().
if self.default is not unspecified:
fail("Can't specify a default to the path_t converter!")
assert self.default is unspecified
if self.c_default is not None:
fail("Can't specify a c_default to the path_t converter!")
self.nullable = nullable
self.allow_fd = allow_fd
def pre_render(self):
def strify(value):
return str(int(bool(value)))
# add self.py_name here when merging with posixmodule conversion
self.c_default = 'PATH_T_INITIALIZE("{}", {}, {})'.format(
self.function.name,
strify(nullable),
strify(allow_fd),
strify(self.nullable),
strify(self.allow_fd),
)
def cleanup(self):
......@@ -2397,7 +2402,7 @@ class dir_fd_converter(CConverter):
[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=d702d58a8469cc7d]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=5c9f456f53244fc3]*/
/*[clinic input]
......@@ -11122,6 +11127,15 @@ posix_set_handle_inheritable(PyObject *self, PyObject *args)
#endif /* MS_WINDOWS */
/*[clinic input]
dump buffer
[clinic start generated code]*/
#ifndef OS_TTYNAME_METHODDEF
#define OS_TTYNAME_METHODDEF
#endif /* !defined(OS_TTYNAME_METHODDEF) */
/*[clinic end generated code: output=5d071bbc8f49ea12 input=524ce2e021e4eba6]*/
static PyMethodDef posix_methods[] = {
......
......@@ -2215,6 +2215,15 @@ _imp_load_dynamic_impl(PyModuleDef *module, PyObject *name, PyObject *path, PyOb
#endif /* HAVE_DYNAMIC_LOADING */
/*[clinic input]
dump buffer
[clinic start generated code]*/
#ifndef _IMP_LOAD_DYNAMIC_METHODDEF
#define _IMP_LOAD_DYNAMIC_METHODDEF
#endif /* !defined(_IMP_LOAD_DYNAMIC_METHODDEF) */
/*[clinic end generated code: output=d07c1d4a343a9579 input=524ce2e021e4eba6]*/
PyDoc_STRVAR(doc_imp,
"(Extremely) low-level import machinery bits as used by importlib and imp.");
......@@ -2230,9 +2239,7 @@ static PyMethodDef imp_methods[] = {
_IMP_INIT_FROZEN_METHODDEF
_IMP_IS_BUILTIN_METHODDEF
_IMP_IS_FROZEN_METHODDEF
#ifdef HAVE_DYNAMIC_LOADING
_IMP_LOAD_DYNAMIC_METHODDEF
#endif
_IMP__FIX_CO_FILENAME_METHODDEF
{NULL, NULL} /* sentinel */
};
......@@ -2324,3 +2331,4 @@ PyImport_AppendInittab(const char *name, PyObject* (*initfunc)(void))
#ifdef __cplusplus
}
#endif
This diff is collapsed.
......@@ -13,6 +13,7 @@ import sys
import unittest
from unittest import TestCase
class FakeConverter:
def __init__(self, name, args):
self.name = name
......@@ -41,10 +42,11 @@ class FakeClinic:
def __init__(self):
self.converters = FakeConvertersDict()
self.legacy_converters = FakeConvertersDict()
self.language = clinic.CLanguage()
self.language = clinic.CLanguage(None)
self.filename = None
self.block_parser = clinic.BlockParser('', self.language)
self.modules = collections.OrderedDict()
self.classes = collections.OrderedDict()
clinic.clinic = self
self.name = "FakeClinic"
self.line_prefix = self.line_suffix = ''
......@@ -92,7 +94,7 @@ class ClinicWholeFileTest(TestCase):
# so it woudl spit out an end line for you.
# and since you really already had one,
# the last line of the block got corrupted.
c = clinic.Clinic(clinic.CLanguage())
c = clinic.Clinic(clinic.CLanguage(None))
raw = "/*[clinic]\nfoo\n[clinic]*/"
cooked = c.parse(raw).splitlines()
end_line = cooked[2].rstrip()
......@@ -220,7 +222,7 @@ class CopyParser:
class ClinicBlockParserTest(TestCase):
def _test(self, input, output):
language = clinic.CLanguage()
language = clinic.CLanguage(None)
blocks = list(clinic.BlockParser(input, language))
writer = clinic.BlockPrinter(language)
......@@ -250,7 +252,7 @@ xyz
""")
def _test_clinic(self, input, output):
language = clinic.CLanguage()
language = clinic.CLanguage(None)
c = clinic.Clinic(language)
c.parsers['inert'] = InertParser(c)
c.parsers['copy'] = CopyParser(c)
......@@ -265,7 +267,7 @@ xyz
def
[copy start generated code]*/
abc
/*[copy end generated code: checksum=03cfd743661f07975fa2f1220c5194cbaff48451]*/
/*[copy end generated code: output=03cfd743661f0797 input=7b18d017f89f61cf]*/
xyz
""", """
verbatim text here
......@@ -274,7 +276,7 @@ xyz
def
[copy start generated code]*/
def
/*[copy end generated code: checksum=7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/
/*[copy end generated code: output=7b18d017f89f61cf input=7b18d017f89f61cf]*/
xyz
""")
......@@ -297,7 +299,7 @@ class ClinicParserTest(TestCase):
def test_param(self):
function = self.parse_function("module os\nos.access\n path: int")
self.assertEqual("access", function.name)
self.assertEqual(1, len(function.parameters))
self.assertEqual(2, len(function.parameters))
p = function.parameters['path']
self.assertEqual('path', p.name)
self.assertIsInstance(p.converter, clinic.int_converter)
......@@ -326,11 +328,22 @@ class ClinicParserTest(TestCase):
module os
os.access
follow_symlinks: bool = True
something_else: str""")
something_else: str = ''""")
p = function.parameters['follow_symlinks']
self.assertEqual(2, len(function.parameters))
self.assertEqual(3, len(function.parameters))
self.assertIsInstance(function.parameters['something_else'].converter, clinic.str_converter)
def test_param_default_parameters_out_of_order(self):
s = self.parse_function_should_fail("""
module os
os.access
follow_symlinks: bool = True
something_else: str""")
self.assertEqual(s, """Error on line 0:
Can't have a parameter without a default ('something_else')
after a parameter with a default!
""")
def disabled_test_converter_arguments(self):
function = self.parse_function("module os\nos.access\n path: path_t(allow_fd=1)")
p = function.parameters['path']
......@@ -346,7 +359,7 @@ os.stat as os_stat_fn
Perform a stat system call on the given path.""")
self.assertEqual("""
stat(path)
sig=($module, path)
Perform a stat system call on the given path.
path
......@@ -366,7 +379,7 @@ This is the documentation for foo.
Okay, we're done here.
""")
self.assertEqual("""
bar(x, y)
sig=($module, x, y)
This is the documentation for foo.
x
......@@ -382,7 +395,7 @@ os.stat
path: str
This/used to break Clinic!
""")
self.assertEqual("os.stat(path)\n\nThis/used to break Clinic!", function.docstring)
self.assertEqual("sig=($module, path)\n\nThis/used to break Clinic!", function.docstring)
def test_c_name(self):
function = self.parse_function("module os\nos.stat as os_stat_fn")
......@@ -538,7 +551,7 @@ foo.two_top_groups_on_left
""")
self.assertEqual(s,
('Error on line 0:\n'
'Function two_top_groups_on_left has an unsupported group configuration. (Unexpected state 2)\n'))
'Function two_top_groups_on_left has an unsupported group configuration. (Unexpected state 2.b)\n'))
def test_disallowed_grouping__two_top_groups_on_right(self):
self.parse_function_should_fail("""
......@@ -611,8 +624,8 @@ foo.bar
Docstring
""")
self.assertEqual("bar()\nDocstring", function.docstring)
self.assertEqual(0, len(function.parameters))
self.assertEqual("sig=($module)\nDocstring", function.docstring)
self.assertEqual(1, len(function.parameters)) # self!
def test_illegal_module_line(self):
self.parse_function_should_fail("""
......@@ -706,7 +719,7 @@ foo.bar
Not at column 0!
""")
self.assertEqual("""
bar(x, *, y)
sig=($module, x, *, y)
Not at column 0!
x
......@@ -720,7 +733,7 @@ os.stat
path: str
This/used to break Clinic!
""")
self.assertEqual("stat(path)\nThis/used to break Clinic!", function.docstring)
self.assertEqual("sig=($module, path)\nThis/used to break Clinic!", function.docstring)
def test_directive(self):
c = FakeClinic()
......
import re
import sys
def negate(condition):
"""
Returns a CPP conditional that is the opposite of the conditional passed in.
"""
if condition.startswith('!'):
return condition[1:]
return "!" + condition
class Monitor:
"""
A simple C preprocessor that scans C source and computes, line by line,
what the current C preprocessor #if state is.
Doesn't handle everything--for example, if you have /* inside a C string,
without a matching */ (also inside a C string), or with a */ inside a C
string but on another line and with preprocessor macros in between...
the parser will get lost.
Anyway this implementation seems to work well enough for the CPython sources.
"""
is_a_simple_defined = re.compile(r'^defined\s*\(\s*[A-Za-z0-9_]+\s*\)$').match
def __init__(self, filename=None, *, verbose=False):
self.stack = []
self.in_comment = False
self.continuation = None
self.line_number = 0
self.filename = filename
self.verbose = verbose
def __repr__(self):
return ''.join((
'<Monitor ',
str(id(self)),
" line=", str(self.line_number),
" condition=", repr(self.condition()),
">"))
def status(self):
return str(self.line_number).rjust(4) + ": " + self.condition()
def condition(self):
"""
Returns the current preprocessor state, as a single #if condition.
"""
return " && ".join(condition for token, condition in self.stack)
def fail(self, *a):
if self.filename:
filename = " " + self.filename
else:
filename = ''
print("Error at" + filename, "line", self.line_number, ":")
print(" ", ' '.join(str(x) for x in a))
sys.exit(-1)
def close(self):
if self.stack:
self.fail("Ended file while still in a preprocessor conditional block!")
def write(self, s):
for line in s.split("\n"):
self.writeline(line)
def writeline(self, line):
self.line_number += 1
line = line.strip()
def pop_stack():
if not self.stack:
self.fail("#" + token + " without matching #if / #ifdef / #ifndef!")
return self.stack.pop()
if self.continuation:
line = self.continuation + line
self.continuation = None
if not line:
return
if line.endswith('\\'):
self.continuation = line[:-1].rstrip() + " "
return
# we have to ignore preprocessor commands inside comments
#
# we also have to handle this:
# /* start
# ...
# */ /* <-- tricky!
# ...
# */
# and this:
# /* start
# ...
# */ /* also tricky! */
if self.in_comment:
if '*/' in line:
# snip out the comment and continue
#
# GCC allows
# /* comment
# */ #include <stdio.h>
# maybe other compilers too?
_, _, line = line.partition('*/')
self.in_comment = False
while True:
if '/*' in line:
if self.in_comment:
self.fail("Nested block comment!")
before, _, remainder = line.partition('/*')
comment, comment_ends, after = remainder.partition('*/')
if comment_ends:
# snip out the comment
line = before.rstrip() + ' ' + after.lstrip()
continue
# comment continues to eol
self.in_comment = True
line = before.rstrip()
break
# we actually have some // comments
# (but block comments take precedence)
before, line_comment, comment = line.partition('//')
if line_comment:
line = before.rstrip()
if not line.startswith('#'):
return
line = line[1:].lstrip()
assert line
fields = line.split()
token = fields[0].lower()
condition = ' '.join(fields[1:]).strip()
if_tokens = {'if', 'ifdef', 'ifndef'}
all_tokens = if_tokens | {'elif', 'else', 'endif'}
if token not in all_tokens:
return
# cheat a little here, to reuse the implementation of if
if token == 'elif':
pop_stack()
token = 'if'
if token in if_tokens:
if not condition:
self.fail("Invalid format for #" + token + " line: no argument!")
if token == 'if':
if not self.is_a_simple_defined(condition):
condition = "(" + condition + ")"
else:
fields = condition.split()
if len(fields) != 1:
self.fail("Invalid format for #" + token + " line: should be exactly one argument!")
symbol = fields[0]
condition = 'defined(' + symbol + ')'
if token == 'ifndef':
condition = '!' + condition
self.stack.append(("if", condition))
if self.verbose:
print(self.status())
return
previous_token, previous_condition = pop_stack()
if token == 'else':
self.stack.append(('else', negate(previous_condition)))
elif token == 'endif':
pass
if self.verbose:
print(self.status())
if __name__ == '__main__':
for filename in sys.argv[1:]:
with open(filename, "rt") as f:
cpp = Monitor(filename, verbose=True)
print()
print(filename)
for line_number, line in enumerate(f.read().split('\n'), 1):
cpp.writeline(line)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment