From 192ba371e487635e01a34ef75574cb1b346576a2 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Fri, 1 May 2026 22:58:16 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20function=20attribute=20tests:=20=5F=5Fdef?= =?UTF-8?q?aults=5F=5F=20del,=20=5F=5Fcode=5F=5F=20free=20vars=20ch?= =?UTF-8?q?=E2=80=A6=20(#7749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix function attribute tests: __defaults__ del, __code__ free vars check, bound method dir() - Support `del func.__defaults__` and `del func.__kwdefaults__` by changing setter signatures to `PySetterValue>` so Delete is treated as setting the value to None (matching CPython behaviour) - Validate free-variable count when assigning to `func.__code__`: raise ValueError when the new code object's freevars count doesn't match the existing closure size - Add `__dir__` to `PyBoundMethod` that delegates to the underlying function's dir(), so attributes set on the function show up in `dir(bound_method)` - Remove `@unittest.expectedFailure` from the four tests that now pass: test_blank_func_defaults, test_func_default_args, test___code__, test_dir_includes_correct_attrs * Remove expectedFailure markers for now-passing tests --- Lib/test/test_funcattrs.py | 4 -- Lib/test/test_unittest/testmock/testpatch.py | 1 - crates/vm/src/builtins/function.rs | 64 +++++++++++++++++--- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py index f24521341..ff696c5c1 100644 --- a/Lib/test/test_funcattrs.py +++ b/Lib/test/test_funcattrs.py @@ -50,7 +50,6 @@ class FunctionPropertiesTest(FuncAttrsTest): def test_module(self): self.assertEqual(self.b.__module__, __name__) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_dir_includes_correct_attrs(self): self.b.known_attr = 7 self.assertIn('known_attr', dir(self.b), @@ -238,7 +237,6 @@ class FunctionPropertiesTest(FuncAttrsTest): func.__type_params__ = (T,) self.assertEqual(func.__type_params__, (T,)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test___code__(self): num_one, num_two = 7, 8 def a(): pass @@ -269,13 +267,11 @@ class FunctionPropertiesTest(FuncAttrsTest): self.fail("__code__ with different numbers of free vars should " "not be possible") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_blank_func_defaults(self): self.assertEqual(self.b.__defaults__, None) del self.b.__defaults__ self.assertEqual(self.b.__defaults__, None) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_func_default_args(self): def first_func(a, b): return a+b diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index 4f4b7f757..bd85fdcfc 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -2012,7 +2012,6 @@ class PatchTest(unittest.TestCase): self.assertEqual(dic2, origdic2) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_special_attrs(self): def foo(x=0): """TEST""" diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index f97037560..770af5621 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -2,8 +2,8 @@ mod jit; use super::{ - PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyModule, PyStr, PyStrRef, PyTuple, - PyTupleRef, PyType, object, + PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyList, PyModule, PyStr, PyStrRef, + PyTuple, PyTupleRef, PyType, object, }; use crate::common::hash::PyHash; use crate::common::lock::PyMutex; @@ -788,7 +788,17 @@ impl PyFunction { } #[pygetset(setter)] - fn set___code__(&self, code: PyRef, vm: &VirtualMachine) { + fn set___code__(&self, code: PyRef, vm: &VirtualMachine) -> PyResult<()> { + let n_free = code.freevars.len(); + let n_closure = self.closure.as_ref().map_or(0, |c| c.len()); + if n_closure != n_free { + return Err(vm.new_value_error(format!( + "{}() requires a code object with {} free vars, not {}", + self.qualname.lock(), + n_closure, + n_free, + ))); + } #[cfg(feature = "jit")] let mut jit_guard = self.jitted_code.lock(); self.code.swap_to_temporary_refs(code, vm); @@ -797,6 +807,7 @@ impl PyFunction { *jit_guard = None; } self.func_version.store(0, Relaxed); + Ok(()) } #[pygetset] @@ -804,8 +815,11 @@ impl PyFunction { self.defaults_and_kwdefaults.lock().0.clone() } #[pygetset(setter)] - fn set___defaults__(&self, defaults: Option) { - self.defaults_and_kwdefaults.lock().0 = defaults; + fn set___defaults__(&self, defaults: PySetterValue>) { + self.defaults_and_kwdefaults.lock().0 = match defaults { + PySetterValue::Assign(d) => d, + PySetterValue::Delete => None, + }; self.func_version.store(0, Relaxed); } @@ -814,8 +828,11 @@ impl PyFunction { self.defaults_and_kwdefaults.lock().1.clone() } #[pygetset(setter)] - fn set___kwdefaults__(&self, kwdefaults: Option) { - self.defaults_and_kwdefaults.lock().1 = kwdefaults; + fn set___kwdefaults__(&self, kwdefaults: PySetterValue>) { + self.defaults_and_kwdefaults.lock().1 = match kwdefaults { + PySetterValue::Assign(d) => d, + PySetterValue::Delete => None, + }; self.func_version.store(0, Relaxed); } @@ -1308,6 +1325,39 @@ impl PyBoundMethod { fn __module__(&self, vm: &VirtualMachine) -> Option { self.function.get_attr("__module__", vm).ok() } + + #[pymethod] + fn __dir__(&self, vm: &VirtualMachine) -> PyResult { + let func_dir = vm.dir(Some(self.function.clone()))?; + + let bound_only = [ + "__self__", + "__func__", + "__doc__", + "__module__", + "__call__", + "__get__", + "__repr__", + ]; + + let mut seen = std::collections::HashSet::new(); + let mut result: Vec = Vec::new(); + + for item in func_dir.borrow_vec().iter() { + if let Ok(s) = item.clone().downcast::() { + seen.insert(s.as_wtf8().to_string()); + } + result.push(item.clone()); + } + + for name in bound_only { + if seen.insert(name.to_owned()) { + result.push(vm.ctx.new_str(name).into()); + } + } + + Ok(PyList::from(result)) + } } impl PyPayload for PyBoundMethod {