Issue #20226: Major improvements to Argument Clinic.

* You may now specify an expression as the default value for a
  parameter!  Example: "sys.maxsize - 1".  This support is
  intentionally quite limited; you may only use values that
  can be represented as static C values.
* Removed "doc_default", simplified support for "c_default"
  and "py_default".  (I'm not sure we still even need
  "py_default", but I'm leaving it in for now in case a
  use presents itself.)
* Parameter lines support a trailing '\\' as a line
  continuation character, allowing you to break up long lines.
* The argument parsing code generated when supporting optional
  groups now uses PyTuple_GET_SIZE instead of PyTuple_GetSize,
  leading to a 850% speedup in parsing.  (Just kidding, this
  is an unmeasurable difference.)
* A bugfix for the recent regression where the generated
  prototype from pydoc for builtins would be littered with
  unreadable "=<object ...>"" default values for parameters
  that had no default value.
* Converted some asserts into proper failure messages.
* Many doc improvements and fixes.
This commit is contained in:
Larry Hastings 2014-01-16 11:32:01 -08:00
parent e1f554490d
commit 2a727916c5
13 changed files with 470 additions and 185 deletions

View File

@ -294,6 +294,8 @@ Other objects
the object pointer is stored. If the Python object does not have the required the object pointer is stored. If the Python object does not have the required
type, :exc:`TypeError` is raised. type, :exc:`TypeError` is raised.
.. _o_ampersand:
``O&`` (object) [*converter*, *anything*] ``O&`` (object) [*converter*, *anything*]
Convert a Python object to a C variable through a *converter* function. This Convert a Python object to a C variable through a *converter* function. This
takes two arguments: the first is a function, the second is the address of a C takes two arguments: the first is a function, the second is the address of a C

View File

