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 selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review. 📝 WalkthroughWalkthroughBuiltin signature generation now supports missing signatures and positional-only formatting. Method generation 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 Merge Risk: ⚪ Minimal · up to This PR corrects builtin signature metadata and related tests without introducing a concrete merge-blocking correctness, security, availability, or deployment risk; no actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant Builtins as Builtin definitions
participant Generator as Signature generator
participant Methods as Method generation
participant Inspect as inspect.signature
Builtins->>Generator: Provide Rust function signature
Generator-->>Methods: Return text signature or None
Methods->>Methods: Combine signature and source documentation
Methods-->>Inspect: Expose optional documentation and signature metadata
Inspect-->>Builtins: Bind positional-only and variadic parameters
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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: [ ] lib: cpython/Lib/pydoc.py dependencies:
dependent tests: (5 tests)
Legend:
|
|
@moreal the OSCCA job is failing |
youknowone
left a comment
There was a problem hiding this comment.
great improvements, thanks!
The merge of main took ord's signature from this branch, which renamed the parameter to character, and its body from main, which rewrote ord to accept bytes and bytearray through a parameter named c. The body then referenced a name that no longer existed and the build failed. Assisted-by: Claude Code:claude-opus-5
Head branch was pushed to by a user without write access
#[pyfunction]/#[pymethod] derive __text_signature__ from the Rust parameter list, and a function that takes FuncArgs to check its own arity has no parameters to report, so func_sig emits "(*args, **kwargs)". Every builtin this branch rewrote that way - len, abs, hash, chr, callable, bin, ord, divmod, isinstance, issubclass and the rest of the 27 - stopped reporting the signature that RustPython#8512 had just made accurate: inspect.signature(len) (*args, **kwargs) # was (obj, /) Add `text_signature = "..."`, which overrides the derived parameter list, and give the affected builtins CPython's own, verified against CPython 3.14.7. round declares (number, ndigits=None) and so has a signature now, where before its destructuring pattern left it with none; builtin_signature.py keeps sum as the signature-less case and asserts round's instead. The derived signature is still used wherever no override is given, so genuinely variadic builtins such as breakpoint keep reporting (*args, **kwargs). Assisted-by: Claude:Claude Opus 5
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