From 333af80f44f04526e47b558b85e20481eab49f7f Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 20 Mar 2026 22:17:47 +0900 Subject: [PATCH 1/3] Add GetDescriptor for PyBoundMethod (return self) CPython's method_descr_get always returns the bound method unchanged. This preserves the original binding when __get__ is called on an already-bound method (e.g. a.meth.__get__(b, B) still returns a). Co-Authored-By: Claude Opus 4.6 (1M context) --- Lib/test/test_descr.py | 1 - crates/vm/src/builtins/function.rs | 21 ++++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 3a66cdcdea..7988f946ca 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -5130,7 +5130,6 @@ class Child(Parent): gc.collect() self.assertEqual(Parent.__subclasses__(), []) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_instance_method_get_behavior(self): # test case for gh-113157 diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 0003720c66..996225838c 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -1216,6 +1216,17 @@ impl GetAttr for PyBoundMethod { } } +impl GetDescriptor for PyBoundMethod { + fn descr_get( + zelf: PyObjectRef, + _obj: Option, + _cls: Option, + _vm: &VirtualMachine, + ) -> PyResult { + Ok(zelf) + } +} + #[derive(FromArgs)] pub struct PyBoundMethodNewArgs { #[pyarg(positional)] @@ -1258,7 +1269,15 @@ impl PyBoundMethod { } #[pyclass( - with(Callable, Comparable, Hashable, GetAttr, Constructor, Representable), + with( + Callable, + Comparable, + Hashable, + GetAttr, + GetDescriptor, + Constructor, + Representable + ), flags(IMMUTABLETYPE, HAS_WEAKREF) )] impl PyBoundMethod { From 1a3fd9f8917c5927a4050d5fb212887d25bb7d0b Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 20 Mar 2026 22:29:08 +0900 Subject: [PATCH 2/3] Add constructor validation for PyBoundMethod Reject non-callable functions and None instances, matching CPython's method_new which checks PyCallable_Check(func) and instance != Py_None. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/vm/src/builtins/function.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 996225838c..9c89b916ea 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -1241,8 +1241,14 @@ impl Constructor for PyBoundMethod { fn py_new( _cls: &Py, Self::Args { function, object }: Self::Args, - _vm: &VirtualMachine, + vm: &VirtualMachine, ) -> PyResult { + if !function.is_callable() { + return Err(vm.new_type_error("first argument must be callable".to_owned())); + } + if vm.is_none(&object) { + return Err(vm.new_type_error("instance must not be None".to_owned())); + } Ok(Self::new(object, function)) } } From b725ca7ecb04cedd5e026515c99ec90eec417db3 Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 20 Mar 2026 22:32:17 +0900 Subject: [PATCH 3/3] Fix PyBoundMethod __reduce__ to propagate errors Previously swallowed errors from get_attr with .ok(), silently returning None. Now propagates errors matching CPython's method_reduce. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/vm/src/builtins/function.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 9c89b916ea..a223815162 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -1291,11 +1291,11 @@ impl PyBoundMethod { fn __reduce__( &self, vm: &VirtualMachine, - ) -> (Option, (PyObjectRef, Option)) { - let builtins_getattr = vm.builtins.get_attr("getattr", vm).ok(); + ) -> PyResult<(PyObjectRef, (PyObjectRef, PyObjectRef))> { + let builtins_getattr = vm.builtins.get_attr("getattr", vm)?; let func_self = self.object.clone(); - let func_name = self.function.get_attr("__name__", vm).ok(); - (builtins_getattr, (func_self, func_name)) + let func_name = self.function.get_attr("__name__", vm)?; + Ok((builtins_getattr, (func_self, func_name))) } #[pygetset]