Skip to content

Report __text_signature__ for methods, and keep it out of __doc__ - #8614

Draft
leehanjeong wants to merge 6 commits into
RustPython:mainfrom
leehanjeong:8383-method-text-signature
Draft

Report __text_signature__ for methods, and keep it out of __doc__#8614
leehanjeong wants to merge 6 commits into
RustPython:mainfrom
leehanjeong:8383-method-text-signature

Conversation

@leehanjeong

@leehanjeong leehanjeong commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

A generated signature and a function's documentation share one string, separated by \n--\n\n, as in CPython. Three defects in writing and reading that string left __doc__ carrying the signature and nearly every method carrying none.

before after
builtin functions whose __doc__ leaks the signature 44 / 46 0 / 46
native module functions reporting a __text_signature__ 630 / 791 775 / 791
method descriptors reporting a __text_signature__ 1 / 1101 1101 / 1101
of the 1030 methods CPython also describes, matching it exactly 0 713

What changed

__doc__ returned the whole string while __text_signature__ read only the front of it, so len.__doc__ was 'len(obj, /)\n--\n\nReturn the number of items in a container.'. get_doc_from_internal_doc already stripped the prefix but was reachable only from PyType.__doc__. Split it the way CPython does, into doc_without_signature (_PyType_DocWithoutSignature) and a wrapper mapping an empty result to None (_PyType_GetDocFromInternalDoc), and call it from PyNativeFunction and PyMethodDescriptor as well.

A #[pyfunction] with no doc comment stored the bare signature, with no marker for a reader to find, so time.clock_getres.__doc__ was 'clock_getres(clk_id, /)' and its __text_signature__ was None. Emit the marker in that case too, as Argument Clinic does for an undocumented function.

A #[pymethod] built its doc only when the method carried a doc comment, dropping the generated signature along with it. Build it from either part, as #[pyfunction] does.

A method taking its receiver as an ordinary first argument rather than &self reported that argument by name, so binding could not drop it: inspect.signature([].__dir__) was (obj, /) and is now (). Mark it $self, or $type for a classmethod. 310 methods were affected.

Notes

  • The 317 methods that still differ do so by parameter name (list.append reports x where CPython reports object) or because the method takes FuncArgs or a #[derive(FromArgs)] struct the generator cannot see into. That is the FromArgs gap Malformed builtin __text_signature__ makes inspect.signature(len/sorted/open) raise ValueError #8383 already tracks, so the issue stays open.
  • Lib/test/test_pydoc/test_pydoc.py drops the expectedFailure markers on test_unbound_builtin_method_noargs and test_bound_builtin_method_noargs, which pass now.
  • Slot wrappers keep no signature: PyWrapper has no __text_signature__ getter, and its docs are hand-written rather than generated, so there is nothing to strip.
  • #[pymethod] receivers are now uniformly named zelf, and func_sig's name check for it is gone. Both are cosmetic: the receiver is marked by position, so the generated signatures are identical before and after.

A function's signature and its documentation share one string, separated
by `\n--\n\n`, as in CPython. __text_signature__ read the front of it,
but __doc__ returned the whole thing:

    >>> len.__doc__
    'len(obj, /)\n--\n\nReturn the number of items in a container.'   # was
    'Return the number of items in a container.'                      # now

44 of the 46 builtin functions leaked the prefix.

get_doc_from_internal_doc already stripped it but was reachable only from
PyType.__doc__. Split it the way CPython does, into doc_without_signature
(_PyType_DocWithoutSignature) and a wrapper mapping an empty result to
None (_PyType_GetDocFromInternalDoc), and call it from PyNativeFunction
and PyMethodDescriptor as well.

Assisted-by: Claude Code:claude-opus-5
A signature and its documentation share one string, separated by
`\n--\n\n`, and readers find the signature by searching for that marker.
A #[pyfunction] with no doc comment stored the bare signature, which no
reader could parse:

    >>> time.clock_getres.__doc__
    'clock_getres(clk_id, /)'
    >>> time.clock_getres.__text_signature__
    None

Emit the marker in that case too, as Argument Clinic does for an
undocumented function. 136 of the 915 functions reachable from the
importable modules were affected.

__doc__ now reports None for them, matching CPython, because nothing
follows the marker.

Assisted-by: Claude Code:claude-opus-5
A #[pymethod] built its doc only when the method carried a doc comment,
so one without a comment dropped the generated signature too:

    >>> list.append.__text_signature__
    None

Build it from either part, as #[pyfunction] does. All 1101 method
descriptors reachable from the builtin types now report a signature, up
from 1, and 713 of the 1030 that CPython also describes match it exactly.

A method that takes its receiver as an ordinary first argument instead of
`&self` reported that argument by name, so binding the method could not
drop it:

    >>> inspect.signature([].__dir__)
    (obj, /)     # was
    ()           # now

Mark it $self, or $type for a classmethod, as CPython does. 310 methods
were affected.

test_unbound_builtin_method_noargs and test_bound_builtin_method_noargs
pass now.

