From bebf73511a1250fc768bcb7192b5b3c3fd04d8f2 Mon Sep 17 00:00:00 2001 From: Larry Hastings Date: Fri, 17 Jan 2014 17:47:17 -0800 Subject: Issue #20287: Argument Clinic's output is now configurable, allowing delaying its output or even redirecting it to a separate file. --- Doc/howto/clinic.rst | 320 +++++++++++++++++- Misc/NEWS | 3 + Modules/_pickle.c | 70 ++-- Modules/_weakref.c | 3 +- Modules/zlibmodule.c | 8 +- Python/import.c | 32 +- Tools/clinic/clinic.py | 793 +++++++++++++++++++++++++++++++++++--------- Tools/clinic/clinic_test.py | 29 ++ 8 files changed, 1015 insertions(+), 243 deletions(-) diff --git a/Doc/howto/clinic.rst b/Doc/howto/clinic.rst index 96f84b0..a61508e 100644 --- a/Doc/howto/clinic.rst +++ b/Doc/howto/clinic.rst @@ -23,6 +23,58 @@ Argument Clinic How-To version of Argument Clinic that ships with CPython 3.5 *could* be totally incompatible and break all your code. +============================ +The Goals Of Argument Clinic +============================ + +Argument Clinic's primary goal +is to take over responsibility for all argument parsing code +inside CPython. This means that, when you convert a function +to work with Argument Clinic, that function should no longer +do any of its own argument parsing--the code generated by +Argument Clinic should be a "black box" to you, where CPython +calls in at the top, and your code gets called at the bottom, +with ``PyObject *args`` (and maybe ``PyObject *kwargs``) +magically converted into the C variables and types you need. + +In order for Argument Clinic to accomplish its primary goal, +it must be easy to use. Currently, working with CPython's +argument parsing library is a chore, requiring maintaining +redundant information in a surprising number of places. +When you use Argument Clinic, you don't have to repeat yourself. + +Obviously, no one would want to use Argument Clinic unless +it's solving a their problem without creating problems of +its own. +So Argument Clinic should generate correct code, and its +code should preferably be slower, and definitely should +not introduce a major speed regression. (Eventually Argument +Clinic should enable a major speedup--we should be able +to rewrite its code generator so it produces tailor-made +parsing code, rather than using the general-purpose functions +from the CPython code base, which would make for the fastest +argument parsing possible.) + +Additionally, Argument Clinic must be flexible enough to +work with any approach to argument parsing. Python has +some functions with some very strange parsing behaviors; +Argument Clinic's goal is to support all of them. + +Finally, the original motivation for Argument Clinic was +to provide introspection "signatures" for CPython builtins. +It used to be, the introspection query functions would throw +an exception if you passed in a builtin. With Argument +Clinic, that's a thing of the past! + +One idea you should keep in mind, as you work with +Argument Clinic: the more information you give it, the +better job it'll be able to do. +Argument Clinic is admittedly relatively simple right +now. But as it evolves it will get more sophisticated, +and it should be able to do many interesting and smart +things with all the information you give it. + + ======================== Basic Concepts And Usage ======================== @@ -84,7 +136,15 @@ Converting Your First Function ============================== The best way to get a sense of how Argument Clinic works is to -convert a function to work with it. Let's dive in! +convert a function to work with it. Here, then, are the bare +minimum steps you'd need to follow to convert a function to +work with Argument Clinic. Note that for code you plan to +check in to CPython, you really should take the conversion farther, +using some of the advanced concepts you'll see later on in +the document (like "return converters" and "self converters"). +But we'll keep it simple for this walkthrough so you can learn. + +Let's dive in! 0. Make sure you're working with a freshly updated checkout of the CPython trunk. @@ -1282,6 +1342,264 @@ 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 +---------------------------------------- + +It can be inconvenient to have Clinic's output interspersed with +your conventional hand-edited C code. Luckily, Clinic is configurable: +you can buffer up its output for printing later (or earlier!), or write +its output to a separate file. You can also add a prefix or suffix to +every line of Clinic's generated output. + +While changing Clinic's output in this manner can be a boon to readability, +it may result in Clinic code using types before they are defined, or +your code attempting to use Clinic-generated code befire it is defined. +These problems can be easily solved by rearranging the declarations in your file, +or moving where Clinic's generated code goes. (This is why the default behavior +of Clinic is to output everything into the current block; while many people +consider this hampers readability, it will never require rearranging your +code to fix definition-before-use problems.) + +Let's start with defining some terminology: + +*field* + A field, in this context, is a subsection of Clinic's output. + For example, the ``#define`` for the ``PyMethodDef`` structure + is a field, called ``methoddef_define``. Clinic has seven + different fields it can output per function definition:: + + docstring_prototype + docstring_definition + methoddef_define + impl_prototype + parser_prototype + parser_definition + impl_definition + + All the names are of the form ``"_"``, + where ``""`` is the semantic object represented (the parsing function, + the impl function, the docstring, or the methoddef structure) and ``""`` + represents what kind of statement the field is. Field names that end in + ``"_prototype"`` + represent forward declarations of that thing, without the actual body/data + of the thing; field names that end in ``"_definition"`` represent the actual + definition of the thing, with the body/data of the thing. (``"methoddef"`` + is special, it's the only one that ends with ``"_define"``, representing that + it's a preprocessor #define.) + +*destination* + A destination is a place Clinic can write output to. There are + five built-in destinations: + + ``block`` + The default destination: printed in the output section of + the current Clinic block. + + ``buffer`` + A text buffer where you can save text for later. Text sent + here is appended to the end of any exsiting text. It's an + error to have any text left in the buffer when Clinic finishes + processing a file. + + ``file`` + A separate "clinic file" that will be created automatically by Clinic. + The filename chosen for the file is ``{basename}.clinic{extension}``, + where ``basename`` and ``extension`` were assigned the output + from ``os.path.splitext()`` run on the current file. (Example: + the ``file`` destination for ``_pickle.c`` would be written to + ``_pickle.clinic.c``.) + + **Important: When using a** ``file`` **destination, you** + *must check in* **the generated file!** + + ``two-pass`` + A buffer like ``buffer``. However, a two-pass buffer can only + be written once, and it prints out all text sent to it during + all of processing, even from Clinic blocks *after* the + + ``suppress`` + The text is suppressed--thrown away. + + +Clinic defines five new directives that let you reconfigure its output. + +The first new directive is ``dump``:: + + dump + +This dumps the current contents of the named destination into the output of +the current block, and empties it. This only works with ``buffer`` and +``two-pass`` destinations. + +The second new directive is ``output``. The most basic form of ``output`` +is like this:: + + output + +This tells Clinic to output *field* to *destination*. ``output`` also +supports a special meta-destination, called ``everything``, which tells +Clinic to output *all* fields to that *destination*. + +``output`` has a number of other functions:: + + output push + output pop + output preset + + +``output push`` and ``output pop`` allow you to push and pop +configurations on an internal configuration stack, so that you +can temporarily modify the output configuration, then easily restore +the previous configuration. Simply push before your change to save +the current configuration, then pop when you wish to restore the +previous configuration. + +``output preset`` sets Clinic's output to one of several built-in +preset configurations, as follows: + + ``original`` + Clinic's starting configuration. + + Suppress the ``parser_prototype`` + and ``docstring_prototype``, write everything else to ``block``. + + ``file`` + Designed to write everything to the "clinic file" that it can. + You then ``#include`` this file near the top of your file. + You may need to rearrange your file to make this work, though + usually this just means creating forward declarations for various + ``typedef`` and ``PyTypeObject`` definitions. + + Suppress the ``parser_prototype`` + and ``docstring_prototype``, write the ``impl_definition`` to + ``block``, and write everything else to ``file``. + + ``buffer`` + Save up all most of the output from Clinic, to be written into + your file near the end. For Python files implementing modules + or builtin types, it's recommended that you dump the buffer + just above the static structures for your module or + builtin type; these are normally very near the end. Using + ``buffer`` may require even more editing than ``file``, if + your file has static ``PyMethodDef`` arrays defined in the + middle of the file. + + Suppress the ``parser_prototype``, ``impl_prototype``, + and ``docstring_prototype``, write the ``impl_definition`` to + ``block``, and write everything else to ``file``. + + ``two-pass`` + Similar to the ``buffer`` preset, but writes forward declarations to + the ``two-pass`` buffer, and definitions to the ``buffer``. + This is similar to the ``buffer`` preset, but may require + less editing than ``buffer``. Dump the ``two-pass`` buffer + near the top of your file, and dump the ``buffer`` near + the end just like you would when using the ``buffer`` preset. + + Suppresses the ``impl_prototype``, write the ``impl_definition`` + to ``block``, write ``docstring_prototype``, ``methoddef_define``, + and ``parser_prototype`` to ``two-pass``, write everything else + to ``buffer``. + + ``partial-buffer`` + Similar to the ``buffer`` preset, but writes more things to ``block``, + only writing the really big chunks of generated code to ``buffer``. + This avoids the definition-before-use problem of ``buffer`` completely, + at the small cost of having slightly more stuff in the block's output. + Dump the ``buffer`` near the end, just like you would when using + the ``buffer`` preset. + + Suppresses the ``impl_prototype``, write the ``docstring_definition`` + and ``parser_defintion`` to ``buffer``, write everything else to ``block``. + +The third new directive is ``destination``:: + + destination [...] + +This performs an operation on the destination named ``name``. + +There are two defined subcommands: ``new`` and ``clear``. + +The ``new`` subcommand works like this:: + + destination new + +This creates a new destination with name ```` and type ````. + +There are five destination types:: + + ``suppress`` + Throws the text away. + + ``block`` + Writes the text to the current block. This is what Clinic + originally did. + + ``buffer`` + A simple text buffer, like the "buffer" builtin destination above. + + ``file`` + A text file. The file destination takes an extra argument, + a template to use for building the filename, like so: + + destination new + + The template can use three strings internally that will be replaced + by bits of the filename: + + {filename} + The full filename. + {basename} + Everything up to but not including the last '.'. + {extension} + The last '.' and everything after it. + + If there are no periods in the filename, {basename} and {filename} + are the same, and {extension} is empty. "{basename}{extension}" + is always exactly the same as "{filename}"." + + ``two-pass`` + A two-pass buffer, like the "two-pass" builtin destination above. + + +The ``clear`` subcommand works like this:: + + destination clear + +It removes all the accumulated text up to this point in the destination. +(I don't know what you'd need this for, but I thought maybe it'd be +useful while someone's experimenting.) + +The fourth new directive is ``set``:: + + set line_prefix "string" + set line_suffix "string" + +``set`` lets you set two internal variables in Clinic. +``line_prefix`` is a string that will be prepended to every line of Clinic's output; +``line_suffix`` is a string that will be appended to every line of Clinic's output. + +Both of these suport two format strings: + + ``{block comment start}`` + Turns into the string ``/*``, the start-comment text sequence for C files. + + ``{block comment end}`` + Turns into the string ``*/``, the end-comment text sequence for C files. + +The final new directive is one you shouldn't need to use directly, +called ``preserve``:: + + preserve + +This tells Clinic that the current contents of the output should be kept, unmodifed. +This is used internally by Clinic when dumping output into ``file`` files; wrapping +it in a Clinic block lets Clinic use its existing checksum functionality to ensure +the file was not modified by hand before it gets overwritten. + + +Using Argument Clinic in Python files ------------------------------------- It's actually possible to use Argument Clinic to preprocess Python files. diff --git a/Misc/NEWS b/Misc/NEWS index df217f2..8eb968a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -88,6 +88,9 @@ Tests Tools/Demos ----------- +- Issue #20287: Argument Clinic's output is now configurable, allowing + delaying its output or even redirecting it to a separate file. + - Issue #20226: Argument Clinic now permits simple expressions (e.g. "sys.maxsize - 1") as default values for parameters. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index e687a1e..13c3ae9 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -3906,16 +3906,12 @@ _pickle_Pickler_clear_memo_impl(PicklerObject *self); static PyObject * _pickle_Pickler_clear_memo(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_Pickler_clear_memo_impl((PicklerObject *)self); - - return return_value; + return _pickle_Pickler_clear_memo_impl((PicklerObject *)self); } static PyObject * _pickle_Pickler_clear_memo_impl(PicklerObject *self) -/*[clinic end generated code: checksum=0574593b102fffb8e781d7bb9b536ceffc525ac1]*/ +/*[clinic end generated code: checksum=015cc3c5befea86cb08b9396938477bebbea4157]*/ { if (self->memo) PyMemoTable_Clear(self->memo); @@ -4191,16 +4187,12 @@ _pickle_PicklerMemoProxy_clear_impl(PicklerMemoProxyObject *self); static PyObject * _pickle_PicklerMemoProxy_clear(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_PicklerMemoProxy_clear_impl((PicklerMemoProxyObject *)self); - - return return_value; + return _pickle_PicklerMemoProxy_clear_impl((PicklerMemoProxyObject *)self); } static PyObject * _pickle_PicklerMemoProxy_clear_impl(PicklerMemoProxyObject *self) -/*[clinic end generated code: checksum=c6ca252530ccb3ea2f4b33507b51b183f23b24c7]*/ +/*[clinic end generated code: checksum=bf8dd8c8688d0c0f7a2e59a804c47375b740f2f0]*/ { if (self->pickler->memo) PyMemoTable_Clear(self->pickler->memo); @@ -4228,16 +4220,12 @@ _pickle_PicklerMemoProxy_copy_impl(PicklerMemoProxyObject *self); static PyObject * _pickle_PicklerMemoProxy_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_PicklerMemoProxy_copy_impl((PicklerMemoProxyObject *)self); - - return return_value; + return _pickle_PicklerMemoProxy_copy_impl((PicklerMemoProxyObject *)self); } static PyObject * _pickle_PicklerMemoProxy_copy_impl(PicklerMemoProxyObject *self) -/*[clinic end generated code: checksum=808c4d5a37359ed5fb2efe81dbe5ff480719f470]*/ +/*[clinic end generated code: checksum=72d46879dc658adbd3d28b5c82dd8dcfa6b9b124]*/ { Py_ssize_t i; PyMemoTable *memo; @@ -4295,16 +4283,12 @@ _pickle_PicklerMemoProxy___reduce___impl(PicklerMemoProxyObject *self); static PyObject * _pickle_PicklerMemoProxy___reduce__(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_PicklerMemoProxy___reduce___impl((PicklerMemoProxyObject *)self); - - return return_value; + return _pickle_PicklerMemoProxy___reduce___impl((PicklerMemoProxyObject *)self); } static PyObject * _pickle_PicklerMemoProxy___reduce___impl(PicklerMemoProxyObject *self) -/*[clinic end generated code: checksum=2293152bdf53951a012d430767b608f5fb4213b5]*/ +/*[clinic end generated code: checksum=aad71c4d81d1ed8bf0d32362dd80a29b9f3b0d03]*/ { PyObject *reduce_value, *dict_args; PyObject *contents = _pickle_PicklerMemoProxy_copy_impl(self); @@ -6342,16 +6326,12 @@ _pickle_Unpickler_load_impl(PyObject *self); static PyObject * _pickle_Unpickler_load(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_Unpickler_load_impl(self); - - return return_value; + return _pickle_Unpickler_load_impl(self); } static PyObject * _pickle_Unpickler_load_impl(PyObject *self) -/*[clinic end generated code: checksum=55f35fcaf034817e75c355ec50b7878577355899]*/ +/*[clinic end generated code: checksum=9477099fe6a90748c13ff1a6dd92ba7ab7a89602]*/ { UnpicklerObject *unpickler = (UnpicklerObject*)self; @@ -6417,8 +6397,8 @@ _pickle_Unpickler_find_class(PyObject *self, PyObject *args) PyObject *module_name; PyObject *global_name; - if (!PyArg_ParseTuple(args, - "OO:find_class", + if (!PyArg_UnpackTuple(args, "find_class", + 2, 2, &module_name, &global_name)) goto exit; return_value = _pickle_Unpickler_find_class_impl((UnpicklerObject *)self, module_name, global_name); @@ -6429,7 +6409,7 @@ exit: static PyObject * _pickle_Unpickler_find_class_impl(UnpicklerObject *self, PyObject *module_name, PyObject *global_name) -/*[clinic end generated code: checksum=1f353d13a32c9d94feb1466b3c2d0529a7e5650e]*/ +/*[clinic end generated code: checksum=15ed4836fd5860425fff9ea7855d4f1f4413c170]*/ { PyObject *global; PyObject *modules_dict; @@ -6752,16 +6732,12 @@ _pickle_UnpicklerMemoProxy_clear_impl(UnpicklerMemoProxyObject *self); static PyObject * _pickle_UnpicklerMemoProxy_clear(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_UnpicklerMemoProxy_clear_impl((UnpicklerMemoProxyObject *)self); - - return return_value; + return _pickle_UnpicklerMemoProxy_clear_impl((UnpicklerMemoProxyObject *)self); } static PyObject * _pickle_UnpicklerMemoProxy_clear_impl(UnpicklerMemoProxyObject *self) -/*[clinic end generated code: checksum=e0f99c26d48444a3f58f598bec3190c66595fce7]*/ +/*[clinic end generated code: checksum=07adecee2181e5e268b2ff184360b1d88ad947f2]*/ { _Unpickler_MemoCleanup(self->unpickler); self->unpickler->memo = _Unpickler_NewMemo(self->unpickler->memo_size); @@ -6791,16 +6767,12 @@ _pickle_UnpicklerMemoProxy_copy_impl(UnpicklerMemoProxyObject *self); static PyObject * _pickle_UnpicklerMemoProxy_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_UnpicklerMemoProxy_copy_impl((UnpicklerMemoProxyObject *)self); - - return return_value; + return _pickle_UnpicklerMemoProxy_copy_impl((UnpicklerMemoProxyObject *)self); } static PyObject * _pickle_UnpicklerMemoProxy_copy_impl(UnpicklerMemoProxyObject *self) -/*[clinic end generated code: checksum=8c0ab91c0b694ea71a1774650898a760d1ab4765]*/ +/*[clinic end generated code: checksum=47b9f0cc12c5a54004252e1b4916822cdfa8a881]*/ { Py_ssize_t i; PyObject *new_memo = PyDict_New(); @@ -6851,16 +6823,12 @@ _pickle_UnpicklerMemoProxy___reduce___impl(UnpicklerMemoProxyObject *self); static PyObject * _pickle_UnpicklerMemoProxy___reduce__(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _pickle_UnpicklerMemoProxy___reduce___impl((UnpicklerMemoProxyObject *)self); - - return return_value; + return _pickle_UnpicklerMemoProxy___reduce___impl((UnpicklerMemoProxyObject *)self); } static PyObject * _pickle_UnpicklerMemoProxy___reduce___impl(UnpicklerMemoProxyObject *self) -/*[clinic end generated code: checksum=4ee76a65511291f0de2e9e63db395d2e5d6d8df6]*/ +/*[clinic end generated code: checksum=2f061bb9ecd9ee8500184c135148a131c46a3b88]*/ { PyObject *reduce_value; PyObject *constructor_args; diff --git a/Modules/_weakref.c b/Modules/_weakref.c index a73dcdb..e5d6519 100644 --- a/Modules/_weakref.c +++ b/Modules/_weakref.c @@ -34,6 +34,7 @@ _weakref_getweakrefcount(PyModuleDef *module, PyObject *object) { PyObject *return_value = NULL; Py_ssize_t _return_value; + _return_value = _weakref_getweakrefcount_impl(module, object); if ((_return_value == -1) && PyErr_Occurred()) goto exit; @@ -45,7 +46,7 @@ exit: static Py_ssize_t _weakref_getweakrefcount_impl(PyModuleDef *module, PyObject *object) -/*[clinic end generated code: checksum=436e8fbe0297434375f039d8c2d9fc3a9bbe773c]*/ +/*[clinic end generated code: checksum=744fa73ba68c0ee89567e9cb9bea11863270d516]*/ { PyWeakReference **list; diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index 59fc620..efa95e9 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -1047,16 +1047,12 @@ zlib_Compress_copy_impl(compobject *self); static PyObject * zlib_Compress_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = zlib_Compress_copy_impl((compobject *)self); - - return return_value; + return zlib_Compress_copy_impl((compobject *)self); } static PyObject * zlib_Compress_copy_impl(compobject *self) -/*[clinic end generated code: checksum=2f454ee15be3bc53cfb4e845c3f891f68be4c8e4]*/ +/*[clinic end generated code: checksum=d57a7911deb7940e85a8d7e65af20b6e2df69000]*/ { compobject *retval = NULL; int err; diff --git a/Python/import.c b/Python/import.c index be0995d..fb7d88c 100644 --- a/Python/import.c +++ b/Python/import.c @@ -246,16 +246,12 @@ _imp_lock_held_impl(PyModuleDef *module); static PyObject * _imp_lock_held(PyModuleDef *module, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _imp_lock_held_impl(module); - - return return_value; + return _imp_lock_held_impl(module); } static PyObject * _imp_lock_held_impl(PyModuleDef *module) -/*[clinic end generated code: checksum=c5858b257881f94dee95526229a8d1a57ccff158]*/ +/*[clinic end generated code: checksum=ede1cafb78eb22e3009602f684c8b780e2b82d62]*/ { #ifdef WITH_THREAD return PyBool_FromLong(import_lock_thread != -1); @@ -289,16 +285,12 @@ _imp_acquire_lock_impl(PyModuleDef *module); static PyObject * _imp_acquire_lock(PyModuleDef *module, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _imp_acquire_lock_impl(module); - - return return_value; + return _imp_acquire_lock_impl(module); } static PyObject * _imp_acquire_lock_impl(PyModuleDef *module) -/*[clinic end generated code: checksum=badb56ed0079a6b902c9616fe068d572765b1863]*/ +/*[clinic end generated code: checksum=5b520b2416c5954a7cf0ed30955d68abe20b5868]*/ { #ifdef WITH_THREAD _PyImport_AcquireLock(); @@ -330,16 +322,12 @@ _imp_release_lock_impl(PyModuleDef *module); static PyObject * _imp_release_lock(PyModuleDef *module, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _imp_release_lock_impl(module); - - return return_value; + return _imp_release_lock_impl(module); } static PyObject * _imp_release_lock_impl(PyModuleDef *module) -/*[clinic end generated code: checksum=f1c2a75e3136a113184e0af2a676d5f0b5b685b4]*/ +/*[clinic end generated code: checksum=efcd9d2923294c04371596e7f6d66a706d43fcac]*/ { #ifdef WITH_THREAD if (_PyImport_ReleaseLock() < 0) { @@ -1847,16 +1835,12 @@ _imp_extension_suffixes_impl(PyModuleDef *module); static PyObject * _imp_extension_suffixes(PyModuleDef *module, PyObject *Py_UNUSED(ignored)) { - PyObject *return_value = NULL; - - return_value = _imp_extension_suffixes_impl(module); - - return return_value; + return _imp_extension_suffixes_impl(module); } static PyObject * _imp_extension_suffixes_impl(PyModuleDef *module) -/*[clinic end generated code: checksum=835921e67fd698e22e101eea64839d1ad62b6451]*/ +/*[clinic end generated code: checksum=82fb35d8429a429a4dc80c84b45b1aad73ff1de7]*/ { PyObject *list; const char *suffix; diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py index cdbe70a..c26d268 100755 --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -16,12 +16,14 @@ import inspect import io import itertools import os +import pprint import re import shlex import sys import tempfile import textwrap import traceback +import uuid # TODO: # @@ -86,10 +88,13 @@ def text_accumulator(): return append, output -def fail(*args, filename=None, line_number=None): +def warn_or_fail(fail=False, *args, filename=None, line_number=None): joined = " ".join([str(a) for a in args]) add, output = text_accumulator() - add("Error") + if fail: + add("Error") + else: + add("Warning") if clinic: if filename is None: filename = clinic.filename @@ -102,9 +107,16 @@ def fail(*args, filename=None, line_number=None): add(':\n') add(joined) print(output()) - sys.exit(-1) + if fail: + sys.exit(-1) +def warn(*args, filename=None, line_number=None): + return warn_or_fail(False, *args, filename=filename, line_number=line_number) + +def fail(*args, filename=None, line_number=None): + return warn_or_fail(True, *args, filename=filename, line_number=line_number) + def quoted_for_c_string(s): for old, new in ( @@ -119,16 +131,14 @@ is_legal_c_identifier = re.compile('^[A-Za-z_][A-Za-z0-9_]*$').match def is_legal_py_identifier(s): return all(is_legal_c_identifier(field) for field in s.split('.')) -# though it's called c_keywords, really it's a list of parameter names -# that are okay in Python but aren't a good idea in C. so if they're used -# Argument Clinic will add "_value" to the end of the name in C. -# (We added "args", "type", "module", "self", "cls", and "null" -# just to be safe, even though they're not C keywords.) +# identifiers that are okay in Python but aren't a good idea in C. +# so if they're used Argument Clinic will add "_value" to the end +# of the name in C. c_keywords = set(""" -args asm auto break case char cls const continue default do double +asm auto break case char cls const continue default do double else enum extern float for goto if inline int long module null register return self short signed sizeof static struct switch -type typedef typeof union unsigned void volatile while +typedef typeof union unsigned void volatile while """.strip().split()) def ensure_legal_c_identifier(s): @@ -190,6 +200,47 @@ def linear_format(s, **kwargs): return output()[:-1] +def indent_all_lines(s, prefix): + """ + Returns 's', with 'prefix' prepended to all lines. + + If the last line is empty, prefix is not prepended + to it. (If s is blank, returns s unchanged.) + + (textwrap.indent only adds to non-blank lines.) + """ + split = s.split('\n') + last = split.pop() + final = [] + for line in split: + final.append(prefix) + final.append(line) + final.append('\n') + if last: + final.append(prefix) + final.append(last) + return ''.join(final) + +def suffix_all_lines(s, suffix): + """ + Returns 's', with 'suffix' appended to all lines. + + If the last line is empty, suffix is not appended + to it. (If s is blank, returns s unchanged.) + """ + split = s.split('\n') + last = split.pop() + final = [] + for line in split: + final.append(line) + final.append(suffix) + final.append('\n') + if last: + final.append(last) + final.append(suffix) + return ''.join(final) + + def version_splitter(s): """Splits a version string into a tuple of integers. @@ -281,7 +332,7 @@ class Language(metaclass=abc.ABCMeta): checksum_line = "" @abc.abstractmethod - def render(self, block): + def render(self, clinic, signatures): pass def validate(self): @@ -378,14 +429,14 @@ class CLanguage(Language): stop_line = "[{dsl_name} start generated code]*/" checksum_line = "/*[{dsl_name} end generated code: checksum={checksum}]*/" - def render(self, signatures): + def render(self, clinic, signatures): function = None for o in signatures: if isinstance(o, Function): if function: fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o)) function = o - return self.render_function(function) + return self.render_function(clinic, function) def docstring_for_c_string(self, f): text, add, output = _text_accumulator() @@ -399,32 +450,91 @@ class CLanguage(Language): add('"') return ''.join(text) - impl_prototype_template = "{c_basename}_impl({impl_parameters})" - @staticmethod - def template_base(*args): - # HACK suppress methoddef define for METHOD_NEW and METHOD_INIT - base = """ + def output_templates(self, f): + parameters = list(f.parameters.values()) + converters = [p.converter for p in parameters] + + has_option_groups = parameters and (parameters[0].group or parameters[-1].group) + default_return_converter = (not f.return_converter or + f.return_converter.type == 'PyObject *') + + positional = parameters and (parameters[-1].kind == inspect.Parameter.POSITIONAL_ONLY) + all_boring_objects = False # yes, this will be false if there are 0 parameters, it's fine + first_optional = len(parameters) + for i, p in enumerate(parameters): + c = p.converter + if type(c) != object_converter: + break + if c.format_unit != 'O': + break + if p.default is not unspecified: + first_optional = min(first_optional, i) + else: + all_boring_objects = True + + meth_o = (len(parameters) == 1 and + parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and + not converters[0].is_optional() and + isinstance(converters[0], object_converter) and + converters[0].format_unit == 'O') + + + # we have to set seven things before we're done: + # + # docstring_prototype + # docstring_definition + # impl_prototype + # methoddef_define + # parser_prototype + # parser_definition + # impl_definition + # + # since impl_prototype is always just impl_definition + ';' + # we just define impl_definition at the top + + docstring_prototype = "PyDoc_VAR({c_basename}__doc__);" + + docstring_definition = """ PyDoc_STRVAR({c_basename}__doc__, {docstring}); -""" +""".strip() - if args[-1] == None: - return base + impl_definition = """ +static {impl_return_type} +{c_basename}_impl({impl_parameters})""".strip() - flags = '|'.join(f for f in args if f) - return base + """ -#define {methoddef_name} \\ - {{"{name}", (PyCFunction){c_basename}, {methoddef_flags}, {c_basename}__doc__}}, -""".replace('{methoddef_flags}', flags) + impl_prototype = parser_prototype = parser_definition = None - def meth_noargs_template(self, methoddef_flags=""): - return self.template_base("METH_NOARGS", methoddef_flags) + """ -static {impl_return_type} -{impl_prototype}; + def meth_varargs(): + nonlocal flags + nonlocal parser_prototype + flags = "METH_VARARGS" + + parser_prototype = """ +static PyObject * +{c_basename}({self_type}{self_name}, PyObject *args) +""".strip() + + if not parameters: + # no parameters, METH_NOARGS + + flags = "METH_NOARGS" + + parser_prototype = """ static PyObject * {c_basename}({self_type}{self_name}, PyObject *Py_UNUSED(ignored)) +""".strip() + + if default_return_converter: + parser_definition = parser_prototype + """ +{{ + return {c_basename}_impl({impl_arguments}); +}} +""".rstrip() + else: + parser_definition = parser_prototype + """ {{ PyObject *return_value = NULL; {declarations} @@ -437,28 +547,43 @@ static PyObject * {cleanup} return return_value; }} +""".rstrip() -static {impl_return_type} -{impl_prototype} -""" + elif meth_o: + if default_return_converter: + # maps perfectly to METH_O, doesn't need a return converter, + # so we skip the parse function and call + # directly into the impl function + + # SLIGHT HACK + # METH_O uses {impl_parameters} for the parser. + + flags = "METH_O" - def meth_o_template(self, methoddef_flags=""): - return self.template_base("METH_O", methoddef_flags) + """ + impl_definition = """ static PyObject * {c_basename}({impl_parameters}) -""" +""".strip() - def meth_o_return_converter_template(self, methoddef_flags=""): - return self.template_base("METH_O", methoddef_flags) + """ -static {impl_return_type} -{impl_prototype}; + impl_prototype = parser_prototype = parser_definition = '' + else: + # SLIGHT HACK + # METH_O uses {impl_parameters} for the parser. + + flags = "METH_O" + + parser_prototype = """ static PyObject * {c_basename}({impl_parameters}) +""".strip() + + parser_definition = parser_prototype + """ {{ PyObject *return_value = NULL; {declarations} {initializers} + _return_value = {c_basename}_impl({impl_arguments}); {return_conversion} @@ -466,18 +591,16 @@ static PyObject * {cleanup} return return_value; }} +""".rstrip() -static {impl_return_type} -{impl_prototype} -""" + elif has_option_groups: + # positional parameters with option groups + # (we have to generate lots of PyArg_ParseTuple calls + # in a big switch statement) - def option_group_template(self, methoddef_flags=""): - return self.template_base("METH_VARARGS", methoddef_flags) + """ -static {impl_return_type} -{impl_prototype}; + meth_varargs() -static PyObject * -{c_basename}({self_type}{self_name}, PyObject *args) + parser_definition = parser_prototype + """ {{ PyObject *return_value = NULL; {declarations} @@ -491,67 +614,138 @@ static PyObject * {cleanup} return return_value; }} +""".rstrip() + + elif positional and all_boring_objects: + # positional-only, but no option groups, + # and nothing but normal objects: + # PyArg_UnpackTuple! + + meth_varargs() + + # substitute in the min and max by hand right here + assert parameters + min_o = first_optional + max_o = len(parameters) + if isinstance(parameters[0].converter, self_converter): + min_o -= 1 + max_o -= 1 + min_o = str(min_o) + max_o = str(max_o) + + parser_definition = parser_prototype + """ +{{ + PyObject *return_value = NULL; + {declarations} + {initializers} -static {impl_return_type} -{impl_prototype} -""" + if (!PyArg_UnpackTuple(args, "{name}", + {min}, {max}, + {parse_arguments})) + goto exit; + {return_value} = {c_basename}_impl({impl_arguments}); + {return_conversion} - def keywords_template(self, methoddef_flags=""): - return self.template_base("METH_VARARGS|METH_KEYWORDS", methoddef_flags) + """ -static {impl_return_type} -{impl_prototype}; +exit: + {cleanup} + return return_value; +}} +""".rstrip().replace('{min}', min_o).replace('{max}', max_o) -static PyObject * -{c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) + elif positional: + # positional-only, but no option groups + # we only need one call to PyArg_ParseTuple + + meth_varargs() + + parser_definition = parser_prototype + """ {{ PyObject *return_value = NULL; - static char *_keywords[] = {{{keywords}, NULL}}; {declarations} {initializers} - if (!PyArg_ParseTupleAndKeywords(args, kwargs, - "{format_units}:{name}", _keywords, + if (!PyArg_ParseTuple(args, + "{format_units}:{name}", {parse_arguments})) goto exit; {return_value} = {c_basename}_impl({impl_arguments}); {return_conversion} -{exit_label} +exit: {cleanup} return return_value; }} +""".rstrip() -static {impl_return_type} -{impl_prototype} -""" - - def positional_only_template(self, methoddef_flags=""): - return self.template_base("METH_VARARGS", methoddef_flags) + """ -static {impl_return_type} -{impl_prototype}; + else: + # positional-or-keyword arguments + flags = "METH_VARARGS|METH_KEYWORDS" + parser_prototype = """ static PyObject * -{c_basename}({self_type}{self_name}, PyObject *args) +{c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) +""".strip() + + parser_definition = parser_prototype + """ {{ PyObject *return_value = NULL; + static char *_keywords[] = {{{keywords}, NULL}}; {declarations} {initializers} - if (!PyArg_ParseTuple(args, - "{format_units}:{name}", + if (!PyArg_ParseTupleAndKeywords(args, kwargs, + "{format_units}:{name}", _keywords, {parse_arguments})) goto exit; {return_value} = {c_basename}_impl({impl_arguments}); {return_conversion} -{exit_label} +exit: {cleanup} return return_value; }} +""".rstrip() -static {impl_return_type} -{impl_prototype} -""" + if f.methoddef_flags: + assert flags + flags += '|' + f.methoddef_flags + + methoddef_define = """ +#define {methoddef_name} \\ + {{"{name}", (PyCFunction){c_basename}, {methoddef_flags}, {c_basename}__doc__}}, +""".strip().replace('{methoddef_flags}', flags) + + # parser_prototype mustn't be None, but it could be an empty string. + assert parser_prototype is not None + assert not parser_prototype.endswith(';') + + if parser_prototype: + parser_prototype += ';' + + assert impl_definition + if impl_prototype is None: + impl_prototype = impl_definition + ";" + + # __new__ and __init__ don't need methoddefs + if f.kind in (METHOD_NEW, METHOD_INIT): + methoddef_define = '' + + d = { + "docstring_prototype" : docstring_prototype, + "docstring_definition" : docstring_definition, + "impl_prototype" : impl_prototype, + "methoddef_define" : methoddef_define, + "parser_prototype" : parser_prototype, + "parser_definition" : parser_definition, + "impl_definition" : impl_definition, + } + + d2 = {} + for name, value in d.items(): + if value: + value = '\n' + value + '\n' + d2[name] = value + return d2 @staticmethod def group_to_variable_name(group): @@ -649,7 +843,7 @@ static {impl_return_type} add("}}") template_dict['option_group_parsing'] = output() - def render_function(self, f): + def render_function(self, clinic, f): if not f: return "" @@ -705,6 +899,27 @@ static {impl_return_type} if has_option_groups and (not positional): fail("You cannot use optional groups ('[' and ']')\nunless all parameters are positional-only ('/').") + # HACK + # when we're METH_O, but have a custom + # return converter, we use + # "impl_parameters" for the parsing + # function because that works better. + # but that means we must supress actually + # declaring the impl's parameters as variables + # in the parsing function. but since it's + # METH_O, we only have one anyway, so we don't + # have any problem finding it. + default_return_converter = (not f.return_converter or + f.return_converter.type == 'PyObject *') + if (len(parameters) == 1 and + parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and + not converters[0].is_optional() and + isinstance(converters[0], object_converter) and + converters[0].format_unit == 'O' and + not default_return_converter): + + data.declarations.pop(0) + # now insert our "self" (or whatever) parameters # (we deliberately don't call render on self converters) stock_self = self_converter('self', f) @@ -731,63 +946,41 @@ static {impl_return_type} template_dict['cleanup'] = "".join(data.cleanup) template_dict['return_value'] = data.return_value - template_dict['impl_prototype'] = self.impl_prototype_template.format_map(template_dict) + if has_option_groups: + self.render_option_group_parsing(f, template_dict) - default_return_converter = (not f.return_converter or - f.return_converter.type == 'PyObject *') + templates = self.output_templates(f) - if not parameters: - template = self.meth_noargs_template(f.methoddef_flags) - elif (len(parameters) == 1 and - parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and - not converters[0].is_optional() and - isinstance(converters[0], object_converter) and - converters[0].format_unit == 'O'): - if default_return_converter: - template = self.meth_o_template(f.methoddef_flags) - else: - # HACK - # we're using "impl_parameters" for the - # non-impl function, because that works - # better for METH_O. but that means we - # must supress actually declaring the - # impl's parameters as variables in the - # non-impl. but since it's METH_O, we - # only have one anyway, so - # we don't have any problem finding it. - declarations_copy = list(data.declarations) - before, pyobject, after = declarations_copy[0].partition('PyObject *') - assert not before, "hack failed, see comment" - assert pyobject, "hack failed, see comment" - assert after and after[0].isalpha(), "hack failed, see comment" - del declarations_copy[0] - template_dict['declarations'] = "\n".join(declarations_copy) - template = self.meth_o_return_converter_template(f.methoddef_flags) - elif has_option_groups: - self.render_option_group_parsing(f, template_dict) - template = self.option_group_template(f.methoddef_flags) + for name, destination in clinic.field_destinations.items(): + template = templates[name] + if has_option_groups: + template = linear_format(template, + option_group_parsing=template_dict['option_group_parsing']) template = linear_format(template, - option_group_parsing=template_dict['option_group_parsing']) - elif positional: - template = self.positional_only_template(f.methoddef_flags) - else: - template = self.keywords_template(f.methoddef_flags) + declarations=template_dict['declarations'], + return_conversion=template_dict['return_conversion'], + initializers=template_dict['initializers'], + cleanup=template_dict['cleanup'], + ) + + # Only generate the "exit:" label + # if we have any gotos + need_exit_label = "goto exit;" in template + template = linear_format(template, + exit_label="exit:" if need_exit_label else '' + ) - template = linear_format(template, - declarations=template_dict['declarations'], - return_conversion=template_dict['return_conversion'], - initializers=template_dict['initializers'], - cleanup=template_dict['cleanup'], - ) + s = template.format_map(template_dict) - # Only generate the "exit:" label - # if we have any gotos - need_exit_label = "goto exit;" in template - template = linear_format(template, - exit_label="exit:" if need_exit_label else '' - ) + if clinic.line_prefix: + s = indent_all_lines(s, clinic.line_prefix) + if clinic.line_suffix: + s = suffix_all_lines(s, clinic.line_suffix) + + destination.append(s) + + return clinic.get_destination('block').dump() - return template.format_map(template_dict) @contextlib.contextmanager @@ -889,19 +1082,27 @@ class BlockParser: self.last_checksum_re = None self.last_dsl_name = None self.dsl_name = None + self.first_block = True def __iter__(self): return self def __next__(self): - if not self.input: - raise StopIteration + while True: + if not self.input: + raise StopIteration + + if self.dsl_name: + return_value = self.parse_clinic_block(self.dsl_name) + self.dsl_name = None + self.first_block = False + return return_value + block = self.parse_verbatim_block() + if self.first_block and not block.input: + continue + self.first_block = False + return block - if self.dsl_name: - return_value = self.parse_clinic_block(self.dsl_name) - self.dsl_name = None - return return_value - return self.parse_verbatim_block() def is_start_line(self, line): match = self.start_re.match(line.lstrip()) @@ -980,7 +1181,8 @@ class BlockParser: if checksum != computed: fail("Checksum mismatch!\nExpected: {}\nComputed: {}\n" "Suggested fix: remove all generated code including " - "the end marker, or use the '-f' option." + "the end marker,\n" + "or use the '-f' option." .format(checksum, computed)) else: # put back output @@ -1025,15 +1227,62 @@ class BlockPrinter: write(self.language.stop_line.format(dsl_name=dsl_name)) write("\n") - output = block.output + output = ''.join(block.output) if output: - write(output) if not output.endswith('\n'): - write('\n') + output += '\n' + write(output) write(self.language.checksum_line.format(dsl_name=dsl_name, checksum=compute_checksum(output))) write("\n") + def write(self, text): + self.f.write(text) + + +class Destination: + def __init__(self, name, type, clinic, *args): + self.name = name + self.type = type + self.clinic = clinic + valid_types = ('buffer', 'file', 'suppress', 'two-pass') + if type not in valid_types: + fail("Invalid destination type " + repr(type) + " for " + name + " , must be " + ', '.join(valid_types)) + extra_arguments = 1 if type == "file" else 0 + if len(args) < extra_arguments: + fail("Not enough arguments for destination " + name + " new " + type) + if len(args) > extra_arguments: + fail("Too many arguments for destination " + name + " new " + type) + if type =='file': + d = {} + d['filename'] = filename = clinic.filename + d['basename'], d['extension'] = os.path.splitext(filename) + self.filename = args[0].format_map(d) + if type == 'two-pass': + self.id = None + + self.text, self.append, self._dump = _text_accumulator() + + def __repr__(self): + if self.type == 'file': + file_repr = " " + repr(self.filename) + else: + file_repr = '' + return "".join(("")) + + def clear(self): + if self.type != 'buffer': + fail("Can't clear destination" + self.name + " , it's not of type buffer") + self.text.clear() + + def dump(self): + if self.type == 'two-pass': + if self.id is None: + self.id = str(uuid.uuid4()) + return self.id + fail("You can only dump a two-pass buffer exactly once!") + return self._dump() + # maps strings to Language objects. # "languages" maps the name of the language ("C", "Python"). @@ -1066,11 +1315,51 @@ legacy_converters = {} return_converters = {} class Clinic: + + presets_text = """ +preset original +everything block +docstring_prototype suppress +parser_prototype suppress + +preset file +everything file +docstring_prototype suppress +parser_prototype suppress +impl_definition block + +preset buffer +everything buffer +docstring_prototype suppress +impl_prototype suppress +parser_prototype suppress +impl_definition block + +preset partial-buffer +everything buffer +docstring_prototype block +impl_prototype suppress +methoddef_define block +parser_prototype block +impl_definition block + +preset two-pass +everything buffer +docstring_prototype two-pass +impl_prototype suppress +methoddef_define two-pass +parser_prototype two-pass +impl_definition block + +""" + def __init__(self, language, printer=None, *, verify=True, filename=None): # maps strings to Parser objects. # (instantiated from the "parsers" global.) self.parsers = {} self.language = language + if printer: + fail("Custom printers are broken right now") self.printer = printer or BlockPrinter(language) self.verify = verify self.filename = filename @@ -1078,9 +1367,66 @@ class Clinic: self.classes = collections.OrderedDict() self.functions = [] + self.line_prefix = self.line_suffix = '' + + self.destinations = {} + self.add_destination("block", "buffer") + self.add_destination("suppress", "suppress") + self.add_destination("buffer", "buffer") + self.add_destination("two-pass", "two-pass") + if filename: + self.add_destination("file", "file", "{basename}.clinic{extension}") + + d = self.destinations.get + self.field_destinations = collections.OrderedDict(( + ('docstring_prototype', d('suppress')), + ('docstring_definition', d('block')), + ('methoddef_define', d('block')), + ('impl_prototype', d('block')), + ('parser_prototype', d('suppress')), + ('parser_definition', d('block')), + ('impl_definition', d('block')), + )) + + self.field_destinations_stack = [] + + self.presets = {} + preset = None + for line in self.presets_text.strip().split('\n'): + line = line.strip() + if not line: + continue + name, value = line.split() + if name == 'preset': + self.presets[value] = preset = collections.OrderedDict() + continue + + destination = self.get_destination(value) + + if name == 'everything': + for name in self.field_destinations: + preset[name] = destination + continue + + assert name in self.field_destinations + preset[name] = destination + global clinic clinic = self + def get_destination(self, name, default=unspecified): + d = self.destinations.get(name) + if not d: + if default is not unspecified: + return default + fail("Destination does not exist: " + repr(name)) + return d + + def add_destination(self, name, type, *args): + if name in self.destinations: + fail("Destination already exists: " + repr(name)) + self.destinations[name] = Destination(name, type, self, *args) + def parse(self, input): printer = self.printer self.block_parser = BlockParser(input, self.language, verify=self.verify) @@ -1097,7 +1443,66 @@ class Clinic: fail('Exception raised during parsing:\n' + traceback.format_exc().rstrip()) printer.print_block(block) - return printer.f.getvalue() + + second_pass_replacements = {} + + for name, destination in self.destinations.items(): + if destination.type == 'suppress': + continue + output = destination._dump() + + if destination.type == 'two-pass': + if destination.id: + second_pass_replacements[destination.id] = output + elif output: + fail("Two-pass buffer " + repr(name) + " not empty at end of file!") + continue + + if output: + + block = Block("", dsl_name="clinic", output=output) + + if destination.type == 'buffer': + block.input = "dump " + name + "\n" + warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.") + printer.write("\n") + printer.print_block(block) + continue + + if destination.type == 'file': + try: + with open(destination.filename, "rt") as f: + parser_2 = BlockParser(f.read(), language=self.language) + blocks = list(parser_2) + if (len(blocks) != 1) or (blocks[0].input != 'preserve\n'): + fail("Modified destination file " + repr(destination.filename) + ", not overwriting!") + except FileNotFoundError: + pass + + block.input = 'preserve\n' + printer_2 = BlockPrinter(self.language) + printer_2.print_block(block) + with open(destination.filename, "wt") as f: + f.write(printer_2.f.getvalue()) + continue + text = printer.f.getvalue() + + if second_pass_replacements: + printer_2 = BlockPrinter(self.language) + parser_2 = BlockParser(text, self.language) + changed = False + for block in parser_2: + if block.dsl_name: + for id, replacement in second_pass_replacements.items(): + if id in block.output: + changed = True + block.output = block.output.replace(id, replacement) + printer_2.print_block(block) + if changed: + text = printer_2.f.getvalue() + + return text + def _module_and_class(self, fields): """ @@ -2195,6 +2600,7 @@ class DSLParser: self.kind = CALLABLE self.coexist = False self.parameter_continuation = '' + self.preserve_output = False def directive_version(self, required): global version @@ -2226,6 +2632,77 @@ class DSLParser: module.classes[name] = c self.block.signatures.append(c) + def directive_set(self, name, value): + if name not in ("line_prefix", "line_suffix"): + fail("unknown variable", repr(name)) + + value = value.format_map({ + 'block comment start': '/*', + 'block comment end': '*/', + }) + + self.clinic.__dict__[name] = value + + def directive_destination(self, name, command, *args): + if command is 'new': + self.clinic.add_destination(name, command, *args) + return + + if command is 'clear': + self.clinic.get_destination(name).clear() + fail("unknown destination command", repr(command)) + + + def directive_output(self, field, destination=''): + fd = self.clinic.field_destinations + + if field == "preset": + preset = self.clinic.presets.get(destination) + if not preset: + fail("Unknown preset " + repr(destination) + "!") + fd.update(preset) + return + + if field == "push": + self.clinic.field_destinations_stack.append(fd.copy()) + return + + if field == "pop": + if not self.clinic.field_destinations_stack: + fail("Can't 'output pop', stack is empty!") + previous_fd = self.clinic.field_destinations_stack.pop() + fd.update(previous_fd) + return + + # secret command for debugging! + if field == "print": + self.block.output.append(pprint.pformat(fd)) + self.block.output.append('\n') + return + + d = self.clinic.get_destination(destination) + + if field == "everything": + for name in list(fd): + fd[name] = d + return + + if field not in fd: + fail("Invalid field " + repr(field) + ", must be one of:\n " + ", ".join(valid_fields)) + fd[field] = d + + def directive_dump(self, name): + self.block.output.append(self.clinic.get_destination(name).dump()) + + def directive_print(self, *args): + self.block.output.append(' '.join(args)) + self.block.output.append('\n') + + def directive_preserve(self): + if self.preserve_output: + fail("Can't have preserve twice in one block!") + self.preserve_output = True + def at_classmethod(self): if self.kind is not CALLABLE: fail("Can't set @classmethod, function is not a normal callable") @@ -2241,10 +2718,11 @@ class DSLParser: fail("Called @coexist twice!") self.coexist = True - def parse(self, block): self.reset() self.block = block + self.saved_output = self.block.output + block.output = [] block_start = self.clinic.block_parser.line_number lines = block.input.split('\n') for line_number, line in enumerate(lines, self.clinic.block_parser.block_start_line_number): @@ -2255,7 +2733,12 @@ class DSLParser: self.next(self.state_terminal) self.state(None) - block.output = self.clinic.language.render(block.signatures) + block.output.extend(self.clinic.language.render(clinic, block.signatures)) + + if self.preserve_output: + if block.output: + fail("'preserve' only works for blocks that don't produce any output!") + block.output = self.saved_output @staticmethod def ignore_line(line): @@ -2313,7 +2796,10 @@ class DSLParser: directive_name = fields[0] directive = self.directives.get(directive_name, None) if directive: - directive(*fields[1:]) + try: + directive(*fields[1:]) + except TypeError as e: + fail(str(e)) return # are we cloning? @@ -2439,8 +2925,6 @@ class DSLParser: # with X spaces such that F < X < P. (As before, F is the indent # of the function declaration.) # - ############## - # # Also, currently Argument Clinic places the following restrictions on groups: # * Each group must contain at least one parameter. # * Each group may contain at most one group, which must be the furthest @@ -2680,6 +3164,9 @@ class DSLParser: kind = inspect.Parameter.KEYWORD_ONLY if self.keyword_only else inspect.Parameter.POSITIONAL_OR_KEYWORD p = Parameter(parameter_name, kind, function=self.function, converter=converter, default=value, group=self.group) + + if parameter_name in self.function.parameters: + fail("You can't have two parameters named " + repr(parameter_name) + "!") self.function.parameters[parameter_name] = p def parse_converter(self, annotation): @@ -3063,20 +3550,6 @@ def main(argv): s = parameter_name parameters.append(s) print(' {}({})'.format(short_name, ', '.join(parameters))) - # add_comma = False - # for parameter_name, parameter in signature.parameters.items(): - # if parameter.kind == inspect.Parameter.KEYWORD_ONLY: - # if add_comma: - # parameters.append(', ') - # else: - # add_comma = True - # s = parameter_name - # if parameter.default != inspect.Parameter.empty: - # s += '=' + repr(parameter.default) - # parameters.append(s) - # parameters.append(')') - - # print(" ", short_name + "".join(parameters)) print() print("All converters also accept (c_default=None, py_default=None, annotation=None).") print("All return converters also accept (py_default=None).") diff --git a/Tools/clinic/clinic_test.py b/Tools/clinic/clinic_test.py index 6472527..0226ce6 100644 --- a/Tools/clinic/clinic_test.py +++ b/Tools/clinic/clinic_test.py @@ -34,6 +34,9 @@ class FakeConvertersDict: def get(self, name, default): return self.used_converters.setdefault(name, FakeConverterFactory(name)) +clinic.Clinic.presets_text = '' +c = clinic.Clinic(language='C') + class FakeClinic: def __init__(self): self.converters = FakeConvertersDict() @@ -44,6 +47,32 @@ class FakeClinic: self.modules = collections.OrderedDict() clinic.clinic = self self.name = "FakeClinic" + self.line_prefix = self.line_suffix = '' + self.destinations = {} + self.add_destination("block", "buffer") + self.add_destination("file", "buffer") + self.add_destination("suppress", "suppress") + d = self.destinations.get + self.field_destinations = collections.OrderedDict(( + ('docstring_prototype', d('suppress')), + ('docstring_definition', d('block')), + ('methoddef_define', d('block')), + ('impl_prototype', d('block')), + ('parser_prototype', d('suppress')), + ('parser_definition', d('block')), + ('impl_definition', d('block')), + )) + + def get_destination(self, name): + d = self.destinations.get(name) + if not d: + sys.exit("Destination does not exist: " + repr(name)) + return d + + def add_destination(self, name, type, *args): + if name in self.destinations: + sys.exit("Destination already exists: " + repr(name)) + self.destinations[name] = clinic.Destination(name, type, self, *args) def is_directive(self, name): return name == "module" -- cgit v0.12