Skip to content

Commit 8d8e85b

Browse files
committed
Fix PyFunction doc behavior
1 parent 9952c97 commit 8d8e85b

File tree

5 files changed

+100
-10
lines changed

5 files changed

+100
-10
lines changed

extra_tests/snippets/syntax_function2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def f4():
5252

5353
assert f4.__doc__ == "test4"
5454

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

5658
def revdocstr(f):
5759
d = f.__doc__

vm/src/builtins/descriptor.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,15 +345,28 @@ impl GetDescriptor for PyMemberDescriptor {
345345
fn descr_get(
346346
zelf: PyObjectRef,
347347
obj: Option<PyObjectRef>,
348-
_cls: Option<PyObjectRef>,
348+
cls: Option<PyObjectRef>,
349349
vm: &VirtualMachine,
350350
) -> PyResult {
351+
let descr = Self::_as_pyref(&zelf, vm)?;
351352
match obj {
352-
Some(x) => {
353-
let zelf = Self::_as_pyref(&zelf, vm)?;
354-
zelf.member.get(x, vm)
353+
Some(x) => descr.member.get(x, vm),
354+
None => {
355+
// When accessed from class (not instance), for __doc__ member descriptor,
356+
// return the class's docstring if available
357+
// When accessed from class (not instance), check if the class has
358+
// an attribute with the same name as this member descriptor
359+
if let Some(cls) = cls {
360+
if let Ok(cls_type) = cls.downcast::<PyType>() {
361+
if let Some(interned) = vm.ctx.interned_str(descr.member.name.as_str()) {
362+
if let Some(attr) = cls_type.attributes.read().get(&interned) {
363+
return Ok(attr.clone());
364+
}
365+
}
366+
}
367+
}
368+
Ok(zelf)
355369
}
356-
None => Ok(zelf),
357370
}
358371
}
359372
}

vm/src/builtins/function.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,15 @@ impl PyFunction {
417417
}
418418

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

426431
#[pymember(magic, setter)]

vm/src/builtins/type.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,22 @@ pub(crate) fn get_text_signature_from_internal_doc<'a>(
10611061
find_signature(name, internal_doc).and_then(get_signature)
10621062
}
10631063

1064+
// _PyType_GetDocFromInternalDoc in CPython
1065+
fn get_doc_from_internal_doc<'a>(name: &str, internal_doc: &'a str) -> &'a str {
1066+
// Similar to CPython's _PyType_DocWithoutSignature
1067+
// If the doc starts with the type name and a '(', it's a signature
1068+
if let Some(doc_without_sig) = find_signature(name, internal_doc) {
1069+
// Find where the signature ends
1070+
if let Some(sig_end_pos) = doc_without_sig.find(SIGNATURE_END_MARKER) {
1071+
let after_sig = &doc_without_sig[sig_end_pos + SIGNATURE_END_MARKER.len()..];
1072+
// Return the documentation after the signature, or empty string if none
1073+
return after_sig;
1074+
}
1075+
}
1076+
// If no signature found, return the whole doc
1077+
internal_doc
1078+
}
1079+
10641080
impl GetAttr for PyType {
10651081
fn getattro(zelf: &Py<Self>, name_str: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
10661082
#[cold]
@@ -1122,6 +1138,55 @@ impl Py<PyType> {
11221138
PyTuple::new_unchecked(elements.into_boxed_slice())
11231139
}
11241140

1141+
#[pygetset(magic)]
1142+
fn doc(&self, vm: &VirtualMachine) -> PyResult {
1143+
// Similar to CPython's type_get_doc
1144+
// For non-heap types (static types), check if there's an internal doc
1145+
if !self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) {
1146+
if let Some(internal_doc) = self.slots.doc {
1147+
// Process internal doc, removing signature if present
1148+
let doc_str = get_doc_from_internal_doc(&self.name(), internal_doc);
1149+
return Ok(vm.ctx.new_str(doc_str).into());
1150+
}
1151+
}
1152+
1153+
// Check if there's a __doc__ in the type's dict
1154+
if let Some(doc_attr) = self.get_attr(vm.ctx.intern_str("__doc__")) {
1155+
// If it's a descriptor, call its __get__ method
1156+
let descr_get = doc_attr
1157+
.class()
1158+
.mro_find_map(|cls| cls.slots.descr_get.load());
1159+
if let Some(descr_get) = descr_get {
1160+
descr_get(doc_attr, None, Some(self.to_owned().into()), vm)
1161+
} else {
1162+
Ok(doc_attr)
1163+
}
1164+
} else {
1165+
Ok(vm.ctx.none())
1166+
}
1167+
}
1168+
1169+
#[pygetset(magic, setter)]
1170+
fn set_doc(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
1171+
// Similar to CPython's type_set_doc
1172+
let value = value.ok_or_else(|| {
1173+
vm.new_type_error(format!(
1174+
"cannot delete '__doc__' attribute of type '{}'",
1175+
self.name()
1176+
))
1177+
})?;
1178+
1179+
// Check if we can set this special type attribute
1180+
self.check_set_special_type_attr(&value, identifier!(vm, __doc__), vm)?;
1181+
1182+
// Set the __doc__ in the type's dict
1183+
self.attributes
1184+
.write()
1185+
.insert(identifier!(vm, __doc__), value);
1186+
1187+
Ok(())
1188+
}
1189+
11251190
#[pymethod(magic)]
11261191
fn dir(&self) -> PyList {
11271192
let attributes: Vec<PyObjectRef> = self

vm/src/class.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ pub trait PyClassImpl: PyClassDef {
9696
}
9797
Self::impl_extend_class(ctx, class);
9898
if let Some(doc) = Self::DOC {
99-
class.set_attr(identifier!(ctx, __doc__), ctx.new_str(doc).into());
99+
// Only set __doc__ if it doesn't already exist (e.g., as a member descriptor)
100+
// This matches CPython's behavior in type_dict_set_doc
101+
let doc_attr_name = identifier!(ctx, __doc__);
102+
if class.attributes.read().get(doc_attr_name).is_none() {
103+
class.set_attr(doc_attr_name, ctx.new_str(doc).into());
104+
}
100105
}
101106
if let Some(module_name) = Self::MODULE_NAME {
102107
class.set_attr(

0 commit comments

Comments
 (0)