Skip to content
1 change: 0 additions & 1 deletion Lib/test/test_pydoc/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1582,7 +1582,6 @@ def test_module_level_callable(self):
self.assertEqual(self._get_summary_line(os.stat),
"stat(path, *, dir_fd=None, follow_symlinks=True)")

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_module_level_callable_noargs(self):
self.assertEqual(self._get_summary_line(time.time),
"time()")
Expand Down
5 changes: 4 additions & 1 deletion crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,10 @@ where
args.attrs.push(allow_attr);
}

let doc = args.attrs.doc().map(|doc| format_doc(&sig_doc, &doc));
let doc = args.attrs.doc().map(|doc| match &sig_doc {
Some(sig_doc) => format_doc(sig_doc, &doc),
None => doc,
});
args.context.method_items.add_item(MethodNurseryItem {
py_name,
cfgs: args.cfgs.to_vec(),
Expand Down
16 changes: 9 additions & 7 deletions crates/derive-impl/src/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ struct FunctionNurseryItem {
py_names: Vec<String>,
cfgs: Vec<Attribute>,
ident: Ident,
doc: String,
doc: Option<String>,
call_flags: TokenStream,
}

Expand Down Expand Up @@ -556,8 +556,10 @@ impl ToTokens for ValidatedFunctionNursery {
let cfgs = &item.cfgs;
let cfgs = quote!(#(#cfgs)*);
let py_names = &item.py_names;
let doc = &item.doc;
let doc = quote!(Some(#doc));
let doc = match &item.doc {
Some(doc) => quote!(Some(#doc)),
None => quote!(None),
};
let flags = &item.call_flags;

inner_tokens.extend(quote![
Expand Down Expand Up @@ -671,10 +673,10 @@ impl ModuleItem for FunctionItem {
.copied()
.map(str::to_owned)
});
let doc = if let Some(doc) = doc {
format_doc(&sig_doc, &doc)
} else {
sig_doc
let doc = match (sig_doc, doc) {
(Some(sig_doc), Some(doc)) => Some(format_doc(&sig_doc, &doc)),
(Some(sig_doc), None) => Some(sig_doc),
(None, doc) => doc,
};

let py_names = {
Expand Down
85 changes: 50 additions & 35 deletions crates/derive-impl/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,13 +732,20 @@ where

// Best effort attempt to generate a template from which a
// __text_signature__ can be created.
pub(crate) fn text_signature(sig: &Signature, name: &str) -> String {
let signature = func_sig(sig);
if signature.starts_with("$self") {
//
// Unlike CPython, a `#[pyfunction]` doesn't take the module as an argument yet,
// so there's no module to mark with `$module`.
pub(crate) fn text_signature(sig: &Signature, name: &str) -> Option<String> {
let signature = func_sig(sig)?;
// Arguments bind through `FuncArgs::take_positional`, which never consults
// the keyword map, so they are positional-only. `*args`/`**kwargs` cannot be
// followed by `/`, and an empty parameter list has nothing to mark.
let signature = if signature.is_empty() || signature.contains('*') {
format!("{name}({signature})")
} else {
format!("{}({}, {})", name, "$module", signature)
}
format!("{name}({signature}, /)")
};
Some(signature)
}

pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize) -> TokenStream {
Expand Down Expand Up @@ -812,37 +819,45 @@ pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize)
}
}

fn func_sig(sig: &Signature) -> String {
sig.inputs
.iter()
.filter_map(|arg| {
let arg = match arg {
FnArg::Typed(typed) => typed,
FnArg::Receiver(_) => return Some("$self".to_owned()),
};
let ty = arg.ty.as_ref();
let ty = quote!(#ty).to_string();
if ty == "FuncArgs" {
return Some("*args, **kwargs".to_owned());
}
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
return None;
}
let ident = match arg.pat.as_ref() {
syn::Pat::Ident(p) => p.ident.to_string(),
// FIXME: other => unreachable!("function arg pattern must be ident but found `{}`", quote!(fn #ident(.. #other ..))),
other => quote!(#other).to_string(),
};
if ident == "zelf" {
return Some("$self".to_owned());
}
if ident == "vm" {
unreachable!("type &VirtualMachine(`{ty}`) must be filtered already");
/// Returns None when an argument has no name to report, in which case no
/// signature can be generated for the function.
fn func_sig(sig: &Signature) -> Option<String> {
let mut params = Vec::new();
for arg in &sig.inputs {
let arg = match arg {
FnArg::Typed(typed) => typed,
FnArg::Receiver(_) => {
params.push("$self".to_owned());
continue;
}
Some(ident)
})
.collect::<Vec<_>>()
.join(", ")
};
let ty = arg.ty.as_ref();
let ty = quote!(#ty).to_string();
if ty == "FuncArgs" {
params.push("*args, **kwargs".to_owned());
continue;
}
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
continue;
}
// An argument bound by a destructuring pattern, e.g.
// `fn round(RoundArgs { number, ndigits }: RoundArgs, ..)`, has no name
// to report. Stringifying the pattern would emit Rust syntax, which
// makes inspect.signature() raise "invalid signature".
let syn::Pat::Ident(pat) = arg.pat.as_ref() else {
return None;
};
let ident = pat.ident.to_string();
if ident == "zelf" {
params.push("$self".to_owned());
continue;
}
if ident == "vm" {
unreachable!("type &VirtualMachine(`{ty}`) must be filtered already");
}
params.push(ident);
}
Some(params.join(", "))
}

pub(crate) fn format_doc(sig: &str, doc: &str) -> String {
Expand Down
56 changes: 32 additions & 24 deletions crates/vm/src/stdlib/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ mod builtins {
}

#[pyfunction]
fn bin(x: PyIntRef) -> String {
let x = x.as_bigint();
fn bin(number: PyIntRef) -> String {
let x = number.as_bigint();
if x.is_negative() {
format!("-0b{:b}", x.abs())
} else {
Expand Down Expand Up @@ -392,11 +392,11 @@ mod builtins {
}

#[pyfunction]
fn delattr(obj: PyObjectRef, attr: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
let attr = attr.try_to_ref::<PyStr>(vm).map_err(|_e| {
fn delattr(obj: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
let attr = name.try_to_ref::<PyStr>(vm).map_err(|_e| {
vm.new_type_error(format!(
"attribute name must be string, not '{}'",
attr.class().name()
name.class().name()
))
})?;
obj.del_attr(attr, vm)
Expand All @@ -408,8 +408,8 @@ mod builtins {
}

#[pyfunction]
fn divmod(a: PyObjectRef, b: PyObjectRef, vm: &VirtualMachine) -> PyResult {
vm._divmod(&a, &b)
fn divmod(x: PyObjectRef, y: PyObjectRef, vm: &VirtualMachine) -> PyResult {
vm._divmod(&x, &y)
}

#[derive(FromArgs)]
Expand Down Expand Up @@ -715,11 +715,11 @@ mod builtins {
}

#[pyfunction]
fn hasattr(obj: PyObjectRef, attr: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
let attr = attr.try_to_ref::<PyStr>(vm).map_err(|_e| {
fn hasattr(obj: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
let attr = name.try_to_ref::<PyStr>(vm).map_err(|_e| {
vm.new_type_error(format!(
"attribute name must be string, not '{}'",
attr.class().name()
name.class().name()
))
})?;
Ok(vm.get_attribute_opt(obj, attr)?.is_some())
Expand Down Expand Up @@ -821,13 +821,21 @@ mod builtins {
}

#[pyfunction]
fn isinstance(obj: PyObjectRef, typ: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
obj.is_instance(&typ, vm)
fn isinstance(
obj: PyObjectRef,
class_or_tuple: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<bool> {
obj.is_instance(&class_or_tuple, vm)
}

#[pyfunction]
fn issubclass(subclass: PyObjectRef, typ: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
subclass.is_subclass(&typ, vm)
fn issubclass(
cls: PyObjectRef,
class_or_tuple: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<bool> {
cls.is_subclass(&class_or_tuple, vm)
}

#[pyfunction]
Expand All @@ -848,8 +856,8 @@ mod builtins {
}

#[pyfunction]
fn aiter(iter_target: PyObjectRef, vm: &VirtualMachine) -> PyResult {
iter_target.get_aiter(vm)
fn aiter(async_iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult {
async_iterable.get_aiter(vm)
}

#[pyfunction]
Expand Down Expand Up @@ -998,8 +1006,8 @@ mod builtins {

#[pyfunction]
// builtin_ord
fn ord(c: PyObjectRef, vm: &VirtualMachine) -> PyResult<u32> {
let bytes = if let Some(string) = c.downcast_ref::<PyStr>() {
fn ord(character: PyObjectRef, vm: &VirtualMachine) -> PyResult<u32> {
let bytes = if let Some(string) = character.downcast_ref::<PyStr>() {
return match string.as_wtf8().code_points().exactly_one() {
Ok(character) => Ok(character.to_u32()),
Err(_) => {
Expand All @@ -1009,14 +1017,14 @@ mod builtins {
)))
}
};
} else if let Some(bytes) = c.downcast_ref::<PyBytes>() {
} else if let Some(bytes) = character.downcast_ref::<PyBytes>() {
bytes.as_bytes().to_vec()
} else if let Some(bytearray) = c.downcast_ref::<PyByteArray>() {
} else if let Some(bytearray) = character.downcast_ref::<PyByteArray>() {
bytearray.borrow_buf().to_vec()
} else {
return Err(vm.new_type_error(format!(
"ord() expected string of length 1, but {} found",
c.class().name()
character.class().name()
)));
};
let bytes_len = bytes.len();
Expand Down Expand Up @@ -1149,14 +1157,14 @@ mod builtins {
#[pyfunction]
fn setattr(
obj: PyObjectRef,
attr: PyObjectRef,
name: PyObjectRef,
value: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
let attr = attr.try_to_ref::<PyStr>(vm).map_err(|_e| {
let attr = name.try_to_ref::<PyStr>(vm).map_err(|_e| {
vm.new_type_error(format!(
"attribute name must be string, not '{}'",
attr.class().name()
name.class().name()
))
})?;
obj.set_attr(attr, value, vm)?;
Expand Down
66 changes: 66 additions & 0 deletions extra_tests/snippets/builtin_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import inspect
import sys

# __text_signature__ is generated from the Rust parameter list, so it must not
# describe parameters the function does not actually take, and must mark the
# ones it does take as positional-only.

# No phantom `module` parameter. RustPython's #[pyfunction]s take no module
# argument, so `__self__` is None and inspect has nothing to strip.
for f in (len, abs, hash, id, repr, bin, ord, divmod, hex, oct, chr, callable):
assert "module" not in inspect.signature(f).parameters, f.__name__

# Plain arguments bind through take_positional(), so they are positional-only.
try:
len(obj=[1, 2])
except TypeError:
pass
else:
raise AssertionError("len() should not accept keyword arguments")

assert str(inspect.signature(len)) == "(obj, /)"
assert str(inspect.signature(abs)) == "(x, /)"
assert str(inspect.signature(hash)) == "(obj, /)"
assert str(inspect.signature(chr)) == "(i, /)"
assert str(inspect.signature(callable)) == "(obj, /)"

assert (
inspect.signature(len).parameters["obj"].kind == inspect.Parameter.POSITIONAL_ONLY
)

# *args/**kwargs cannot be followed by `/`. The parameter names themselves still
# differ from CPython here, which is out of scope.
breakpoint_kinds = [p.kind for p in inspect.signature(breakpoint).parameters.values()]
assert breakpoint_kinds == [
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD,
], breakpoint_kinds

# Parameter names follow CPython, so signatures are directly comparable.
assert str(inspect.signature(bin)) == "(number, /)"
assert str(inspect.signature(ord)) == "(character, /)"
assert str(inspect.signature(divmod)) == "(x, y, /)"
assert str(inspect.signature(hasattr)) == "(obj, name, /)"
assert str(inspect.signature(setattr)) == "(obj, name, value, /)"
assert str(inspect.signature(delattr)) == "(obj, name, /)"
assert str(inspect.signature(isinstance)) == "(obj, class_or_tuple, /)"
assert str(inspect.signature(issubclass)) == "(cls, class_or_tuple, /)"
assert str(inspect.signature(aiter)) == "(async_iterable, /)"

if sys.implementation.name == "rustpython":
# Functions whose Rust arguments are destructuring patterns rather than
# plain names get no signature at all, instead of emitting text that is not
# valid Python and makes inspect.signature() raise "invalid signature".
#
# CPython does have signatures for these, hand-written via Argument Clinic.
# We cannot derive them until FromArgs reports the parameters of its own
# structs, so until then we report no signature, which is at least how
# CPython behaves for the builtins it has no signature for.
for f in (round, sum):
assert f.__text_signature__ is None, f.__name__
try:
inspect.signature(f)
except ValueError as e:
assert "no signature found" in str(e), str(e)
else:
raise AssertionError(f"{f.__name__} should have no signature")
Loading