From f893da6d30f62f3b9f3b9f5758e8554e1a4e43a0 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 10 Dec 2025 14:29:59 +0900 Subject: [PATCH 1/2] __slots__ xor __dict__ --- Lib/test/datetimetester.py | 2 -- Lib/test/test_contextlib.py | 2 -- Lib/test/test_contextlib_async.py | 2 -- Lib/test/test_dataclasses.py | 4 ---- Lib/test/test_decimal.py | 4 ---- Lib/test/test_fractions.py | 2 -- Lib/test/test_functools.py | 1 - Lib/test/test_os.py | 1 - Lib/test/test_statistics.py | 2 -- Lib/test/test_typing.py | 2 -- crates/vm/src/builtins/object.rs | 12 +++++++++--- crates/vm/src/builtins/type.rs | 12 ++++++++++-- crates/vm/src/stdlib/typevar.rs | 7 ++++++- 13 files changed, 25 insertions(+), 28 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 018b0c4d03..2636c1fbb9 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -5455,8 +5455,6 @@ def test_bug_1028306(self): self.assertEqual(as_datetime, datetime_sc) self.assertEqual(datetime_sc, as_datetime) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_extra_attributes(self): with self.assertWarns(DeprecationWarning): utcnow = datetime.utcnow() diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 0982a21b2f..9bb3fd0179 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -24,8 +24,6 @@ def __exit__(self, *args): manager = DefaultEnter() self.assertIs(manager.__enter__(), manager) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_slots(self): class DefaultContextManager(AbstractContextManager): __slots__ = () diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index f7edcfe55d..d7331c4d43 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -27,8 +27,6 @@ async def __aexit__(self, *args): async with manager as context: self.assertIs(manager, context) - # TODO: RUSTPYTHON - @unittest.expectedFailure async def test_slots(self): class DefaultAsyncContextManager(AbstractAsyncContextManager): __slots__ = () diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 46430d3231..f11edd957c 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2776,8 +2776,6 @@ class C: class TestSlots(unittest.TestCase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_simple(self): @dataclass class C: @@ -2819,8 +2817,6 @@ class Derived(Base): # We can add a new field to the derived instance. d.z = 10 - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_generated_slots(self): @dataclass(slots=True) class C: diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 163ca92bb4..01b0c06196 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -2923,10 +2923,6 @@ class CPythonAPItests(PythonAPItests, unittest.TestCase): class PyPythonAPItests(PythonAPItests, unittest.TestCase): decimal = P - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_complex(self): # TODO(RUSTPYTHON): Remove this test when it pass - return super().test_complex() class ContextAPItests: diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 41f93390b5..5c74e36a18 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -1137,8 +1137,6 @@ def test_copy_deepcopy_pickle(self): self.assertTypedEquals(dr, copy(dr)) self.assertTypedEquals(dr, deepcopy(dr)) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_slots(self): # Issue 4998 r = F(13, 7) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 8050c4c897..6de5d14bf7 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3278,7 +3278,6 @@ def test_cached_attribute_name_differs_from_func_name(self): self.assertEqual(item.get_cost(), 4) self.assertEqual(item.cached_cost, 3) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_object_with_slots(self): item = CachedCostItemWithSlots() with self.assertRaisesRegex( diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 4fefc9d88a..6fce916c7d 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -5332,7 +5332,6 @@ class A(os.PathLike): def test_pathlike_class_getitem(self): self.assertIsInstance(os.PathLike[bytes], types.GenericAlias) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_pathlike_subclass_slots(self): class A(os.PathLike): __slots__ = () diff --git a/Lib/test/test_statistics.py b/Lib/test/test_statistics.py index 22bd17908a..9c2714e99d 100644 --- a/Lib/test/test_statistics.py +++ b/Lib/test/test_statistics.py @@ -2889,8 +2889,6 @@ class TestNormalDist: # inaccurate. There isn't much we can do about this short of # implementing our own implementations from scratch. - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_slots(self): nd = self.module.NormalDist(300, 23) with self.assertRaises(TypeError): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4dd3cd8b78..db0dc916f1 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5269,7 +5269,6 @@ def test_weakref_all(self): for t in things: self.assertEqual(weakref.ref(t)(), t) - @unittest.expectedFailure # TODO: RUSTPYTHON - __slots__ with Generic doesn't prevent new attributes def test_parameterized_slots(self): T = TypeVar('T') class C(Generic[T]): @@ -5289,7 +5288,6 @@ def foo(x: C['C']): ... self.assertEqual(get_type_hints(foo, globals(), locals())['x'], C[C]) self.assertEqual(copy(C[int]), deepcopy(C[int])) - @unittest.expectedFailure # TODO: RUSTPYTHON - __slots__ with Generic doesn't prevent new attributes def test_parameterized_slots_dict(self): T = TypeVar('T') class D(Generic[T]): diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 810e3c2540..94a1e0c9ad 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -66,10 +66,16 @@ impl Constructor for PyBaseObject { } // more or less __new__ operator - let dict = if cls.is(vm.ctx.types.object_type) { - None - } else { + // Only create dict if the class has HAS_DICT flag (i.e., __slots__ was not defined + // or __dict__ is in __slots__) + let dict = if cls + .slots + .flags + .has_feature(crate::types::PyTypeFlags::HAS_DICT) + { Some(vm.ctx.new_dict()) + } else { + None }; // Ensure that all abstract methods are implemented before instantiating instance. diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 883be4f8bf..36b1997db6 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -307,7 +307,12 @@ impl PyType { ) -> Result, String> { let mro = Self::resolve_mro(&bases)?; - if base.slots.flags.has_feature(PyTypeFlags::HAS_DICT) { + // Inherit HAS_DICT from any base in MRO that has it + // (not just the first base, as any base with __dict__ means subclass needs it too) + if mro + .iter() + .any(|b| b.slots.flags.has_feature(PyTypeFlags::HAS_DICT)) + { slots.flags |= PyTypeFlags::HAS_DICT } @@ -1223,7 +1228,10 @@ impl Constructor for PyType { // since they inherit it from type // Add __dict__ descriptor after type creation to ensure correct __objclass__ - if !base_is_type { + // Only add if: + // 1. base is not type (type subclasses inherit __dict__ from type) + // 2. the class has HAS_DICT flag (i.e., __slots__ was not defined or __dict__ is in __slots__) + if !base_is_type && typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT) { let __dict__ = identifier!(vm, __dict__); if !typ.attributes.read().contains_key(&__dict__) { unsafe { diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index 11c20ba787..4d56fb3ce3 100644 --- a/crates/vm/src/stdlib/typevar.rs +++ b/crates/vm/src/stdlib/typevar.rs @@ -1,6 +1,6 @@ // spell-checker:ignore typevarobject funcobj use crate::{ - AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + AsObject, Context, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyTupleRef, PyTypeRef, pystr::AsPyStr}, common::lock::PyMutex, function::{FuncArgs, IntoFuncArgs, PyComparisonValue}, @@ -972,6 +972,11 @@ pub struct Generic {} #[pyclass(flags(BASETYPE))] impl Generic { + #[pyattr] + fn __slots__(ctx: &Context) -> PyTupleRef { + ctx.empty_tuple.clone() + } + #[pyclassmethod] fn __class_getitem__(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { call_typing_args_kwargs("_generic_class_getitem", cls, args, vm) From 8718dc422c52b12cedafa6147b3ecbcda696d1ad Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 10 Dec 2025 20:57:44 +0900 Subject: [PATCH 2/2] mangle_name for `__` prefixed members --- crates/vm/src/builtins/type.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 36b1997db6..b9c996371a 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1185,25 +1185,27 @@ impl Constructor for PyType { if let Some(ref slots) = heaptype_slots { let mut offset = base_member_count; + let class_name = typ.name().to_string(); for member in slots.as_slice() { + // Apply name mangling for private attributes (__x -> _ClassName__x) + let mangled_name = mangle_name(&class_name, member.as_str()); let member_def = PyMemberDef { - name: member.to_string(), + name: mangled_name.clone(), kind: MemberKind::ObjectEx, getter: MemberGetter::Offset(offset), setter: MemberSetter::Offset(offset), doc: None, }; + let attr_name = vm.ctx.intern_str(mangled_name.as_str()); let member_descriptor: PyRef = vm.ctx.new_pyref(PyMemberDescriptor { common: PyDescriptorOwned { typ: typ.clone(), - name: vm.ctx.intern_str(member.as_str()), + name: attr_name, qualname: PyRwLock::new(None), }, member: member_def, }); - - let attr_name = vm.ctx.intern_str(member.as_str()); // __slots__ attributes always get a member descriptor // (this overrides any inherited attribute from MRO) typ.set_attr(attr_name, member_descriptor.into()); @@ -1793,6 +1795,18 @@ fn best_base<'a>(bases: &'a [PyTypeRef], vm: &VirtualMachine) -> PyResult<&'a Py Ok(base.unwrap()) } +/// Apply Python name mangling for private attributes. +/// `__x` becomes `_ClassName__x` if inside a class. +fn mangle_name(class_name: &str, name: &str) -> String { + // Only mangle names starting with __ and not ending with __ + if !name.starts_with("__") || name.ends_with("__") || name.contains('.') { + return name.to_string(); + } + // Strip leading underscores from class name + let class_name = class_name.trim_start_matches('_'); + format!("_{}{}", class_name, name) +} + #[cfg(test)] mod tests { use super::*;