Skip to content

Fix PyFunction doc behavior #5827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Lib/test/test_funcattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,6 @@ def test_delete___dict__(self):
else:
self.fail("deleting function dictionary should raise TypeError")

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_unassigned_dict(self):
self.assertEqual(self.b.__dict__, {})

Expand All @@ -379,8 +377,6 @@ def test_set_docstring_attr(self):
self.assertEqual(self.fi.a.__doc__, docstr)
self.cannot_set_attr(self.fi.a, "__doc__", docstr, AttributeError)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_delete_docstring(self):
self.b.__doc__ = "The docstring"
del self.b.__doc__
Expand Down
2 changes: 2 additions & 0 deletions extra_tests/snippets/syntax_function2.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def f4():

assert f4.__doc__ == "test4"

assert type(lambda: None).__doc__.startswith("Create a function object."), type(f4).__doc__


def revdocstr(f):
d = f.__doc__
Expand Down
23 changes: 18 additions & 5 deletions vm/src/builtins/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,28 @@ impl GetDescriptor for PyMemberDescriptor {
fn descr_get(
zelf: PyObjectRef,
obj: Option<PyObjectRef>,
_cls: Option<PyObjectRef>,
cls: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult {
let descr = Self::_as_pyref(&zelf, vm)?;
match obj {
Some(x) => {
let zelf = Self::_as_pyref(&zelf, vm)?;
zelf.member.get(x, vm)
Some(x) => descr.member.get(x, vm),
None => {
// When accessed from class (not instance), for __doc__ member descriptor,
// return the class's docstring if available
// When accessed from class (not instance), check if the class has
// an attribute with the same name as this member descriptor
if let Some(cls) = cls {
if let Ok(cls_type) = cls.downcast::<PyType>() {
if let Some(interned) = vm.ctx.interned_str(descr.member.name.as_str()) {
if let Some(attr) = cls_type.attributes.read().get(&interned) {
return Ok(attr.clone());
}
}
}
}
Ok(zelf)
}
None => Ok(zelf),
}
}
}
Expand Down
13 changes: 9 additions & 4 deletions vm/src/builtins/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,10 +417,15 @@ impl PyFunction {
}

#[pymember(magic)]
fn doc(_vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult {
let zelf: PyRef<PyFunction> = zelf.downcast().unwrap_or_else(|_| unreachable!());
let doc = zelf.doc.lock();
Ok(doc.clone())
fn doc(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult {
// When accessed from instance, obj is the PyFunction instance
if let Ok(func) = obj.downcast::<PyFunction>() {
let doc = func.doc.lock();
Ok(doc.clone())
} else {
// When accessed from class, return None as there's no instance
Ok(vm.ctx.none())
}
}

#[pymember(magic, setter)]
Expand Down
65 changes: 65 additions & 0 deletions vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,22 @@ 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
// 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
if let Some(sig_end_pos) = doc_without_sig.find(SIGNATURE_END_MARKER) {
let after_sig = &doc_without_sig[sig_end_pos + SIGNATURE_END_MARKER.len()..];
// Return the documentation after the signature, or empty string if none
return after_sig;
}
}
// If no signature found, return the whole doc
internal_doc
}

impl GetAttr for PyType {
fn getattro(zelf: &Py<Self>, name_str: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
#[cold]
Expand Down Expand Up @@ -1122,6 +1138,55 @@ impl Py<PyType> {
PyTuple::new_unchecked(elements.into_boxed_slice())
}

#[pygetset(magic)]
fn doc(&self, vm: &VirtualMachine) -> PyResult {
// Similar to CPython's type_get_doc
// For non-heap types (static types), check if there's an internal doc
if !self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) {
if let Some(internal_doc) = self.slots.doc {
// 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());
}
}

// Check if there's a __doc__ in the type's dict
if let Some(doc_attr) = self.get_attr(vm.ctx.intern_str("__doc__")) {
// If it's a descriptor, call its __get__ method
let descr_get = doc_attr
.class()
.mro_find_map(|cls| cls.slots.descr_get.load());
if let Some(descr_get) = descr_get {
descr_get(doc_attr, None, Some(self.to_owned().into()), vm)
} else {
Ok(doc_attr)
}
} else {
Ok(vm.ctx.none())
}
}

#[pygetset(magic, setter)]
fn set_doc(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
// Similar to CPython's type_set_doc
let value = value.ok_or_else(|| {
vm.new_type_error(format!(
"cannot delete '__doc__' attribute of type '{}'",
self.name()
))
})?;

// Check if we can set this special type attribute
self.check_set_special_type_attr(&value, identifier!(vm, __doc__), vm)?;

// Set the __doc__ in the type's dict
self.attributes
.write()
.insert(identifier!(vm, __doc__), value);

Ok(())
}

#[pymethod(magic)]
fn dir(&self) -> PyList {
let attributes: Vec<PyObjectRef> = self
Expand Down
7 changes: 6 additions & 1 deletion vm/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ pub trait PyClassImpl: PyClassDef {
}
Self::impl_extend_class(ctx, class);
if let Some(doc) = Self::DOC {
class.set_attr(identifier!(ctx, __doc__), ctx.new_str(doc).into());
// Only set __doc__ if it doesn't already exist (e.g., as a member descriptor)
// This matches CPython's behavior in type_dict_set_doc
let doc_attr_name = identifier!(ctx, __doc__);
if class.attributes.read().get(doc_attr_name).is_none() {
class.set_attr(doc_attr_name, ctx.new_str(doc).into());
}
}
if let Some(module_name) = Self::MODULE_NAME {
class.set_attr(
Expand Down
Loading