Fix function attribute tests: __defaults__ del, __code__ free vars ch… (#7749)

* 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<Option<...>>` 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
This commit is contained in:
Jiseok CHOI
2026-05-01 22:58:16 +09:00
committed by GitHub
parent d25195ed0e
commit 192ba371e4
3 changed files with 57 additions and 12 deletions

View File

@@ -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

View File

@@ -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"""

View File

@@ -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<PyCode>, vm: &VirtualMachine) {
fn set___code__(&self, code: PyRef<PyCode>, 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<PyTupleRef>) {
self.defaults_and_kwdefaults.lock().0 = defaults;
fn set___defaults__(&self, defaults: PySetterValue<Option<PyTupleRef>>) {
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<PyDictRef>) {
self.defaults_and_kwdefaults.lock().1 = kwdefaults;
fn set___kwdefaults__(&self, kwdefaults: PySetterValue<Option<PyDictRef>>) {
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<PyObjectRef> {
self.function.get_attr("__module__", vm).ok()
}
#[pymethod]
fn __dir__(&self, vm: &VirtualMachine) -> PyResult<PyList> {
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<PyObjectRef> = Vec::new();
for item in func_dir.borrow_vec().iter() {
if let Ok(s) = item.clone().downcast::<PyStr>() {
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 {