builtins: generate accurate __text_signature__ - #8512
Conversation
CPython's C functions receive the module as their first argument, so
PyCFunction.__self__ is the module and inspect strips the $module
parameter when building a Signature. A #[pyfunction] takes no such
argument, PyNativeFunction::zelf is None, and inspect has nothing to
strip, so the marker surfaced as a parameter that does not exist:
inspect.signature(len)
(module, /, obj) # was
(obj) # now
All 45 builtins shared with CPython carried it. Methods are unaffected;
their $self marker comes from func_sig and both branches now produce the
same string.
Assisted-by: Claude Code:claude-opus-5
Arguments bind through `FuncArgs::take_positional`, which pops from the
positional list and never consults the keyword map, so a #[pyfunction]
argument cannot be passed by name:
>>> len(obj=[1, 2])
TypeError
The generated signature omitted the `/` marker, so inspect reported those
parameters as POSITIONAL_OR_KEYWORD, contradicting the call above. Emit
the marker, except for `*args`/`**kwargs`, which cannot be followed by
`/`, and for empty parameter lists.
14 of the 45 builtins shared with CPython now report an identical
signature, up from 0.
Assisted-by: Claude Code:claude-opus-5
Arguments bound by a destructuring pattern, e.g.
fn round(RoundArgs { number, ndigits }: RoundArgs, ..)
have no name to report, and func_sig stringified the pattern verbatim:
>>> round.__text_signature__
'($module, RoundArgs { number, ndigits })'
That is not valid Python, so inspect.signature() raised "builtin has
invalid signature". Return None instead, which leaves
__text_signature__ unset and makes inspect raise "no signature found",
the same as for a CPython builtin that has no signature.
Affects round, sum, os.pathconf, binascii.b2a_base64 and
binascii.b2a_uu. Their docstrings are unchanged; only the signature
prefix is dropped.
Assisted-by: Claude Code:claude-opus-5
These parameters are positional-only, so their names only ever appear in
__text_signature__ and cannot be used at a call site. Naming them after
CPython makes the generated signatures directly comparable:
bin x -> number
ord string -> character
divmod a, b -> x, y
setattr attr -> name
delattr attr -> name
hasattr attr -> name
isinstance typ -> class_or_tuple
issubclass subclass,typ -> cls, class_or_tuple
aiter iter_target -> async_iterable
23 of the 45 builtins shared with CPython now report an identical
signature, up from 0 before this branch. The remainder need FromArgs to
report the parameters of its own structs, which is left for a follow-up.
Add extra_tests/snippets/builtin_signature.py covering the phantom
module parameter, the positional-only marker, the names above, and the
signature-less builtins.
Assisted-by: Claude Code:claude-opus-5
pydoc's summary line for time.time was "time(module)" because the generated signature carried a $module parameter that inspect could not strip. It now reads "time()", as the test expects. Assisted-by: Claude Code:claude-opus-5
test_snippets runs every snippet under CPython as well, and CPython does have Argument Clinic signatures for round and sum, so that block only holds for RustPython. Assisted-by: Claude Code:claude-opus-5
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
📝 WalkthroughWalkthroughBuiltin signature generation now supports missing signatures and positional-only formatting. Method generation now preserves available documentation and emits no documentation when none exists. Builtin parameter names and signature tests were updated. ChangesBuiltin signature and documentation handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Mergeability Score: ⚪ Minimal · up to This PR improves builtin signature metadata and related tests without any identified merge-blocking risk; it is merge-ready after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_set.py (TODO: 4) dependencies: dependent tests: (no tests depend on set) [x] test: cpython/Lib/test/test_marshal.py (TODO: 15) dependencies: dependent tests: (25 tests)
[ ] lib: cpython/Lib/typing.py dependencies:
dependent tests: (19 tests)
[ ] lib: cpython/Lib/pydoc.py dependencies:
dependent tests: (5 tests)
[x] test: cpython/Lib/test/test_itertools.py (TODO: 6) dependencies: dependent tests: (56 tests)
Legend:
|
|
@moreal the OSCCA job is failing |
Problem
RustPython's
#[pyfunction]s generate a__text_signature__that isn't comparable to CPython's. Of the 45 builtin functions RustPython shares with CPython 3.14, none reported an identicalinspect.signature()before this PR:moduleparameter that doesn't exist.round,sum) emitted a signature string that isn't valid Python, soinspect.signature()raisedValueError: builtin has invalid signature. The same applies toos.pathconf,binascii.b2a_base64andbinascii.b2a_uuin the wider stdlib.Tools that rely on
inspect.signature()(unittest.mock.autospec,pydoc, IDE completion) act on this bad metadata.Scope
This PR fixes everything reachable by changing
crates/derive-impl/src/util.rs's signature generator alone, without touchingFromArgsor adding new types. The#[derive(FromArgs)]struct case is deliberately left out, so #8383 stays open after this merges.What changed
$modulemarker. CPython's C functions receive the module as__self__, soinspectstrips a leading$module. RustPython's#[pyfunction]s take no module argument, so__self__is alwaysNoneand there was nothing to strip. The marker surfaced as a parameter that doesn't exist (len(module, /, obj)instead oflen(obj)).FuncArgs::take_positional, which never consults the keyword map:len(obj=[1, 2])already raisedTypeError, butinspectreportedobjasPOSITIONAL_OR_KEYWORD. Generated signatures now carry the/marker, except for*args/**kwargsand empty parameter lists, which cannot have one.fn round(RoundArgs { number, ndigits }: RoundArgs, ..)). The generator used to stringify the Rust pattern verbatim, producing text that isn't valid Python.inspect.signature()still raisesValueErrorfor these, but now withno signature found for builtin, matching how CPython reports a builtin it has no signature for, rather thanbuiltin has invalid signature.bin,ord,divmod,setattr,delattr,hasattr,isinstance,issubclass,aiter). All positional-only, so the name is documentation only and renaming doesn't change behavior.test_module_level_callable_noargsinLib/test/test_pydoc/test_pydoc.pynow passes for real (the phantommoduleparameter was the cause), so itsexpectedFailuremarker is dropped. The new snippet's "no signature" assertions are guarded to RustPython, since CPython has real Argument Clinic signatures forround/sum.Before / after
Measured across the 45 builtin functions RustPython and CPython 3.14 share:
ValueErrormoduleinvalid signature)no signature found)The 22 that still differ fall into two groups that need different work.
CPython documents a signature we could match (13). Twelve of these hold their arguments in a type the signature generator can't see into: a
#[derive(FromArgs)]struct (__import__,compile,eval,exec,open,pow,print,sorted, andround/sum, which now report no signature at all) or anOptionalArgwhose default lives in the function body (format,input). The thirteenth,breakpoint, differs only because CPython names its keyword catch-all**kwsand the generator hardcodes**kwargsfor everyFuncArgsfunction.CPython has no signature at all, and RustPython reports one (9).
__build_class__,anext,dir,getattr,iter,max,min,nextandvars. TeachingFromArgsto report its parameters would not close this gap, since the generated signature is not what's wrong: matching CPython here means deciding to suppress a signature we are able to produce.test_autospec_on_bound_builtin_functionstaysexpectedFailurefor exactly this reason, viatime.ctime.Not in this PR
Two follow-ups would close most of the first group above:
FromArgsreporting its own parameters. Implementors would report the parameters they consume, mirroring howarity()already self-reports parameter count. The information is already there in each field's#[pyarg(...)]attribute; it just isn't reachable from the signature generator today.OptionalArg's hidden defaults, which the same mechanism would have to carry.The second group needs a separate decision about whether RustPython should suppress signatures CPython doesn't publish, which seems worth settling before either follow-up.
Two unrelated problems turned up while investigating, both out of scope here:
__doc__carries the raw signature prefix.len.__doc__is'len(obj, /)\n--\n\nReturn the number of items in a container.'.get_doc_from_internal_doc(type.rs) strips this, but is only wired toPyType.__doc__, not tobuiltin_func.rsordescriptor.rs.pyclass.rsonly attaches a generated signature when the method already has a doc comment, solist.append,str.splitanddict.getall report__text_signature__ofNone. This PR touches the#[pymethod]path only enough to keep it compiling.Test plan
extra_tests/snippets/builtin_signature.py, run under both CPython and RustPython.cargo run --release -- -m test test_inspect test_pydoc test_unittestcargo test -p rustpython-derive-implprek run --all-filesDeveloped with assistance from Claude Code (claude-opus-5)
Summary by CodeRabbit
Bug Fixes
moduleparameters from displayed signatures.Tests