@ -127,6 +127,12 @@ convert a function to work with it. Let's dive in!
margin, with no line wider than 80 characters. margin, with no line wider than 80 characters.
(Argument Clinic will preserve indents inside the docstring.) (Argument Clinic will preserve indents inside the docstring.)
If the old docstring had a first line that looked like a function
signature, throw that line away. (The docstring doesn't need it
anymore--when you use ``help()`` on your builtin in the future,
the first line will be built automatically based on the function's
signature.)
Sample:: Sample::
/*[clinic input] /*[clinic input]
@ -196,6 +202,10 @@ convert a function to work with it. Let's dive in!
name_of_parameter: converter = default_value name_of_parameter: converter = default_value
Argument Clinic's support for "default values" is quite sophisticated;
please see :ref:`the section below on default values <default_values>`
for more information.
Add a blank line below the parameters. Add a blank line below the parameters.
What's a "converter"? It establishes both the type What's a "converter"? It establishes both the type
@ -513,16 +523,6 @@ The base function would now be named ``pickler_dumper()``,
and the impl function would now be named ``pickler_dumper_impl()``. and the impl function would now be named ``pickler_dumper_impl()``.
The NULL default value
----------------------
For string and object parameters, you can set them to ``None`` to indicate
that there is no default. However, that means the C variable will be
initialized to ``Py_None``. For convenience's sakes, there's a special
value called ``NULL`` for just this case: from Python's perspective it
behaves like a default value of ``None``, but the C variable is initialized
with ``NULL``.
Converting functions using PyArg_UnpackTuple Converting functions using PyArg_UnpackTuple
-------------------------------------------- --------------------------------------------
@ -654,35 +654,70 @@ the same converters.
All arguments to Argument Clinic converters are keyword-only. All arguments to Argument Clinic converters are keyword-only.
All Argument Clinic converters accept the following arguments: All Argument Clinic converters accept the following arguments:
``py_default`` ``c_default``
The default value for this parameter when defined in Python. The default value for this parameter when defined in C.
Specifically, the value that will be used in the ``inspect.Signature`` Specifically, this will be the initializer for the variable declared
string. in the "parse function". See :ref:`the section on default values <default_values>`
If a default value is specified for the parameter, defaults to for how to use this.
``repr(default)``, else defaults to ``None``. Specified as a string.
Specified as a string.
``c_default`` ``annotation``
The default value for this parameter when defined in C. The annotation value for this parameter. Not currently supported,
Specifically, this will be the initializer for the variable declared because PEP 8 mandates that the Python library may not use
in the "parse function". annotations.
Specified as a string.
``required`` In addition, some converters accept additional arguments. Here is a list
If a parameter takes a default value, Argument Clinic infers that the of these arguments, along with their meanings:
parameter is optional. However, you may want a parameter to take a
default value in C, but not behave in Python as if the parameter is
optional. Passing in ``required=True`` to a converter tells Argument
Clinic that this parameter is not optional, even if it has a default
value.
(The need for ``required`` may be obviated by ``c_default``, which is ``bitwise``
newer but arguably a better solution.) Only supported for unsigned integers. The native integer value of this
Python argument will be written to the parameter without any range checking,
even for negative values.
``annotation`` ``converter``
The annotation value for this parameter. Not currently supported, Only supported by the ``object`` converter. Specifies the name of a
because PEP 8 mandates that the Python library may not use :ref:`C "converter function" <o_ampersand>`
annotations. to use to convert this object to a native type.
``encoding``
Only supported for strings. Specifies the encoding to use when converting
this string from a Python str (Unicode) value into a C ``char *`` value.
``length``
Only supported for strings. If true, requests that the length of the
string be passed in to the impl function, just after the string parameter,
in a parameter named ``<parameter_name>_length``.
``nullable``
Only supported for strings. If true, this parameter may also be set to
``None``, in which case the C parameter will be set to ``NULL``.
``subclass_of``
Only supported for the ``object`` converter. Requires that the Python
value be a subclass of a Python type, as expressed in C.
``types``
Only supported for the ``object`` (and ``self``) converter. Specifies
the C type that will be used to declare the variable. Default value is
``"PyObject *"``.
``types``
A string containing a list of Python types (and possibly pseudo-types);
this restricts the allowable Python argument to values of these types.
(This is not a general-purpose facility; as a rule it only supports
specific lists of types as shown in the legacy converter table.)
``zeroes``
Only supported for strings. If true, embedded NUL bytes (``'\\0'``) are
permitted inside the value.
Please note, not every possible combination of arguments will work.
Often these arguments are implemented internally by specific ``PyArg_ParseTuple``
*format units*, with specific behavior. For example, currently you cannot
call ``str`` and pass in ``zeroes=True`` without also specifying an ``encoding``;
although it's perfectly reasonable to think this would work, these semantics don't
map to any existing format unit. So Argument Clinic doesn't support it. (Or, at
least, not yet.)
Below is a table showing the mapping of legacy converters into real Below is a table showing the mapping of legacy converters into real
Argument Clinic converters. On the left is the legacy converter, Argument Clinic converters. On the left is the legacy converter,
@ -720,9 +755,9 @@ on the right is the text you'd replace it with.
``'u'`` ``Py_UNICODE`` ``'u'`` ``Py_UNICODE``
``'U'`` ``unicode`` ``'U'`` ``unicode``
``'w*'`` ``Py_buffer(types='bytearray rwbuffer')`` ``'w*'`` ``Py_buffer(types='bytearray rwbuffer')``
``'y#'`` ``str(type='bytes', length=True)`` ``'y#'`` ``str(types='bytes', length=True)``
``'Y'`` ``PyByteArrayObject`` ``'Y'`` ``PyByteArrayObject``
``'y'`` ``str(type='bytes')`` ``'y'`` ``str(types='bytes')``
``'y*'`` ``Py_buffer`` ``'y*'`` ``Py_buffer``
``'Z#'`` ``Py_UNICODE(nullable=True, length=True)`` ``'Z#'`` ``Py_UNICODE(nullable=True, length=True)``
``'z#'`` ``str(nullable=True, length=True)`` ``'z#'`` ``str(nullable=True, length=True)``
@ -789,6 +824,90 @@ This restriction doesn't seem unreasonable; CPython itself always passes in stat
hard-coded encoding strings for parameters whose format units start with ``e``. hard-coded encoding strings for parameters whose format units start with ``e``.
.. _default_values:
Parameter default values
------------------------
Default values for parameters can be any of a number of values.
At their simplest, they can be string, int, or float literals::
foo: str = "abc"
bar: int = 123
bat: float = 45.6
They can also use any of Python's built-in constants::
yep: bool = True
nope: bool = False
nada: object = None
There's also special support for a default value of ``NULL``, and
for simple expressions, documented in the following sections.
The ``NULL`` default value
--------------------------
For string and object parameters, you can set them to ``None`` to indicate
that there's no default. However, that means the C variable will be
initialized to ``Py_None``. For convenience's sakes, there's a special
value called ``NULL`` for just this reason: from Python's perspective it
behaves like a default value of ``None``, but the C variable is initialized
with ``NULL``.
Expressions specified as default values
---------------------------------------
The default value for a parameter can be more than just a literal value.
It can be an entire expression, using math operators and looking up attributes
on objects. However, this support isn't exactly simple, because of some
non-obvious semantics.
Consider the following example::
foo: Py_ssize_t = sys.maxsize - 1
``sys.maxsize`` can have different values on different platforms. Therefore
Argument Clinic can't simply evaluate that expression locally and hard-code it
in C. So it stores the default in such a way that it will get evaluated at
runtime, when the user asks for the function's signature.
What namespace is available when the expression is evaluated? It's evaluated
in the context of the module the builtin came from. So, if your module has an
attribute called "``max_widgets``", you may simply use it::
foo: Py_ssize_t = max_widgets
If the symbol isn't found in the current module, it fails over to looking in
``sys.modules``. That's how it can find ``sys.maxsize`` for example. (Since you
don't know in advance what modules the user will load into their interpreter,
it's best to restrict yourself to modules that are preloaded by Python itself.)
Evaluating default values only at runtime means Argument Clinic can't compute
the correct equivalent C default value. So you need to tell it explicitly.
When you use an expression, you must also specify the equivalent expression
in C, using the ``c_default`` parameter to the converter::
foo: Py_ssize_t(c_default="PY_SSIZE_T_MAX - 1") = sys.maxsize - 1
Another complication: Argument Clinic can't know in advance whether or not the
expression you supply is valid. It parses it to make sure it looks legal, but
it can't *actually* know. You must be very careful when using expressions to
specify values that are guaranteed to be valid at runtime!
Finally, because expressions must be representable as static C values, there
are many restrictions on legal expressions. Here's a list of Python features
you're not permitted to use:
* Function calls.
* Inline if statements (``3 if foo else 5``).
* Automatic sequence unpacking (``*[1, 2, 3]``).
* List/set/dict comprehensions and generator expressions.
* Tuple/list/set/dict literals.
Using a return converter Using a return converter
------------------------ ------------------------
@ -1096,7 +1215,73 @@ any arguments.
You can still use a self converter, a return converter, and specify You can still use a self converter, a return converter, and specify
a ``type`` argument to the object converter for ``METH_O``. a ``type`` argument to the object converter for ``METH_O``.
Using Argument Clinic in Python files 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.)
------------------------------------- -------------------------------------
It's actually possible to use Argument Clinic to preprocess Python files. It's actually possible to use Argument Clinic to preprocess Python files.

View File

@ -1954,6 +1954,8 @@ class Signature:
if not s: if not s:
return None return None
Parameter = cls._parameter_cls
if s.endswith("/)"): if s.endswith("/)"):
kind = Parameter.POSITIONAL_ONLY kind = Parameter.POSITIONAL_ONLY
s = s[:-2] + ')' s = s[:-2] + ')'
@ -1969,55 +1971,74 @@ class Signature:
if not isinstance(module, ast.Module): if not isinstance(module, ast.Module):
return None return None
# ast.FunctionDef
f = module.body[0] f = module.body[0]
parameters = [] parameters = []
empty = Parameter.empty empty = Parameter.empty
invalid = object() invalid = object()
def parse_attribute(node): module = None
if not isinstance(node.ctx, ast.Load): module_dict = {}
return None module_name = getattr(func, '__module__', None)
if module_name:
module = sys.modules.get(module_name, None)
if module:
module_dict = module.__dict__
sys_module_dict = sys.modules
value = node.value def parse_name(node):
o = parse_node(value) assert isinstance(node, ast.arg)
if o is invalid: if node.annotation != None:
return invalid raise ValueError("Annotations are not currently supported")
return node.arg
if isinstance(value, ast.Name): def wrap_value(s):
name = o try:
if name not in sys.modules: value = eval(s, module_dict)
return invalid except NameError:
o = sys.modules[name] try:
value = eval(s, sys_module_dict)
except NameError:
raise RuntimeError()
return getattr(o, node.attr, invalid) if isinstance(value, str):
return ast.Str(value)
if isinstance(value, (int, float)):
return ast.Num(value)
if isinstance(value, bytes):
return ast.Bytes(value)
if value in (True, False, None):
return ast.NameConstant(value)
raise RuntimeError()
def parse_node(node): class RewriteSymbolics(ast.NodeTransformer):
if isinstance(node, ast.arg): def visit_Attribute(self, node):
if node.annotation != None: a = []
raise ValueError("Annotations are not currently supported") n = node
return node.arg while isinstance(n, ast.Attribute):
if isinstance(node, ast.Num): a.append(n.attr)
return node.n n = n.value
if isinstance(node, ast.Str): if not isinstance(n, ast.Name):
return node.s raise RuntimeError()
if isinstance(node, ast.NameConstant): a.append(n.id)
return node.value value = ".".join(reversed(a))
if isinstance(node, ast.Attribute): return wrap_value(value)
return parse_attribute(node)
if isinstance(node, ast.Name): def visit_Name(self, node):
if not isinstance(node.ctx, ast.Load): if not isinstance(node.ctx, ast.Load):
return invalid raise ValueError()
return node.id return wrap_value(node.id)
return invalid
def p(name_node, default_node, default=empty): def p(name_node, default_node, default=empty):
name = parse_node(name_node) name = parse_name(name_node)
if name is invalid: if name is invalid:
return None return None
if default_node: if default_node:
o = parse_node(default_node) try:
default_node = RewriteSymbolics().visit(default_node)
o = ast.literal_eval(default_node)
except ValueError:
o = invalid
if o is invalid: if o is invalid:
return None return None
default = o if o is not invalid else default default = o if o is not invalid else default

View File

@ -1601,12 +1601,15 @@ class TestSignatureObject(unittest.TestCase):
self.assertTrue(isinstance(signature, inspect.Signature)) self.assertTrue(isinstance(signature, inspect.Signature))
def p(name): return signature.parameters[name].default def p(name): return signature.parameters[name].default
self.assertEqual(p('s'), 'avocado') self.assertEqual(p('s'), 'avocado')
self.assertEqual(p('b'), b'bytes')
self.assertEqual(p('d'), 3.14) self.assertEqual(p('d'), 3.14)
self.assertEqual(p('i'), 35) self.assertEqual(p('i'), 35)
self.assertEqual(p('c'), sys.maxsize)
self.assertEqual(p('n'), None) self.assertEqual(p('n'), None)
self.assertEqual(p('t'), True) self.assertEqual(p('t'), True)
self.assertEqual(p('f'), False) self.assertEqual(p('f'), False)
self.assertEqual(p('local'), 3)
self.assertEqual(p('sys'), sys.maxsize)
self.assertEqual(p('exp'), sys.maxsize - 1)
def test_signature_on_non_function(self): def test_signature_on_non_function(self):
with self.assertRaisesRegex(TypeError, 'is not a callable object'): with self.assertRaisesRegex(TypeError, 'is not a callable object'):

View File

@ -88,6 +88,9 @@ Tests
Tools/Demos Tools/Demos
----------- -----------
- Issue #20226: Argument Clinic now permits simple expressions
(e.g. "sys.maxsize - 1") as default values for parameters.
- Issue #19936: Added executable bits or shebang lines to Python scripts which - Issue #19936: Added executable bits or shebang lines to Python scripts which
requires them. Disable executable bits and shebang lines in test and requires them. Disable executable bits and shebang lines in test and
benchmark files in order to prevent using a random system python, and in benchmark files in order to prevent using a random system python, and in

View File

@ -618,7 +618,7 @@ curses_window_addch(PyObject *self, PyObject *args)
int group_right_1 = 0; int group_right_1 = 0;
long attr = 0; long attr = 0;
switch (PyTuple_Size(args)) { switch (PyTuple_GET_SIZE(args)) {
case 1: case 1:
if (!PyArg_ParseTuple(args, "O:addch", &ch)) if (!PyArg_ParseTuple(args, "O:addch", &ch))
return NULL; return NULL;
@ -650,7 +650,7 @@ curses_window_addch(PyObject *self, PyObject *args)
static PyObject * static PyObject *
curses_window_addch_impl(PyObject *self, int group_left_1, int x, int y, PyObject *ch, int group_right_1, long attr) curses_window_addch_impl(PyObject *self, int group_left_1, int x, int y, PyObject *ch, int group_right_1, long attr)
/*[clinic end generated code: checksum=44ed958b891cde91205e584c766e048f3999714f]*/ /*[clinic end generated code: checksum=b073327add8197b6ba7fb96c87062422c8312954]*/
{ {
PyCursesWindowObject *cwself = (PyCursesWindowObject *)self; PyCursesWindowObject *cwself = (PyCursesWindowObject *)self;
int coordinates_group = group_left_1; int coordinates_group = group_left_1;

View File

@ -297,7 +297,7 @@ dbm_dbm_get(PyObject *self, PyObject *args)
int group_right_1 = 0; int group_right_1 = 0;
PyObject *default_value = NULL; PyObject *default_value = NULL;
switch (PyTuple_Size(args)) { switch (PyTuple_GET_SIZE(args)) {
case 1: case 1:
if (!PyArg_ParseTuple(args, "s#:get", &key, &key_length)) if (!PyArg_ParseTuple(args, "s#:get", &key, &key_length))
return NULL; return NULL;
@ -318,7 +318,7 @@ dbm_dbm_get(PyObject *self, PyObject *args)
static PyObject * 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, int group_right_1, PyObject *default_value)
/*[clinic end generated code: checksum=28cf8928811bde51e535d67ae98ea039d79df717]*/ /*[clinic end generated code: checksum=2c3209571267017f1b9abbd19e1b521849fd5d4a]*/
{ {
datum dbm_key, val; datum dbm_key, val;
@ -450,7 +450,7 @@ dbm.open as dbmopen
flags: str="r" flags: str="r"
How to open the file. "r" for reading, "w" for writing, etc. How to open the file. "r" for reading, "w" for writing, etc.
mode: int(doc_default="0o666") = 0o666 mode: int(py_default="0o666") = 0o666
If creating a new file, the mode bits for the new file If creating a new file, the mode bits for the new file
(e.g. os.O_RDWR). (e.g. os.O_RDWR).

View File

@ -39,7 +39,7 @@ _opcode_stack_effect(PyModuleDef *module, PyObject *args)
int oparg = 0; int oparg = 0;
int _return_value; int _return_value;
switch (PyTuple_Size(args)) { switch (PyTuple_GET_SIZE(args)) {
case 1: case 1:
if (!PyArg_ParseTuple(args, "i:stack_effect", &opcode)) if (!PyArg_ParseTuple(args, "i:stack_effect", &opcode))
return NULL; return NULL;
@ -64,7 +64,7 @@ exit:
static int static int
_opcode_stack_effect_impl(PyModuleDef *module, int opcode, int group_right_1, int oparg) _opcode_stack_effect_impl(PyModuleDef *module, int opcode, int group_right_1, int oparg)
/*[clinic end generated code: checksum=e880e62dc7b0de73403026eaf4f8074aa106358b]*/ /*[clinic end generated code: checksum=47e76ec27523da4ab192713642d32482cd743aa4]*/
{ {
int effect; int effect;
if (HAS_ARG(opcode)) { if (HAS_ARG(opcode)) {

View File

@ -2870,7 +2870,7 @@ PyDoc_STRVAR(docstring_with_signature_and_extra_newlines,
); );
PyDoc_STRVAR(docstring_with_signature_with_defaults, PyDoc_STRVAR(docstring_with_signature_with_defaults,
"docstring_with_signature_with_defaults(s='avocado', d=3.14, i=35, c=sys.maxsize, n=None, t=True, f=False)\n" "docstring_with_signature_with_defaults(s='avocado', b=b'bytes', d=3.14, i=35, n=None, t=True, f=False, local=the_number_three, sys=sys.maxsize, exp=sys.maxsize - 1)\n"
"\n" "\n"
"\n" "\n"
"\n" "\n"
@ -3317,6 +3317,8 @@ PyInit__testcapi(void)
Py_INCREF(&PyInstanceMethod_Type); Py_INCREF(&PyInstanceMethod_Type);
PyModule_AddObject(m, "instancemethod", (PyObject *)&PyInstanceMethod_Type); PyModule_AddObject(m, "instancemethod", (PyObject *)&PyInstanceMethod_Type);
PyModule_AddIntConstant(m, "the_number_three", 3);
TestError = PyErr_NewException("_testcapi.error", NULL, NULL); TestError = PyErr_NewException("_testcapi.error", NULL, NULL);
Py_INCREF(TestError); Py_INCREF(TestError);
PyModule_AddObject(m, "error", TestError); PyModule_AddObject(m, "error", TestError);

View File

@ -2401,7 +2401,7 @@ class dir_fd_converter(CConverter):
/*[clinic input] /*[clinic input]
os.stat -> object(doc_default='stat_result') os.stat
path : path_t(allow_fd=True) path : path_t(allow_fd=True)
Path to be examined; can be string, bytes, or open-file-descriptor int. Path to be examined; can be string, bytes, or open-file-descriptor int.
@ -2523,7 +2523,7 @@ posix_lstat(PyObject *self, PyObject *args, PyObject *kwargs)
#define OS_ACCESS_DIR_FD_CONVERTER dir_fd_unavailable #define OS_ACCESS_DIR_FD_CONVERTER dir_fd_unavailable
#endif #endif
/*[clinic input] /*[clinic input]
os.access -> object(doc_default='True if granted, False otherwise') os.access
path: path_t(allow_fd=True) path: path_t(allow_fd=True)
Path to be tested; can be string, bytes, or open-file-descriptor int. Path to be tested; can be string, bytes, or open-file-descriptor int.

View File

@ -202,7 +202,7 @@ zlib_compress(PyModuleDef *module, PyObject *args)
int group_right_1 = 0; int group_right_1 = 0;
int level = 0; int level = 0;
switch (PyTuple_Size(args)) { switch (PyTuple_GET_SIZE(args)) {
case 1: case 1:
if (!PyArg_ParseTuple(args, "y*:compress", &bytes)) if (!PyArg_ParseTuple(args, "y*:compress", &bytes))
return NULL; return NULL;
@ -227,7 +227,7 @@ zlib_compress(PyModuleDef *module, PyObject *args)
static PyObject * static PyObject *
zlib_compress_impl(PyModuleDef *module, Py_buffer *bytes, int group_right_1, int level) zlib_compress_impl(PyModuleDef *module, Py_buffer *bytes, int group_right_1, int level)
/*[clinic end generated code: checksum=2c59af563a4595c5ecea4011701f482ae350aa5f]*/ /*[clinic end generated code: checksum=66c4d16d0b8b9dd423648d9ef00d6a89d3363665]*/
{ {
PyObject *ReturnVal = NULL; PyObject *ReturnVal = NULL;
Byte *input, *output = NULL; Byte *input, *output = NULL;

View File

@ -55,6 +55,13 @@ class Null:
NULL = Null() NULL = Null()
class Unknown:
def __repr__(self):
return '<Unknown>'
unknown = Unknown()
def _text_accumulator(): def _text_accumulator():
text = [] text = []
def output(): def output():
@ -197,7 +204,7 @@ def version_splitter(s):
accumulator = [] accumulator = []
def flush(): def flush():
if not accumulator: if not accumulator:
raise ValueError('Malformed version string: ' + repr(s)) raise ValueError('Unsupported version string: ' + repr(s))
version.append(int(''.join(accumulator))) version.append(int(''.join(accumulator)))
accumulator.clear() accumulator.clear()
@ -596,7 +603,7 @@ static {impl_return_type}
count_min = sys.maxsize count_min = sys.maxsize
count_max = -1 count_max = -1
add("switch (PyTuple_Size(args)) {{\n") add("switch (PyTuple_GET_SIZE(args)) {{\n")
for subset in permute_optional_groups(left, required, right): for subset in permute_optional_groups(left, required, right):
count = len(subset) count = len(subset)
count_min = min(count_min, count) count_min = min(count_min, count)
@ -1069,6 +1076,7 @@ class Clinic:
self.filename = filename self.filename = filename
self.modules = collections.OrderedDict() self.modules = collections.OrderedDict()
self.classes = collections.OrderedDict() self.classes = collections.OrderedDict()
self.functions = []
global clinic global clinic
clinic = self clinic = self
@ -1343,29 +1351,7 @@ class Parameter:
def is_keyword_only(self): def is_keyword_only(self):
return self.kind == inspect.Parameter.KEYWORD_ONLY return self.kind == inspect.Parameter.KEYWORD_ONLY
py_special_values = {
NULL: "None",
}
def py_repr(o):
special = py_special_values.get(o)
if special:
return special
return repr(o)
c_special_values = {
NULL: "NULL",
None: "Py_None",
}
def c_repr(o):
special = c_special_values.get(o)
if special:
return special
if isinstance(o, str):
return '"' + quoted_for_c_string(o) + '"'
return repr(o)
def add_c_converter(f, name=None): def add_c_converter(f, name=None):
if not name: if not name:
@ -1407,8 +1393,7 @@ class CConverter(metaclass=CConverterAutoRegister):
""" """
For the init function, self, name, function, and default For the init function, self, name, function, and default
must be keyword-or-positional parameters. All other must be keyword-or-positional parameters. All other
parameters (including "required" and "doc_default") parameters must be keyword-only.
must be keyword-only.
""" """
# The C type to use for this variable. # The C type to use for this variable.
@ -1418,23 +1403,23 @@ class CConverter(metaclass=CConverterAutoRegister):
# The Python default value for this parameter, as a Python value. # The Python default value for this parameter, as a Python value.
# Or the magic value "unspecified" if there is no default. # Or the magic value "unspecified" if there is no default.
# Or the magic value "unknown" if this value is a cannot be evaluated
# at Argument-Clinic-preprocessing time (but is presumed to be valid
# at runtime).
default = unspecified default = unspecified
# If not None, default must be isinstance() of this type. # If not None, default must be isinstance() of this type.
# (You can also specify a tuple of types.) # (You can also specify a tuple of types.)
default_type = None default_type = None
# "default" as it should appear in the documentation, as a string.
# Or None if there is no default.
doc_default = None
# "default" converted into a str for rendering into Python code.
py_default = None
# "default" converted into a C value, as a string. # "default" converted into a C value, as a string.
# Or None if there is no default. # Or None if there is no default.
c_default = None c_default = None
# "default" converted into a Python value, as a string.
# Or None if there is no default.
py_default = None
# The default value used to initialize the C variable when # The default value used to initialize the C variable when
# there is no default, but not specifying a default may # there is no default, but not specifying a default may
# result in an "uninitialized variable" warning. This can # result in an "uninitialized variable" warning. This can
@ -1485,12 +1470,12 @@ class CConverter(metaclass=CConverterAutoRegister):
# Only used by format units ending with '#'. # Only used by format units ending with '#'.
length = False length = False
def __init__(self, name, function, default=unspecified, *, doc_default=None, c_default=None, py_default=None, required=False, annotation=unspecified, **kwargs): def __init__(self, name, function, default=unspecified, *, c_default=None, py_default=None, annotation=unspecified, **kwargs):
self.function = function self.function = function
self.name = name self.name = name
if default is not unspecified: if default is not unspecified:
if self.default_type and not isinstance(default, self.default_type): if self.default_type and not isinstance(default, (self.default_type, Unknown)):
if isinstance(self.default_type, type): if isinstance(self.default_type, type):
types_str = self.default_type.__name__ types_str = self.default_type.__name__
else: else:
@ -1498,23 +1483,19 @@ class CConverter(metaclass=CConverterAutoRegister):
fail("{}: default value {!r} for field {} is not of type {}".format( fail("{}: default value {!r} for field {} is not of type {}".format(
self.__class__.__name__, default, name, types_str)) self.__class__.__name__, default, name, types_str))
self.default = default self.default = default
self.py_default = py_default if py_default is not None else py_repr(default)
self.doc_default = doc_default if doc_default is not None else self.py_default self.c_default = c_default
self.c_default = c_default if c_default is not None else c_repr(default) self.py_default = py_default
else:
self.py_default = py_default
self.doc_default = doc_default
self.c_default = c_default
if annotation != unspecified: if annotation != unspecified:
fail("The 'annotation' parameter is not currently permitted.") fail("The 'annotation' parameter is not currently permitted.")
self.required = required
self.converter_init(**kwargs) self.converter_init(**kwargs)
def converter_init(self): def converter_init(self):
pass pass
def is_optional(self): def is_optional(self):
return (self.default is not unspecified) and (not self.required) return (self.default is not unspecified)
def render(self, parameter, data): def render(self, parameter, data):
""" """
@ -1655,8 +1636,9 @@ class bool_converter(CConverter):
c_ignored_default = '0' c_ignored_default = '0'
def converter_init(self): def converter_init(self):
self.default = bool(self.default) if self.default is not unspecified:
self.c_default = str(int(self.default)) self.default = bool(self.default)
self.c_default = str(int(self.default))
class char_converter(CConverter): class char_converter(CConverter):
type = 'char' type = 'char'
@ -1665,7 +1647,7 @@ class char_converter(CConverter):
c_ignored_default = "'\0'" c_ignored_default = "'\0'"
def converter_init(self): def converter_init(self):
if len(self.default) != 1: if isinstance(self.default, str) and (len(self.default) != 1):
fail("char_converter: illegal default value " + repr(self.default)) fail("char_converter: illegal default value " + repr(self.default))
@ -1797,8 +1779,8 @@ class object_converter(CConverter):
@add_legacy_c_converter('s#', length=True) @add_legacy_c_converter('s#', length=True)
@add_legacy_c_converter('y', type="bytes") @add_legacy_c_converter('y', types="bytes")
@add_legacy_c_converter('y#', type="bytes", length=True) @add_legacy_c_converter('y#', types="bytes", length=True)
@add_legacy_c_converter('z', nullable=True) @add_legacy_c_converter('z', nullable=True)
@add_legacy_c_converter('z#', nullable=True, length=True) @add_legacy_c_converter('z#', nullable=True, length=True)
class str_converter(CConverter): class str_converter(CConverter):
@ -1993,8 +1975,8 @@ class CReturnConverter(metaclass=CReturnConverterAutoRegister):
# Or the magic value "unspecified" if there is no default. # Or the magic value "unspecified" if there is no default.
default = None default = None
def __init__(self, *, doc_default=None, **kwargs): def __init__(self, *, py_default=None, **kwargs):
self.doc_default = doc_default self.py_default = py_default
try: try:
self.return_converter_init(**kwargs) self.return_converter_init(**kwargs)
except TypeError as e: except TypeError as e:
@ -2212,6 +2194,7 @@ class DSLParser:
self.indent = IndentStack() self.indent = IndentStack()
self.kind = CALLABLE self.kind = CALLABLE
self.coexist = False self.coexist = False
self.parameter_continuation = ''
def directive_version(self, required): def directive_version(self, required):
global version global version
@ -2244,15 +2227,18 @@ class DSLParser:
self.block.signatures.append(c) self.block.signatures.append(c)
def at_classmethod(self): def at_classmethod(self):
assert self.kind is CALLABLE if self.kind is not CALLABLE:
fail("Can't set @classmethod, function is not a normal callable")
self.kind = CLASS_METHOD self.kind = CLASS_METHOD
def at_staticmethod(self): def at_staticmethod(self):
assert self.kind is CALLABLE if self.kind is not CALLABLE:
fail("Can't set @staticmethod, function is not a normal callable")
self.kind = STATIC_METHOD self.kind = STATIC_METHOD
def at_coexist(self): def at_coexist(self):
assert self.coexist == False if self.coexist:
fail("Called @coexist twice!")
self.coexist = True self.coexist = True
@ -2503,6 +2489,7 @@ class DSLParser:
if not self.indent.infer(line): if not self.indent.infer(line):
return self.next(self.state_function_docstring, line) return self.next(self.state_function_docstring, line)
self.parameter_continuation = ''
return self.next(self.state_parameter, line) return self.next(self.state_parameter, line)
@ -2516,6 +2503,10 @@ class DSLParser:
p.group = -p.group p.group = -p.group
def state_parameter(self, line): def state_parameter(self, line):
if self.parameter_continuation:
line = self.parameter_continuation + ' ' + line.lstrip()
self.parameter_continuation = ''
if self.ignore_line(line): if self.ignore_line(line):
return return
@ -2529,6 +2520,11 @@ class DSLParser:
# we indented, must be to new parameter docstring column # we indented, must be to new parameter docstring column
return self.next(self.state_parameter_docstring_start, line) return self.next(self.state_parameter_docstring_start, line)
line = line.rstrip()
if line.endswith('\\'):
self.parameter_continuation = line[:-1]
return
line = line.lstrip() line = line.lstrip()
if line in ('*', '/', '[', ']'): if line in ('*', '/', '[', ']'):
@ -2547,48 +2543,123 @@ class DSLParser:
else: else:
fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ")") fail("Function " + self.function.name + " has an unsupported group configuration. (Unexpected state " + str(self.parameter_state) + ")")
ast_input = "def x({}): pass".format(line) base, equals, default = line.rpartition('=')
if not equals:
base = default
default = None
module = None module = None
try: try:
ast_input = "def x({}): pass".format(base)
module = ast.parse(ast_input) module = ast.parse(ast_input)
except SyntaxError: except SyntaxError:
pass try:
default = None
ast_input = "def x({}): pass".format(line)
module = ast.parse(ast_input)
except SyntaxError:
pass
if not module: if not module:
fail("Function " + self.function.name + " has an invalid parameter declaration:\n\t" + line) fail("Function " + self.function.name + " has an invalid parameter declaration:\n\t" + line)
function_args = module.body[0].args function_args = module.body[0].args
parameter = function_args.args[0] parameter = function_args.args[0]
py_default = None
parameter_name = parameter.arg parameter_name = parameter.arg
name, legacy, kwargs = self.parse_converter(parameter.annotation) name, legacy, kwargs = self.parse_converter(parameter.annotation)
if function_args.defaults: if not default:
expr = function_args.defaults[0] value = unspecified
# mild hack: explicitly support NULL as a default value if 'py_default' in kwargs:
if isinstance(expr, ast.Name) and expr.id == 'NULL': fail("You can't specify py_default without specifying a default value!")
value = NULL else:
elif isinstance(expr, ast.Attribute): default = default.strip()
ast_input = "x = {}".format(default)
try:
module = ast.parse(ast_input)
# blacklist of disallowed ast nodes
class DetectBadNodes(ast.NodeVisitor):
bad = False
def bad_node(self, node):
self.bad = True
# inline function call
visit_Call = bad_node
# inline if statement ("x = 3 if y else z")
visit_IfExp = bad_node
# comprehensions and generator expressions
visit_ListComp = visit_SetComp = bad_node
visit_DictComp = visit_GeneratorExp = bad_node
# literals for advanced types
visit_Dict = visit_Set = bad_node
visit_List = visit_Tuple = bad_node
# "starred": "a = [1, 2, 3]; *a"
visit_Starred = bad_node
# allow ellipsis, for now
# visit_Ellipsis = bad_node
blacklist = DetectBadNodes()
blacklist.visit(module)
if blacklist.bad:
fail("Unsupported expression as default value: " + repr(default))
expr = module.body[0].value
# mild hack: explicitly support NULL as a default value
if isinstance(expr, ast.Name) and expr.id == 'NULL':
value = NULL
py_default = 'None'
c_default = "NULL"
elif (isinstance(expr, ast.BinOp) or
(isinstance(expr, ast.UnaryOp) and not isinstance(expr.operand, ast.Num))):
c_default = kwargs.get("c_default")
if not (isinstance(c_default, str) and c_default):
fail("When you specify an expression (" + repr(default) + ") as your default value,\nyou MUST specify a valid c_default.")
py_default = default
value = unknown
elif isinstance(expr, ast.Attribute):
a = []
n = expr
while isinstance(n, ast.Attribute):
a.append(n.attr)
n = n.value
if not isinstance(n, ast.Name):
fail("Unsupported default value " + repr(default) + " (looked like a Python constant)")
a.append(n.id)
py_default = ".".join(reversed(a))
c_default = kwargs.get("c_default")
if not (isinstance(c_default, str) and c_default):
fail("When you specify a named constant (" + repr(py_default) + ") as your default value,\nyou MUST specify a valid c_default.")
try:
value = eval(py_default)
except NameError:
value = unknown
else:
value = ast.literal_eval(expr)
py_default = repr(value)
if isinstance(value, (bool, None.__class__)):
c_default = "Py_" + py_default
elif isinstance(value, str):
c_default = '"' + quoted_for_c_string(value) + '"'
else:
c_default = py_default
except SyntaxError as e:
fail("Syntax error: " + repr(e.text))
except (ValueError, AttributeError):
value = unknown
c_default = kwargs.get("c_default") c_default = kwargs.get("c_default")
py_default = default
if not (isinstance(c_default, str) and c_default): if not (isinstance(c_default, str) and c_default):
fail("When you specify a named constant (" + repr(py_default) + ") as your default value,\nyou MUST specify a valid c_default.") fail("When you specify a named constant (" + repr(py_default) + ") as your default value,\nyou MUST specify a valid c_default.")
a = [] kwargs.setdefault('c_default', c_default)
n = expr kwargs.setdefault('py_default', py_default)
while isinstance(n, ast.Attribute):
a.append(n.attr)
n = n.value
if not isinstance(n, ast.Name):
fail("Malformed default value (looked like a Python constant)")
a.append(n.id)
py_default = ".".join(reversed(a))
kwargs["py_default"] = py_default
value = eval(py_default)
else:
value = ast.literal_eval(expr)
else:
value = unspecified
dict = legacy_converters if legacy else converters dict = legacy_converters if legacy else converters
legacy_str = "legacy " if legacy else "" legacy_str = "legacy " if legacy else ""
@ -2777,7 +2848,7 @@ class DSLParser:
if p.converter.is_optional(): if p.converter.is_optional():
a.append('=') a.append('=')
value = p.converter.default value = p.converter.default
a.append(p.converter.doc_default) a.append(p.converter.py_default)
s = fix_right_bracket_count(p.right_bracket_count) s = fix_right_bracket_count(p.right_bracket_count)
s += "".join(a) s += "".join(a)
if add_comma: if add_comma:
@ -2788,9 +2859,18 @@ class DSLParser:
add(fix_right_bracket_count(0)) add(fix_right_bracket_count(0))
add(')') add(')')
# if f.return_converter.doc_default: # PEP 8 says:
#
# The Python standard library will not use function annotations
# as that would result in a premature commitment to a particular
# annotation style. Instead, the annotations are left for users
# to discover and experiment with useful annotation styles.
#
# therefore this is commented out:
#
# if f.return_converter.py_default:
# add(' -> ') # add(' -> ')
# add(f.return_converter.doc_default) # add(f.return_converter.py_default)
docstring_first_line = output() docstring_first_line = output()
@ -2998,8 +3078,8 @@ def main(argv):
# print(" ", short_name + "".join(parameters)) # print(" ", short_name + "".join(parameters))
print() print()
print("All converters also accept (doc_default=None, required=False, annotation=None).") print("All converters also accept (c_default=None, py_default=None, annotation=None).")
print("All return converters also accept (doc_default=None).") print("All return converters also accept (py_default=None).")
sys.exit(0) sys.exit(0)
if ns.make: if ns.make:

View File

@ -231,20 +231,20 @@ xyz
self._test_clinic(""" self._test_clinic("""
verbatim text here verbatim text here
lah dee dah lah dee dah
/*[copy] /*[copy input]
def def
[copy]*/ [copy start generated code]*/
abc abc
/*[copy checksum: 03cfd743661f07975fa2f1220c5194cbaff48451]*/ /*[copy end generated code: checksum=03cfd743661f07975fa2f1220c5194cbaff48451]*/
xyz xyz
""", """ """, """
verbatim text here verbatim text here
lah dee dah lah dee dah
/*[copy] /*[copy input]
def def
[copy]*/ [copy start generated code]*/
def def
/*[copy checksum: 7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/ /*[copy end generated code: checksum=7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/
xyz xyz
""") """)
@ -292,17 +292,6 @@ os.access
p = function.parameters['path'] p = function.parameters['path']
self.assertEqual(1, p.converter.args['allow_fd']) self.assertEqual(1, p.converter.args['allow_fd'])
def test_param_docstring(self):
function = self.parse_function("""
module os
os.stat as os_stat_fn -> object(doc_default='stat_result')
path: str
Path to be examined""")
p = function.parameters['path']
self.assertEqual("Path to be examined", p.docstring)
self.assertEqual(function.return_converter.doc_default, 'stat_result')
def test_function_docstring(self): def test_function_docstring(self):
function = self.parse_function(""" function = self.parse_function("""
module os module os