diff --git a/Lib/inspect.py b/Lib/inspect.py --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -31,6 +31,7 @@ __author__ = ('Ka-Ping Yee ', 'Yury Selivanov ') +import ast import importlib.machinery import itertools import linecache @@ -1461,6 +1462,9 @@ if isinstance(obj, types.FunctionType): return Signature.from_function(obj) + if isinstance(obj, types.BuiltinFunctionType): + return Signature.from_builtin(obj) + if isinstance(obj, functools.partial): sig = signature(obj.func) @@ -1942,6 +1946,64 @@ return_annotation=annotations.get('return', _empty), __validate_parameters__=False) + @classmethod + def from_builtin(cls, func): + s = getattr(func, "__text_signature__", None) + if not s: + return None + + if s.endswith("/)"): + kind = Parameter.POSITIONAL_ONLY + s = s[:-2] + ')' + else: + kind = Parameter.POSITIONAL_OR_KEYWORD + + s = "def foo" + s + ": pass" + + try: + module = ast.parse(s) + except SyntaxError: + return None + if not isinstance(module, ast.Module): + return None + + # ast.FunctionDef + f = module.body[0] + + parameters = [] + empty = Parameter.empty + + def p(name_node, default_node, default=empty): + name = name_node.arg + + if isinstance(default_node, ast.Num): + default = default.n + elif isinstance(default_node, ast.NameConstant): + default = default_node.value + parameters.append(Parameter(name, kind, default=default, annotation=empty)) + + # non-keyword-only parameters + for name, default in reversed(list(itertools.zip_longest(reversed(f.args.args), reversed(f.args.defaults), fillvalue=None))): + p(name, default) + + # *args + if f.args.vararg: + kind = Parameter.VAR_POSITIONAL + p(f.args.vararg, empty) + + # keyword-only arguments + kind = Parameter.KEYWORD_ONLY + for name, default in zip(f.args.kwonlyargs, f.args.kw_defaults): + p(name, default) + + # **kwargs + if f.args.kwarg: + kind = Parameter.VAR_KEYWORD + p(f.args.kwarg, empty) + + return cls(parameters, return_annotation=cls.empty) + + @property def parameters(self): return self._parameters diff --git a/Lib/pydoc.py b/Lib/pydoc.py --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -916,20 +916,18 @@ reallink = realname title = '%s = %s' % ( anchor, name, reallink) - if inspect.isfunction(object): - args, varargs, kwonlyargs, kwdefaults, varkw, defaults, ann = \ - inspect.getfullargspec(object) - argspec = inspect.formatargspec( - args, varargs, kwonlyargs, kwdefaults, varkw, defaults, ann, - formatvalue=self.formatvalue, - formatannotation=inspect.formatannotationrelativeto(object)) - if realname == '': - title = '%s lambda ' % name - # XXX lambda's won't usually have func_annotations['return'] - # since the syntax doesn't support but it is possible. - # So removing parentheses isn't truly safe. - argspec = argspec[1:-1] # remove parentheses - else: + argspec = None + if inspect.isfunction(object) or inspect.isbuiltin(object): + signature = inspect.signature(object) + if signature: + argspec = str(signature) + if realname == '': + title = '%s lambda ' % name + # XXX lambda's won't usually have func_annotations['return'] + # since the syntax doesn't support but it is possible. + # So removing parentheses isn't truly safe. + argspec = argspec[1:-1] # remove parentheses + if not argspec: argspec = '(...)' decl = title + argspec + (note and self.grey( @@ -1313,20 +1311,18 @@ cl.__dict__[realname] is object): skipdocs = 1 title = self.bold(name) + ' = ' + realname - if inspect.isfunction(object): - args, varargs, varkw, defaults, kwonlyargs, kwdefaults, ann = \ - inspect.getfullargspec(object) - argspec = inspect.formatargspec( - args, varargs, varkw, defaults, kwonlyargs, kwdefaults, ann, - formatvalue=self.formatvalue, - formatannotation=inspect.formatannotationrelativeto(object)) - if realname == '': - title = self.bold(name) + ' lambda ' - # XXX lambda's won't usually have func_annotations['return'] - # since the syntax doesn't support but it is possible. - # So removing parentheses isn't truly safe. - argspec = argspec[1:-1] # remove parentheses - else: + argspec = None + if inspect.isfunction(object) or inspect.isbuiltin(object): + signature = inspect.signature(object) + if signature: + argspec = str(signature) + if realname == '': + title = self.bold(name) + ' lambda ' + # XXX lambda's won't usually have func_annotations['return'] + # since the syntax doesn't support but it is possible. + # So removing parentheses isn't truly safe. + argspec = argspec[1:-1] # remove parentheses + if not argspec: argspec = '(...)' decl = title + argspec + note diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -109,6 +109,35 @@ self.assertRaises(TypeError, _posixsubprocess.fork_exec, Z(),[b'1'],3,[1, 2],5,6,7,8,9,10,11,12,13,14,15,16,17) + def test_docstring_signature_parsing(self): + + self.assertEqual(_testcapi.no_docstring.__doc__, None) + self.assertEqual(_testcapi.no_docstring.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_empty.__doc__, "") + self.assertEqual(_testcapi.docstring_empty.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_no_signature.__doc__, + "This docstring has no signature.") + self.assertEqual(_testcapi.docstring_no_signature.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_with_invalid_signature.__doc__, + "docstring_with_invalid_signature (boo)\n" + "\n" + "This docstring has an invalid siganture." + ) + self.assertEqual(_testcapi.docstring_with_invalid_signature.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_with_signature.__doc__, + "This docstring has a valid siganture.") + self.assertEqual(_testcapi.docstring_with_signature.__text_signature__, "(sig)") + + self.assertEqual(_testcapi.docstring_with_signature_and_extra_newlines.__doc__, + "This docstring has a valid siganture and some extra newlines.") + self.assertEqual(_testcapi.docstring_with_signature_and_extra_newlines.__text_signature__, + "(parameter)") + + @unittest.skipUnless(threading, 'Threading required for this test.') class TestPendingCalls(unittest.TestCase): diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -1588,10 +1588,9 @@ with self.assertRaisesRegex(ValueError, 'not supported by signature'): # support for 'method-wrapper' inspect.signature(min.__call__) - with self.assertRaisesRegex(ValueError, - 'no signature found for builtin function'): - # support for 'method-wrapper' - inspect.signature(min) + self.assertEqual(inspect.signature(min), None) + signature = inspect.signature(os.stat) + self.assertTrue(isinstance(signature, inspect.Signature)) def test_signature_on_non_function(self): with self.assertRaisesRegex(TypeError, 'is not a callable object'): diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,9 @@ Core and Builtins ----------------- +- Issue #19674: inspect.signature() now produces a correct signature + for some builtins. + - Use the repr of a module name in more places in import, especially exceptions. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -580,9 +580,8 @@ [clinic]*/ PyDoc_STRVAR(curses_window_addch__doc__, +"addch([x, y,] ch, [attr])\n" "Paint character ch at (y, x) with attributes attr.\n" -"\n" -"curses.window.addch([x, y,] ch, [attr])\n" " x\n" " X-coordinate.\n" " y\n" @@ -592,6 +591,7 @@ " attr\n" " Attributes for the character.\n" "\n" +"\n" "Paint character ch at (y, x) with attributes attr,\n" "overwriting any character previously painted at that location.\n" "By default, the character position and attributes are the\n" @@ -646,7 +646,7 @@ static PyObject * curses_window_addch_impl(PyObject *self, int group_left_1, int x, int y, PyObject *ch, int group_right_1, long attr) -/*[clinic checksum: 094d012af1019387c0219a9c0bc76e90729c833f]*/ +/*[clinic checksum: 59cec2236010d3c7aea0ab8a7e3679e60d592546]*/ { PyCursesWindowObject *cwself = (PyCursesWindowObject *)self; int coordinates_group = group_left_1; diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -4155,12 +4155,12 @@ [clinic]*/ PyDoc_STRVAR(datetime_datetime_now__doc__, +"now(tz=None)\n" "Returns new datetime object representing current time local to tz.\n" -"\n" -"datetime.datetime.now(tz=None)\n" " tz\n" " Timezone object.\n" "\n" +"\n" "If no tz is specified, uses local timezone."); #define DATETIME_DATETIME_NOW_METHODDEF \ @@ -4188,7 +4188,7 @@ static PyObject * datetime_datetime_now_impl(PyObject *cls, PyObject *tz) -/*[clinic checksum: cde1daca68c9b7dca6df51759db2de1d43a39774]*/ +/*[clinic checksum: a6c9a8139e693fc989940b52a46376400958db20]*/ { PyObject *self; diff --git a/Modules/_dbmmodule.c b/Modules/_dbmmodule.c --- a/Modules/_dbmmodule.c +++ b/Modules/_dbmmodule.c @@ -400,9 +400,8 @@ [clinic]*/ PyDoc_STRVAR(dbmopen__doc__, +"open(filename, flags=\'r\', mode=0o666)\n" "Return a database object.\n" -"\n" -"dbm.open(filename, flags=\'r\', mode=0o666)\n" " filename\n" " The filename to open.\n" " flags\n" @@ -437,7 +436,7 @@ static PyObject * dbmopen_impl(PyObject *module, const char *filename, const char *flags, int mode) -/*[clinic checksum: 2b0ec9e3c6ecd19e06d16c9f0ba33848245cb1ab]*/ +/*[clinic checksum: ffedcffaeb679fca363aa61aee58511aa1850e2a]*/ { int iflags; diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2842,6 +2842,33 @@ return test_setallocators(PYMEM_DOMAIN_OBJ); } +PyDoc_STRVAR(docstring_empty, +"" +); + +PyDoc_STRVAR(docstring_no_signature, +"This docstring has no signature." +); + +PyDoc_STRVAR(docstring_with_invalid_signature, +"docstring_with_invalid_signature (boo)\n" +"\n" +"This docstring has an invalid siganture." +); + +PyDoc_STRVAR(docstring_with_signature, +"docstring_with_signature(sig)\n" +"This docstring has a valid siganture." +); + +PyDoc_STRVAR(docstring_with_signature_and_extra_newlines, +"docstring_with_signature_and_extra_newlines(parameter)\n" +"\n" +"\n" +"\n" +"This docstring has a valid siganture and some extra newlines." +); + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, {"raise_memoryerror", (PyCFunction)raise_memoryerror, METH_NOARGS}, @@ -2953,6 +2980,23 @@ (PyCFunction)test_pymem_setallocators, METH_NOARGS}, {"test_pyobject_setallocators", (PyCFunction)test_pyobject_setallocators, METH_NOARGS}, + {"no_docstring", + (PyCFunction)test_with_docstring, METH_NOARGS}, + {"docstring_empty", + (PyCFunction)test_with_docstring, METH_NOARGS, + docstring_empty}, + {"docstring_no_signature", + (PyCFunction)test_with_docstring, METH_NOARGS, + docstring_no_signature}, + {"docstring_with_invalid_signature", + (PyCFunction)test_with_docstring, METH_NOARGS, + docstring_with_invalid_signature}, + {"docstring_with_signature", + (PyCFunction)test_with_docstring, METH_NOARGS, + docstring_with_signature}, + {"docstring_with_signature_and_extra_newlines", + (PyCFunction)test_with_docstring, METH_NOARGS, + docstring_with_signature_and_extra_newlines}, {NULL, NULL} /* sentinel */ }; diff --git a/Modules/_weakref.c b/Modules/_weakref.c --- a/Modules/_weakref.c +++ b/Modules/_weakref.c @@ -17,9 +17,8 @@ [clinic]*/ PyDoc_STRVAR(_weakref_getweakrefcount__doc__, -"Return the number of weak references to \'object\'.\n" -"\n" -"_weakref.getweakrefcount(object)"); +"getweakrefcount(object)\n" +"Return the number of weak references to \'object\'."); #define _WEAKREF_GETWEAKREFCOUNT_METHODDEF \ {"getweakrefcount", (PyCFunction)_weakref_getweakrefcount, METH_O, _weakref_getweakrefcount__doc__}, @@ -43,7 +42,7 @@ static Py_ssize_t _weakref_getweakrefcount_impl(PyObject *module, PyObject *object) -/*[clinic checksum: 05cffbc3a4b193a0b7e645da81be281748704f69]*/ +/*[clinic checksum: 50b243d3debe4f6ef45e43ae2503dcb6dcda9b39]*/ { PyWeakReference **list; diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -2435,9 +2435,8 @@ [clinic]*/ PyDoc_STRVAR(os_stat__doc__, +"stat(path, *, dir_fd=None, follow_symlinks=True)\n" "Perform a stat system call on the given path.\n" -"\n" -"os.stat(path, *, dir_fd=None, follow_symlinks=True) -> stat_result\n" " path\n" " Path to be examined; can be string, bytes, or open-file-descriptor int.\n" " dir_fd\n" @@ -2449,6 +2448,7 @@ " stat will examine the symbolic link itself instead of the file\n" " the link points to.\n" "\n" +"\n" "dir_fd and follow_symlinks may not be implemented\n" " on your platform. If they are unavailable, using them will raise a\n" " NotImplementedError.\n" @@ -2486,7 +2486,7 @@ static PyObject * os_stat_impl(PyObject *module, path_t *path, int dir_fd, int follow_symlinks) -/*[clinic checksum: 89390f78327e3f045a81974d758d3996e2a71f68]*/ +/*[clinic checksum: a6bbcb8ae6424694d095a63692cf0a3df565ceba]*/ { return posix_do_stat("stat", path, dir_fd, follow_symlinks); } @@ -2567,9 +2567,9 @@ [clinic]*/ PyDoc_STRVAR(os_access__doc__, +"access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True)\n" "Use the real uid/gid to test for access to a path.\n" "\n" -"os.access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True) -> True if granted, False otherwise\n" " path\n" " Path to be tested; can be string, bytes, or open-file-descriptor int.\n" " mode\n" @@ -2586,8 +2586,6 @@ " If False, and the last element of the path is a symbolic link,\n" " access will examine the symbolic link itself instead of the file\n" " the link points to.\n" -"\n" -"{parameters}\n" "dir_fd, effective_ids, and follow_symlinks may not be implemented\n" " on your platform. If they are unavailable, using them will raise a\n" " NotImplementedError.\n" @@ -2628,7 +2626,7 @@ static PyObject * os_access_impl(PyObject *module, path_t *path, int mode, int dir_fd, int effective_ids, int follow_symlinks) -/*[clinic checksum: aa3e145816a748172e62df8e44af74169c7e1247]*/ +/*[clinic checksum: 82ea56c89336cf3575f6c0a3fecbe72b3720fe6f]*/ { PyObject *return_value = NULL; @@ -2724,9 +2722,8 @@ [clinic]*/ PyDoc_STRVAR(os_ttyname__doc__, +"ttyname(fd)\n" "Return the name of the terminal device connected to \'fd\'.\n" -"\n" -"os.ttyname(fd)\n" " fd\n" " Integer file descriptor handle."); @@ -2758,7 +2755,7 @@ static char * os_ttyname_impl(PyObject *module, int fd) -/*[clinic checksum: c742dd621ec98d0f81d37d264e1d3c89c7a5fb1a]*/ +/*[clinic checksum: f59199639ce9e9ef38a4a32ff0f0335ec6f30a5a]*/ { char *ret; diff --git a/Modules/unicodedata.c b/Modules/unicodedata.c --- a/Modules/unicodedata.c +++ b/Modules/unicodedata.c @@ -124,9 +124,9 @@ [clinic]*/ PyDoc_STRVAR(unicodedata_UCD_decimal__doc__, +"decimal(unichr, default=None)\n" "Converts a Unicode character into its equivalent decimal value.\n" "\n" -"unicodedata.UCD.decimal(unichr, default=None)\n" "\n" "Returns the decimal value assigned to the Unicode character unichr\n" "as integer. If no such value is defined, default is returned, or, if\n" @@ -157,7 +157,7 @@ static PyObject * unicodedata_UCD_decimal_impl(PyObject *self, PyObject *unichr, PyObject *default_value) -/*[clinic checksum: a0980c387387287e2ac230c37d95b26f6903e0d2]*/ +/*[clinic checksum: a0167f5301c420400f3e0add31abe146d0a4025f]*/ { PyUnicodeObject *v = (PyUnicodeObject *)unichr; int have_old = 0; diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -704,9 +704,8 @@ [clinic]*/ PyDoc_STRVAR(zlib_Decompress_decompress__doc__, +"decompress(data, max_length=0)\n" "Return a string containing the decompressed version of the data.\n" -"\n" -"zlib.Decompress.decompress(data, max_length=0)\n" " data\n" " The binary data to decompress.\n" " max_length\n" @@ -714,6 +713,7 @@ " Unconsumed input data will be stored in\n" " the unconsumed_tail attribute.\n" "\n" +"\n" "After calling this function, some of the input data may still be stored in\n" "internal buffers for later processing.\n" "Call the flush() method to clear these buffers."); @@ -746,7 +746,7 @@ static PyObject * zlib_Decompress_decompress_impl(PyObject *self, Py_buffer *data, unsigned int max_length) -/*[clinic checksum: 76ca9259e3f5ca86bae9da3d0e75637b5d492234]*/ +/*[clinic checksum: d90e7e3c1f37cd7886d0e21392457a145e6c97e4]*/ { compobject *zself = (compobject *)self; int err; @@ -974,16 +974,15 @@ [clinic]*/ PyDoc_STRVAR(zlib_Compress_copy__doc__, -"Return a copy of the compression object.\n" -"\n" -"zlib.Compress.copy()"); +"copy()\n" +"Return a copy of the compression object."); #define ZLIB_COMPRESS_COPY_METHODDEF \ {"copy", (PyCFunction)zlib_Compress_copy, METH_NOARGS, zlib_Compress_copy__doc__}, static PyObject * zlib_Compress_copy(PyObject *self) -/*[clinic checksum: 2551952e72329f0f2beb48a1dde3780e485a220b]*/ +/*[clinic checksum: 8d30351f05defbc2b335c2a78d18f07aa367bb1d]*/ { compobject *zself = (compobject *)self; compobject *retval = NULL; diff --git a/Objects/dictobject.c b/Objects/dictobject.c --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2172,16 +2172,15 @@ [clinic]*/ PyDoc_STRVAR(dict___contains____doc__, -"True if D has a key k, else False\"\n" -"\n" -"dict.__contains__(key)"); +"__contains__(key)\n" +"True if D has a key k, else False\""); #define DICT___CONTAINS___METHODDEF \ {"__contains__", (PyCFunction)dict___contains__, METH_O|METH_COEXIST, dict___contains____doc__}, static PyObject * dict___contains__(PyObject *self, PyObject *key) -/*[clinic checksum: 61c5c802ea1d35699a1a754f1f3538ea9b259cf4]*/ +/*[clinic checksum: 3bbac5ce898ae630d9668fa1c8b3afb645ff22e8]*/ { register PyDictObject *mp = (PyDictObject *)self; Py_hash_t hash; diff --git a/Objects/methodobject.c b/Objects/methodobject.c --- a/Objects/methodobject.c +++ b/Objects/methodobject.c @@ -159,15 +159,75 @@ } } +/* + * finds the docstring's introspection signature. + * if present, returns a pointer pointing to the first '('. + * otherwise returns NULL. + */ +static const char *find_signature(PyCFunctionObject *m) +{ + const char *trace = m->m_ml->ml_doc; + const char *name = m->m_ml->ml_name; + size_t length; + if (!trace || !name) + return NULL; + length = strlen(name); + if (strncmp(trace, name, length)) + return NULL; + trace += length; + if (*trace != '(') + return NULL; + return trace; +} + +/* + * skips to the end of the docstring's instrospection signature. + */ +static const char *skip_signature(const char *trace) +{ + while (*trace && *trace != '\n') + trace++; + return trace; +} + +static const char *skip_eols(const char *trace) +{ + while (*trace == '\n') + trace++; + return trace; +} + +static PyObject * +meth_get__text_signature__(PyCFunctionObject *m, void *closure) +{ + const char *start = find_signature(m); + const char *trace; + + if (!start) { + Py_INCREF(Py_None); + return Py_None; + } + + trace = skip_signature(start); + return PyUnicode_FromStringAndSize(start, trace - start); +} + static PyObject * meth_get__doc__(PyCFunctionObject *m, void *closure) { - const char *doc = m->m_ml->ml_doc; + const char *doc = find_signature(m); - if (doc != NULL) - return PyUnicode_FromString(doc); - Py_INCREF(Py_None); - return Py_None; + if (doc) + doc = skip_eols(skip_signature(doc)); + else + doc = m->m_ml->ml_doc; + + if (!doc) { + Py_INCREF(Py_None); + return Py_None; + } + + return PyUnicode_FromString(doc); } static PyObject * @@ -236,6 +296,7 @@ {"__name__", (getter)meth_get__name__, NULL, NULL}, {"__qualname__", (getter)meth_get__qualname__, NULL, NULL}, {"__self__", (getter)meth_get__self__, NULL, NULL}, + {"__text_signature__", (getter)meth_get__text_signature__, NULL, NULL}, {0} }; diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -12908,9 +12908,9 @@ [clinic]*/ PyDoc_STRVAR(unicode_maketrans__doc__, +"maketrans(x, y=None, z=None)\n" "Return a translation table usable for str.translate().\n" "\n" -"str.maketrans(x, y=None, z=None)\n" "\n" "If there is only one argument, it must be a dictionary mapping Unicode\n" "ordinals (integers) or characters to Unicode ordinals, strings or None.\n" @@ -12946,7 +12946,7 @@ static PyObject * unicode_maketrans_impl(PyObject *x, PyObject *y, PyObject *z) -/*[clinic checksum: 137db9c3199e7906b7967009f511c24fa3235b5f]*/ +/*[clinic checksum: fc0df184eab52c3b26fac97f412cc12413d47578]*/ { PyObject *new = NULL, *key, *value; Py_ssize_t i = 0; diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -2251,7 +2251,7 @@ ## docstring first line ## - add(f.full_name) + add(f.name) add('(') # populate "right_bracket_count" field for every parameter @@ -2308,29 +2308,30 @@ add(fix_right_bracket_count(0)) add(')') - if f.return_converter.doc_default: - add(' -> ') - add(f.return_converter.doc_default) + # if f.return_converter.doc_default: + # add(' -> ') + # add(f.return_converter.doc_default) docstring_first_line = output() # now fix up the places where the brackets look wrong docstring_first_line = docstring_first_line.replace(', ]', ',] ') - # okay. now we're officially building the - # "prototype" section. - add(docstring_first_line) - + # okay. now we're officially building the "parameters" section. # create substitution text for {parameters} + spacer_line = False for p in parameters: if not p.docstring.strip(): continue - add('\n') + if spacer_line: + add('\n') + else: + spacer_line = True add(" ") add(p.name) add('\n') add(textwrap.indent(rstrip_lines(p.docstring.rstrip()), " ")) - prototype = output() + parameters = output() ## ## docstring body @@ -2359,21 +2360,26 @@ elif len(lines) == 1: # the docstring is only one line right now--the summary line. # add an empty line after the summary line so we have space - # between it and the {prototype} we're about to add. + # between it and the {parameters} we're about to add. lines.append('') - prototype_marker_count = len(docstring.split('{prototype}')) - 1 - if prototype_marker_count: - fail('You may not specify {prototype} in a docstring!') - # insert *after* the summary line - lines.insert(2, '{prototype}\n') + parameters_marker_count = len(docstring.split('{parameters}')) - 1 + if parameters_marker_count > 1: + fail('You may not specify {parameters} more than once in a docstring!') + + # insert at front of docstring + lines.insert(0, docstring_first_line) + + if not parameters_marker_count: + # insert after summary line + lines.insert(2, '{parameters}\n') docstring = "\n".join(lines) add(docstring) docstring = output() - docstring = linear_format(docstring, prototype=prototype) + docstring = linear_format(docstring, parameters=parameters) docstring = docstring.rstrip() return docstring