Skip to content

Commit b8b7e33

Browse files
derive: let a function declare its own __text_signature__
#[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 #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
1 parent 7af100d commit b8b7e33

5 files changed

Lines changed: 42 additions & 23 deletions

File tree

crates/derive-impl/src/pyclass.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,10 @@ where
10771077
}
10781078

10791079
let raw = item_meta.raw()?;
1080-
let sig_doc = text_signature(func.sig(), &py_name);
1080+
let sig_doc = match item_meta.explicit_text_signature()? {
1081+
Some(params) => Some(format!("{py_name}{params}")),
1082+
None => text_signature(func.sig(), &py_name),
1083+
};
10811084
let has_receiver = func
10821085
.sig()
10831086
.inputs
@@ -1600,7 +1603,7 @@ impl ToTokens for MemberNursery {
16001603
struct MethodItemMeta(ItemMetaInner);
16011604

16021605
impl ItemMeta for MethodItemMeta {
1603-
const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw"];
1606+
const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "text_signature"];
16041607

16051608
fn from_inner(inner: ItemMetaInner) -> Self {
16061609
Self(inner)

crates/derive-impl/src/pymodule.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,10 @@ impl ModuleItem for FunctionItem {
664664
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &item_attr)?;
665665

666666
let py_name = item_meta.simple_name()?;
667-
let sig_doc = text_signature(func.sig(), &py_name);
667+
let sig_doc = match item_meta.explicit_text_signature()? {
668+
Some(params) => Some(format!("{py_name}{params}")),
669+
None => text_signature(func.sig(), &py_name),
670+
};
668671

669672
let module = args.module_name();
670673
// TODO: doc must exist at least one of code or CPython

crates/derive-impl/src/util.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,13 @@ pub(crate) trait ItemMeta: Sized {
296296
self.inner()._optional_str("name").ok().flatten()
297297
}
298298

299+
/// An explicitly declared `__text_signature__` parameter list, for a
300+
/// function whose Rust arguments cannot describe its Python ones, e.g. one
301+
/// that takes `FuncArgs` to check its own arity.
302+
fn explicit_text_signature(&self) -> Result<Option<String>> {
303+
self.inner()._optional_str("text_signature")
304+
}
305+
299306
fn new_meta_error(&self, msg: &str) -> syn::Error {
300307
let inner = self.inner();
301308
err_span!(inner.meta_ident, "#[{}] {}", inner.meta_name(), msg)
@@ -304,7 +311,7 @@ pub(crate) trait ItemMeta: Sized {
304311
pub(crate) struct SimpleItemMeta(pub ItemMetaInner);
305312

306313
impl ItemMeta for SimpleItemMeta {
307-
const ALLOWED_NAMES: &'static [&'static str] = &["name"];
314+
const ALLOWED_NAMES: &'static [&'static str] = &["name", "text_signature"];
308315

309316
fn from_inner(inner: ItemMetaInner) -> Self {
310317
Self(inner)

crates/vm/src/stdlib/builtins.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ mod builtins {
4242
const CODEGEN_NOT_SUPPORTED: &str =
4343
"can't compile() to bytecode when the `codegen` feature of rustpython is disabled";
4444

45-
#[pyfunction]
45+
#[pyfunction(text_signature = "(x, /)")]
4646
fn abs(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
4747
check_meth_o(vm, "abs", &func_args)?;
4848
let (x,): (PyObjectRef,) = func_args.bind(vm)?;
@@ -73,14 +73,14 @@ mod builtins {
7373
obj.ascii(vm)
7474
}
7575

76-
#[pyfunction(name = "ascii")]
76+
#[pyfunction(name = "ascii", text_signature = "(obj, /)")]
7777
fn py_ascii(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<PyStrRef> {
7878
check_meth_o(vm, "ascii", &func_args)?;
7979
let (obj,): (PyObjectRef,) = func_args.bind(vm)?;
8080
ascii(obj, vm)
8181
}
8282

83-
#[pyfunction]
83+
#[pyfunction(text_signature = "(number, /)")]
8484
fn bin(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<String> {
8585
check_meth_o(vm, "bin", &func_args)?;
8686
let (x,): (ArgIndex,) = func_args.bind(vm)?;
@@ -93,14 +93,14 @@ mod builtins {
9393
})
9494
}
9595

96-
#[pyfunction]
96+
#[pyfunction(text_signature = "(obj, /)")]
9797
fn callable(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<bool> {
9898
check_meth_o(vm, "callable", &func_args)?;
9999
let (obj,): (PyObjectRef,) = func_args.bind(vm)?;
100100
Ok(obj.is_callable())
101101
}
102102

103-
#[pyfunction]
103+
#[pyfunction(text_signature = "(i, /)")]
104104
fn chr(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<CodePoint> {
105105
check_meth_o(vm, "chr", &func_args)?;
106106
let (i,): (ArgIndex,) = func_args.bind(vm)?;
@@ -431,7 +431,7 @@ mod builtins {
431431
vm.dir(obj.into_option())
432432
}
433433

434-
#[pyfunction]
434+
#[pyfunction(text_signature = "(x, y, /)")]
435435
fn divmod(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
436436
check_no_kwargs(vm, "divmod", &func_args)?;
437437
check_positional(vm, "divmod", func_args.args.len(), 2, 2)?;
@@ -771,7 +771,7 @@ mod builtins {
771771
}
772772
}
773773

774-
#[pyfunction]
774+
#[pyfunction(text_signature = "()")]
775775
fn globals(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<PyDictRef> {
776776
check_noargs(vm, "globals", &func_args)?;
777777
Ok(vm.current_globals())
@@ -788,7 +788,7 @@ mod builtins {
788788
Ok(vm.get_attribute_opt(obj, attr)?.is_some())
789789
}
790790

791-
#[pyfunction]
791+
#[pyfunction(text_signature = "(obj, /)")]
792792
fn hash(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<PyHash> {
793793
check_meth_o(vm, "hash", &func_args)?;
794794
let (obj,): (PyObjectRef,) = func_args.bind(vm)?;
@@ -806,7 +806,7 @@ mod builtins {
806806
}
807807
}
808808

809-
#[pyfunction]
809+
#[pyfunction(text_signature = "(number, /)")]
810810
fn hex(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<String> {
811811
check_meth_o(vm, "hex", &func_args)?;
812812
let (number,): (ArgIndex,) = func_args.bind(vm)?;
@@ -815,7 +815,7 @@ mod builtins {
815815
Ok(format!("{n:#x}"))
816816
}
817817

818-
#[pyfunction]
818+
#[pyfunction(text_signature = "(obj, /)")]
819819
fn id(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<usize> {
820820
check_meth_o(vm, "id", &func_args)?;
821821
let (obj,): (PyObjectRef,) = func_args.bind(vm)?;
@@ -889,15 +889,15 @@ mod builtins {
889889
false
890890
}
891891

892-
#[pyfunction]
892+
#[pyfunction(text_signature = "(obj, class_or_tuple, /)")]
893893
fn isinstance(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<bool> {
894894
check_no_kwargs(vm, "isinstance", &func_args)?;
895895
check_positional(vm, "isinstance", func_args.args.len(), 2, 2)?;
896896
let (obj, typ): (PyObjectRef, PyObjectRef) = func_args.bind(vm)?;
897897
obj.is_instance(&typ, vm)
898898
}
899899

900-
#[pyfunction]
900+
#[pyfunction(text_signature = "(cls, class_or_tuple, /)")]
901901
fn issubclass(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<bool> {
902902
check_no_kwargs(vm, "issubclass", &func_args)?;
903903
check_positional(vm, "issubclass", func_args.args.len(), 2, 2)?;
@@ -955,14 +955,14 @@ mod builtins {
955955
}
956956
}
957957

958-
#[pyfunction]
958+
#[pyfunction(text_signature = "(obj, /)")]
959959
fn len(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<usize> {
960960
check_meth_o(vm, "len", &func_args)?;
961961
let (obj,): (PyObjectRef,) = func_args.bind(vm)?;
962962
obj.length(vm)
963963
}
964964

965-
#[pyfunction]
965+
#[pyfunction(text_signature = "()")]
966966
fn locals(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<ArgMapping> {
967967
check_noargs(vm, "locals", &func_args)?;
968968
vm.current_locals()
@@ -1062,7 +1062,7 @@ mod builtins {
10621062
})
10631063
}
10641064

1065-
#[pyfunction]
1065+
#[pyfunction(text_signature = "(number, /)")]
10661066
fn oct(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
10671067
check_meth_o(vm, "oct", &func_args)?;
10681068
let (number,): (ArgIndex,) = func_args.bind(vm)?;
@@ -1077,7 +1077,7 @@ mod builtins {
10771077
Ok(vm.ctx.new_str(s).into())
10781078
}
10791079

1080-
#[pyfunction]
1080+
#[pyfunction(text_signature = "(character, /)")]
10811081
// builtin_ord
10821082
fn ord(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<u32> {
10831083
check_meth_o(vm, "ord", &func_args)?;
@@ -1217,7 +1217,7 @@ mod builtins {
12171217
Ok(())
12181218
}
12191219

1220-
#[pyfunction]
1220+
#[pyfunction(text_signature = "(obj, /)")]
12211221
fn repr(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult<PyStrRef> {
12221222
check_meth_o(vm, "repr", &func_args)?;
12231223
let (obj,): (PyObjectRef,) = func_args.bind(vm)?;
@@ -1252,7 +1252,7 @@ mod builtins {
12521252
ndigits: OptionalOption<PyObjectRef>,
12531253
}
12541254

1255-
#[pyfunction]
1255+
#[pyfunction(text_signature = "(number, ndigits=None)")]
12561256
fn round(func_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
12571257
if func_args.args.is_empty() && !func_args.kwargs.contains_key("number") {
12581258
return Err(vm.new_type_error("round() missing required argument 'number' (pos 1)"));

extra_tests/snippets/builtin_signature.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
assert str(inspect.signature(issubclass)) == "(cls, class_or_tuple, /)"
4848
assert str(inspect.signature(aiter)) == "(async_iterable, /)"
4949

50+
# A function that takes FuncArgs to check its own arity has no Rust parameter
51+
# list to report, so it declares the signature itself.
52+
assert str(inspect.signature(round)) == "(number, ndigits=None)"
53+
assert str(inspect.signature(globals)) == "()"
54+
assert str(inspect.signature(ascii)) == "(obj, /)"
55+
5056
if sys.implementation.name == "rustpython":
5157
# Functions whose Rust arguments are destructuring patterns rather than
5258
# plain names get no signature at all, instead of emitting text that is not
@@ -56,7 +62,7 @@
5662
# We cannot derive them until FromArgs reports the parameters of its own
5763
# structs, so until then we report no signature, which is at least how
5864
# CPython behaves for the builtins it has no signature for.
59-
for f in (round, sum):
65+
for f in (sum,):
6066
assert f.__text_signature__ is None, f.__name__
6167
try:
6268
inspect.signature(f)

0 commit comments

Comments
 (0)