Assisted-by: Claude Code:claude-opus-5
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the z-ca-2026 Tag to track Contribution Academy 2026 label Aug 30, 2026
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[ ] lib: cpython/Lib/concurrent
[ ] test: cpython/Lib/test/test_concurrent_futures (TODO: 4)
[ ] test: cpython/Lib/test/test_interpreters
[ ] test: cpython/Lib/test/test__interpreters.py
[ ] test: cpython/Lib/test/test__interpchannels.py
[ ] test: cpython/Lib/test/test_crossinterp.py

dependencies:

  • concurrent (native: _crossinterp, _interpqueues, _interpreters, _queues, concurrent.futures, concurrent.futures._base, interpreter, itertools, multiprocessing.connection, multiprocessing.queues, multiprocessing.synchronize, process, sys, thread, time)
    • logging (native: atexit, collections.abc, email.message, email.utils, errno, http.client, logging.handlers, multiprocessing.queues, select, sys, time, urllib.parse, win32evtlog, win32evtlogutil)
    • multiprocessing (native: _multiprocessing, _posixshmem, _posixsubprocess, _winapi, array, atexit, collections.abc, connection, context, dummy, errno, forkserver, heap, itertools, managers, mmap, msvcrt, multiprocessing.connection, pool, popen_fork, popen_forkserver, popen_spawn_posix, popen_spawn_win32, queues, resource_sharer, resource_tracker, sharedctypes, spawn, synchronize, sys, time, util, xmlrpc.client)
    • pickle (native: _pickle, itertools, sys)
    • collections, functools, os, queue, threading, traceback, types, weakref

dependent tests: (17 tests)

  • concurrent: test_asyncio test_compileall test_concurrent_futures test_context test_genericalias test_inspect test_struct test_sys test_threading test_types test_wmi
    • asyncio: test_asyncio test_external_inspection test_logging test_os test_pdb test_unittest

[x] lib: cpython/Lib/pydoc.py
[x] lib: cpython/Lib/pydoc_data
[ ] test: cpython/Lib/test/test_pydoc (TODO: 29)

dependencies:

  • pydoc

dependent tests: (5 tests)

  • pydoc: test_enum test_pydoc
    • pdb: test_pdb
    • xmlrpc.server: test_docxmlrpc test_xmlrpc

[x] test: cpython/Lib/test/test_cmd_line_script.py (TODO: 14)

dependencies:

dependent tests: (no tests depend on cmd_line_script)

[ ] test: cpython/Lib/test/test_import (TODO: 3)

dependencies:

dependent tests: (no tests depend on import)

[x] test: cpython/Lib/test/test_descr.py (TODO: 31)
[ ] test: cpython/Lib/test/test_descrtut.py (TODO: 3)

dependencies:

dependent tests: (no tests depend on descr)

[ ] test: cpython/Lib/test/test_class.py (TODO: 13)
[x] test: cpython/Lib/test/test_genericclass.py
[x] test: cpython/Lib/test/test_subclassinit.py

dependencies:

dependent tests: (no tests depend on class)

[ ] test: cpython/Lib/test/test_pyrepl (TODO: 21)
[ ] test: cpython/Lib/test/test_repl.py (TODO: 7)

dependencies:

dependent tests: (no tests depend on pyrepl)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone

Copy link
Copy Markdown
Member
======================================================================
UNEXPECTED SUCCESS: test_simple_completion (test.test_pyrepl.test_pyrepl.TestPyReplCompleter.test_simple_completion)
---------------------------------

rlcompleter closes the parenthesis for a callable whose signature takes
no parameters, and leaves it open when the signature cannot be read:

    def _callable_postfix(self, val, word):
        if callable(val):
            word += "("
            try:
                if not inspect.signature(val).parameters:
                    word += ")"
            except ValueError:
                pass

os.getpid reports `()` since the generated signature carries the `--`
terminator, so completing `os.getpid` yields `os.getpid()` and the test
passes.

Assisted-by: Claude Code:claude-opus-5
A method that needs an owned handle cannot take `&self`, so it takes the
receiver as its first argument and names it `zelf`, `self` being a Rust
keyword. 24 of the 163 such methods named it something else: `instance`
(11), `obj` (4), `exc` (3), `_self` (3), `_instance` (2).

Rename them. The generated signature is unaffected either way, since the
macro marks the receiver by position rather than by name.

Assisted-by: Claude Code:claude-opus-5
The receiver is now marked by position, so the name it carries no longer
matters, and no path reaches this check with an argument named zelf:

- a #[pymethod] or #[pyclassmethod] without a `&self` receiver has its
  first argument replaced by the marker before the name is read
- a `&self` receiver is handled as syn::FnArg::Receiver
- a #[pymethod(raw)] is passed to static_raw_func, whose PyNativeFn bound
  fixes its signature to (&VirtualMachine, FuncArgs)
- no #[pystaticmethod] or #[pyfunction] names an argument zelf
- #[pymember], #[pygetset] and #[pyslot] never reach func_sig

Every generated signature is byte for byte identical without it.

Assisted-by: Claude Code:claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants