Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = ()
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_contextlib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = ()
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2776,8 +2776,6 @@ class C:


class TestSlots(unittest.TestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_simple(self):
@dataclass
class C:
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = ()
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand All @@ -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]):
Expand Down
12 changes: 9 additions & 3 deletions crates/vm/src/builtins/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 28 additions & 6 deletions crates/vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,12 @@ impl PyType {
) -> Result<PyRef<Self>, 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
}

Expand Down Expand Up @@ -1180,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<PyMemberDescriptor> =
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());
Expand All @@ -1223,7 +1230,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 {
Expand Down Expand Up @@ -1785,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::*;
Expand Down
7 changes: 6 additions & 1 deletion crates/vm/src/stdlib/typevar.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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)
Expand Down
Loading