Report every generated __text_signature__, and keep it out of __doc__ (#8614)

* Strip the signature prefix from builtin __doc__

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

* Terminate a generated __text_signature__ that carries no documentation

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

* Report a __text_signature__ for every method

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

* Drop the expectedFailure on 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

* Name every #[pymethod] receiver zelf

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

* Drop the unreachable zelf name check in func_sig

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

* Keep the argument bundle when marking the receiver

A #[pyclassmethod] that takes FuncArgs receives the class inside the
bundle along with every other argument, but the marker was spent on the
bundle and the arguments went unreported:

    >>> object.__subclasshook__.__text_signature__
    '($type, /)'                    # was, claims it takes nothing
    '($type, *args, **kwargs)'      # now

Report both, the shape CPython uses for __new__. Two methods take their
arguments this way, __subclasshook__ on object and on type.

Assisted-by: Claude Code:claude-opus-5
This commit is contained in:
Hanjeong Lee authored and GitHub committed 2026-08-31 14:23:29 +09:00
1 parent 5b1a1c08d7
commit d1bbf4bc1a
14 files changed
+115 -78

No files matched your search

-2
View File
@@ -1597,12 +1597,10 @@ class TestDescriptions(unittest.TestCase):
self.assertEqual(self._get_summary_line(_stat.S_IMODE),
"S_IMODE(object, /)")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_unbound_builtin_method_noargs(self):
self.assertEqual(self._get_summary_line(str.lower),
"lower(self, /) unbound builtins.str method")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_bound_builtin_method_noargs(self):
self.assertEqual(self._get_summary_line(''.lower),
"lower() method of builtins.str instance")
-1
View File
@@ -858,7 +858,6 @@ class TestPyReplCompleter(TestCase):
reader = ReadlineAlikeReader(console=console, config=config)
return reader
@unittest.expectedFailure # TODO: RUSTPYTHON
@patch("rlcompleter._readline_available", False)
def test_simple_completion(self):
events = code_to_events("os.getpid\t\n")
+18 -8
View File
@@ -1077,16 +1077,25 @@ where
}
let raw = item_meta.raw()?;
let sig_doc = text_signature(func.sig(), &py_name);
let has_receiver = func
.sig()
.inputs
.iter()
.any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
let drop_first_typed = match self.inner.attr_name {
AttrName::Method | AttrName::ClassMethod if !has_receiver && !raw => 1,
_ => 0,
// Without a `&self` receiver the first argument is the one the call
// binds to, and CPython names it $self for a method and $type for a
// classmethod.
let implicit_self = if has_receiver || raw {
None
} else {
match self.inner.attr_name {
AttrName::Method => Some("$self"),
AttrName::ClassMethod => Some("$type"),
_ => None,
}
};
let drop_first_typed = usize::from(implicit_self.is_some());
let sig_doc = text_signature(func.sig(), &py_name, implicit_self);
let call_flags = infer_native_call_flags(func.sig(), drop_first_typed);
// Add #[allow(non_snake_case)] for setter methods like set___name__
@@ -1096,10 +1105,11 @@ where
args.attrs.push(allow_attr);
}
let doc = args.attrs.doc().map(|doc| match &sig_doc {
Some(sig_doc) => format_doc(sig_doc, &doc),
None => doc,
});
let doc = match (sig_doc, args.attrs.doc()) {
(Some(sig_doc), Some(doc)) => Some(format_doc(&sig_doc, &doc)),
(Some(sig_doc), None) => Some(format_doc(&sig_doc, "")),
(None, doc) => doc,
};
args.context.method_items.add_item(MethodNurseryItem {
py_name,
cfgs: args.cfgs.to_vec(),
+2 -2
View File
@@ -664,7 +664,7 @@ impl ModuleItem for FunctionItem {
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &item_attr)?;
let py_name = item_meta.simple_name()?;
let sig_doc = text_signature(func.sig(), &py_name);
let sig_doc = text_signature(func.sig(), &py_name, None);
let module = args.module_name();
// TODO: doc must exist at least one of code or CPython
@@ -675,7 +675,7 @@ impl ModuleItem for FunctionItem {
});
let doc = match (sig_doc, doc) {
(Some(sig_doc), Some(doc)) => Some(format_doc(&sig_doc, &doc)),
(Some(sig_doc), None) => Some(sig_doc),
(Some(sig_doc), None) => Some(format_doc(&sig_doc, "")),
(None, doc) => doc,
};
+17 -7
View File
@@ -735,8 +735,12 @@ where
//
// 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)?;
pub(crate) fn text_signature(
sig: &Signature,
name: &str,
implicit_self: Option<&str>,
) -> Option<String> {
let signature = func_sig(sig, implicit_self)?;
// 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.
@@ -821,7 +825,10 @@ pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize)
/// 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> {
///
/// `implicit_self` is the marker to report for a first argument that the call
/// binds to without a `&self` receiver.
fn func_sig(sig: &Signature, mut implicit_self: Option<&str>) -> Option<String> {
let mut params = Vec::new();
for arg in &sig.inputs {
let arg = match arg {
@@ -834,12 +841,19 @@ fn func_sig(sig: &Signature) -> Option<String> {
let ty = arg.ty.as_ref();
let ty = quote!(#ty).to_string();
if ty == "FuncArgs" {
// The bundle carries the receiver along with everything else, so
// report both rather than spending the marker on it.
params.extend(implicit_self.take().map(str::to_owned));
params.push("*args, **kwargs".to_owned());
continue;
}
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
continue;
}
if let Some(marker) = implicit_self.take() {
params.push(marker.to_owned());
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
@@ -848,10 +862,6 @@ fn func_sig(sig: &Signature) -> Option<String> {
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");
}
+3 -3
View File
@@ -42,17 +42,17 @@ pub(crate) mod ssl_error {
impl PySSLError {
// Returns strerror attribute if available, otherwise str(args)
#[pymethod]
fn __str__(exc: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
use crate::vm::AsObject;
// Try to get strerror attribute first (OSError compatibility)
if let Ok(strerror) = exc.as_object().get_attr("strerror", vm)
if let Ok(strerror) = zelf.as_object().get_attr("strerror", vm)
&& !vm.is_none(&strerror)
{
return strerror.str(vm);
}
// Otherwise return str(args)
let args = exc.args();
let args = zelf.args();
if args.len() == 1 {
args.as_slice()[0].str(vm)
} else {
+2 -2
View File
@@ -106,8 +106,8 @@ impl Constructor for PyBool {
#[pyclass(with(Constructor, AsNumber, Representable), flags(_MATCH_SELF))]
impl PyBool {
#[pymethod]
fn __format__(obj: PyObjectRef, spec: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<String> {
let new_bool = obj.try_to_bool(vm)?;
fn __format__(zelf: PyObjectRef, spec: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<String> {
let new_bool = zelf.try_to_bool(vm)?;
FormatSpec::parse(spec.as_str())
.and_then(|format_spec| format_spec.format_bool(new_bool))
.map_err(|err| err.into_pyexception(vm))
+3 -1
View File
@@ -164,9 +164,11 @@ impl PyNativeFunction {
Ok(qualname)
}
// meth_get__doc__ in CPython
#[pygetset]
fn __doc__(zelf: NativeFunctionOrMethod) -> Option<&'static str> {
zelf.0.value.doc
let doc = zelf.0.value.doc?;
type_::get_doc_from_internal_doc(zelf.0.value.name, doc)
}
// meth_get__self__ in CPython
+3 -2
View File
@@ -134,8 +134,9 @@ impl PyMethodDescriptor {
}
#[pygetset]
const fn __doc__(&self) -> Option<&'static str> {
self.method.doc
fn __doc__(&self) -> Option<&'static str> {
let doc = self.method.doc?;
type_::get_doc_from_internal_doc(self.method.name, doc)
}
#[pygetset]
+8 -8
View File
@@ -397,8 +397,8 @@ impl PyBaseObject {
fn __init_subclass__(_cls: PyTypeRef) {}
#[pymethod]
pub fn __dir__(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyList> {
obj.dir(vm)
pub fn __dir__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyList> {
zelf.dir(vm)
}
#[pymethod]
@@ -468,22 +468,22 @@ impl PyBaseObject {
}
#[pymethod]
fn __reduce__(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
common_reduce(obj, 0, vm)
fn __reduce__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
common_reduce(zelf, 0, vm)
}
#[pymethod]
fn __reduce_ex__(obj: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
fn __reduce_ex__(zelf: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
let __reduce__ = identifier!(vm, __reduce__);
if let Some(reduce) = vm.get_attribute_opt(obj.clone(), __reduce__)? {
if let Some(reduce) = vm.get_attribute_opt(zelf.clone(), __reduce__)? {
let object_reduce = vm.ctx.types.object_type.get_attr(__reduce__).unwrap();
let typ_obj: PyObjectRef = obj.class().to_owned().into();
let typ_obj: PyObjectRef = zelf.class().to_owned().into();
let class_reduce = typ_obj.get_attr(__reduce__, vm)?;
if !class_reduce.is(&object_reduce) {
return reduce.call((), vm);
}
}
common_reduce(obj, proto, vm)
common_reduce(zelf, proto, vm)
}
#[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
+9 -4
View File
@@ -2758,9 +2758,8 @@ pub(crate) fn get_text_signature_from_internal_doc<'a>(
find_signature(name, internal_doc).and_then(get_signature)
}
// _PyType_GetDocFromInternalDoc in CPython
fn get_doc_from_internal_doc<'a>(name: &str, internal_doc: &'a str) -> &'a str {
// Similar to CPython's _PyType_DocWithoutSignature
// _PyType_DocWithoutSignature in CPython
fn doc_without_signature<'a>(name: &str, internal_doc: &'a str) -> &'a str {
// If the doc starts with the type name and a '(', it's a signature
if let Some(doc_without_sig) = find_signature(name, internal_doc) {
// Find where the signature ends
@@ -2774,6 +2773,12 @@ fn get_doc_from_internal_doc<'a>(name: &str, internal_doc: &'a str) -> &'a str {
internal_doc
}
// _PyType_GetDocFromInternalDoc in CPython
pub(crate) fn get_doc_from_internal_doc<'a>(name: &str, internal_doc: &'a str) -> Option<&'a str> {
let doc = doc_without_signature(name, internal_doc);
(!doc.is_empty()).then_some(doc)
}
impl Initializer for PyType {
type Args = FuncArgs;
@@ -2862,7 +2867,7 @@ impl Py<PyType> {
{
// Process internal doc, removing signature if present
let doc_str = get_doc_from_internal_doc(&self.name(), internal_doc);
return Ok(vm.ctx.new_str(doc_str).into());
return Ok(doc_str.map_or_else(|| vm.ctx.none(), |doc| vm.ctx.new_str(doc).into()));
}
// Check if there's a __doc__ in THIS type's dict only (not MRO)
+9 -9
View File
@@ -1916,11 +1916,11 @@ pub(super) mod types {
#[pyexception(with(Initializer))]
impl PyImportError {
#[pymethod]
fn __reduce__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
let obj = exc.as_object().to_owned();
let args: PyObjectRef = match exc.get_arg(0) {
fn __reduce__(zelf: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
let obj = zelf.as_object().to_owned();
let args: PyObjectRef = match zelf.get_arg(0) {
Some(arg) => vm.new_tuple((arg,)).into(),
None => exc.args().into(),
None => zelf.args().into(),
};
let mut result: Vec<PyObjectRef> = vec![obj.class().to_owned().into(), args];
@@ -2360,15 +2360,15 @@ pub(super) mod types {
}
#[pymethod]
fn __reduce__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
let args = exc.args();
let obj = exc.as_object().to_owned();
fn __reduce__(zelf: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
let args = zelf.args();
let obj = zelf.as_object().to_owned();
let mut result: Vec<PyObjectRef> = vec![obj.class().to_owned().into()];
if args.len() >= 2 && args.len() <= 5 {
// SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic
let errno = exc.get_arg(0).unwrap();
let msg = exc.get_arg(1).unwrap();
let errno = zelf.get_arg(0).unwrap();
let msg = zelf.get_arg(1).unwrap();
if let Ok(filename) = obj.get_attr("filename", vm) {
if !vm.is_none(&filename) {
+29 -29
View File
@@ -450,41 +450,41 @@ mod _io {
}
#[pymethod]
fn __enter__(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult {
check_closed(&instance, vm)?;
Ok(instance)
fn __enter__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
check_closed(&zelf, vm)?;
Ok(zelf)
}
#[pymethod]
fn __exit__(instance: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
vm.call_method(&instance, "close", ())?;
fn __exit__(zelf: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
vm.call_method(&zelf, "close", ())?;
Ok(())
}
#[pymethod]
fn flush(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
fn flush(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
// just check if this is closed; if it isn't, do nothing
check_closed(&instance, vm)
check_closed(&zelf, vm)
}
#[pymethod]
fn seekable(_self: PyObjectRef) -> bool {
fn seekable(_zelf: PyObjectRef) -> bool {
false
}
#[pymethod]
fn readable(_self: PyObjectRef) -> bool {
fn readable(_zelf: PyObjectRef) -> bool {
false
}
#[pymethod]
fn writable(_self: PyObjectRef) -> bool {
fn writable(_zelf: PyObjectRef) -> bool {
false
}
#[pymethod]
fn isatty(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
check_closed(&instance, vm)?;
fn isatty(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
check_closed(&zelf, vm)?;
Ok(false)
}
@@ -494,8 +494,8 @@ mod _io {
}
#[pymethod]
fn close(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
iobase_close(&instance, vm)
fn close(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
iobase_close(&zelf, vm)
}
#[pymethod]
@@ -560,23 +560,23 @@ mod _io {
}
#[pymethod(name = "_checkClosed")]
fn check_closed(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_closed(&instance, vm)
fn check_closed(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_closed(&zelf, vm)
}
#[pymethod(name = "_checkReadable")]
fn check_readable(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_readable(&instance, vm)
fn check_readable(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_readable(&zelf, vm)
}
#[pymethod(name = "_checkWritable")]
fn check_writable(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_writable(&instance, vm)
fn check_writable(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_writable(&zelf, vm)
}
#[pymethod(name = "_checkSeekable")]
fn check_seekable(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_seekable(&instance, vm)
fn check_seekable(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
check_seekable(&zelf, vm)
}
}
@@ -641,12 +641,12 @@ mod _io {
#[pyclass(flags(BASETYPE, HAS_DICT, HAS_WEAKREF))]
impl _RawIOBase {
#[pymethod]
fn read(instance: PyObjectRef, size: OptionalSize, vm: &VirtualMachine) -> PyResult {
fn read(zelf: PyObjectRef, size: OptionalSize, vm: &VirtualMachine) -> PyResult {
if let Some(size) = size.to_usize() {
let b = PyByteArray::from(vm.new_zeroed_bytes(size)?).into_ref(&vm.ctx);
let n = <Option<isize>>::try_from_object(
vm,
vm.call_method(&instance, "readinto", (b.clone(),))?,
vm.call_method(&zelf, "readinto", (b.clone(),))?,
)?;
Ok(match n {
None => vm.ctx.none(),
@@ -665,18 +665,18 @@ mod _io {
}
})
} else {
vm.call_method(&instance, "readall", ())
vm.call_method(&zelf, "readall", ())
}
}
#[pymethod]
fn readall(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<Option<Vec<u8>>> {
fn readall(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<Option<Vec<u8>>> {
let mut chunks = Vec::new();
let mut total_len = 0;
loop {
// Loop with EINTR handling (PEP 475)
let data = loop {
let res = vm.call_method(&instance, "read", (DEFAULT_BUFFER_SIZE,));
let res = vm.call_method(&zelf, "read", (DEFAULT_BUFFER_SIZE,));
match trap_eintr(res, vm)? {
Some(val) => break val,
None => continue,
@@ -707,12 +707,12 @@ mod _io {
}
#[pymethod]
fn readinto(_instance: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
fn readinto(_zelf: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
Err(vm.new_not_implemented_error(String::new()))
}
#[pymethod]
fn write(_instance: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
fn write(_zelf: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
Err(vm.new_not_implemented_error(String::new()))
}
}
+12
View File
@@ -1,4 +1,5 @@
import inspect
import os
import sys
# __text_signature__ is generated from the Rust parameter list, so it must not
@@ -47,6 +48,17 @@ 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, /)"
# A generated signature is stored in __doc__ and read back out of it, so it
# needs the `--` terminator even when the function has no documentation.
assert str(inspect.signature(os.getpid)) == "()"
assert str(inspect.signature(os.getcwd)) == "()"
# Methods report a signature too, with the receiver marked so that binding a
# method drops it.
assert str(inspect.signature(list.__dir__)) == "(self, /)"
assert str(inspect.signature([].__dir__)) == "()"
assert str(inspect.signature(float.fromhex)) == "(string, /)"
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