mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Align type layouts and generic descriptors with Python 3.15 (#9175)
Import canonical descriptor tests through update_lib, preserving existing guards. Support tuple member slots, distinguish base reassignment from class layout compatibility, and share object-owned generic descriptors. Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
1 parent
fc9f4a9541
commit
75a2417043
5 files changed
+506
-158
No files matched your search
Vendored
+380
-88
@@ -1329,6 +1329,18 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
with self.assertRaisesRegex(AttributeError, "'X' object has no attribute 'a'"):
|
||||
X().a
|
||||
|
||||
def test_slots_after_items(self):
|
||||
class C(tuple):
|
||||
__slots__ = ['a']
|
||||
x = C((1, 2, 3))
|
||||
self.assertNotHasAttr(x, "__dict__")
|
||||
self.assertNotHasAttr(x, "a")
|
||||
x.a = 42
|
||||
self.assertEqual(x.a, 42)
|
||||
del x.a
|
||||
self.assertNotHasAttr(x, "a")
|
||||
self.assertEqual(x, (1, 2, 3))
|
||||
|
||||
def test_slots_special(self):
|
||||
# Testing __dict__ and __weakref__ in __slots__...
|
||||
class D(object):
|
||||
@@ -1338,18 +1350,17 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
self.assertNotHasAttr(a, "__weakref__")
|
||||
a.foo = 42
|
||||
self.assertEqual(a.__dict__, {"foo": 42})
|
||||
with self.assertRaises(TypeError):
|
||||
weakref.ref(a)
|
||||
|
||||
class W(object):
|
||||
__slots__ = ["__weakref__"]
|
||||
a = W()
|
||||
self.assertHasAttr(a, "__weakref__")
|
||||
self.assertNotHasAttr(a, "__dict__")
|
||||
try:
|
||||
with self.assertRaises(AttributeError):
|
||||
a.foo = 42
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.fail("shouldn't be allowed to set a.foo")
|
||||
self.assertIs(weakref.ref(a)(), a)
|
||||
|
||||
class C1(W, D):
|
||||
__slots__ = []
|
||||
@@ -1358,6 +1369,7 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
self.assertHasAttr(a, "__weakref__")
|
||||
a.foo = 42
|
||||
self.assertEqual(a.__dict__, {"foo": 42})
|
||||
self.assertIs(weakref.ref(a)(), a)
|
||||
|
||||
class C2(D, W):
|
||||
__slots__ = []
|
||||
@@ -1366,6 +1378,80 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
self.assertHasAttr(a, "__weakref__")
|
||||
a.foo = 42
|
||||
self.assertEqual(a.__dict__, {"foo": 42})
|
||||
self.assertIs(weakref.ref(a)(), a)
|
||||
|
||||
@unittest.skipIf(_testcapi is None, 'need the _testcapi module')
|
||||
def test_slots_special_before_items(self):
|
||||
class D(_testcapi.HeapCCollection):
|
||||
__slots__ = ["__dict__"]
|
||||
a = D(1, 2, 3)
|
||||
self.assertHasAttr(a, "__dict__")
|
||||
self.assertNotHasAttr(a, "__weakref__")
|
||||
a.foo = 42
|
||||
self.assertEqual(a.__dict__, {"foo": 42})
|
||||
with self.assertRaises(TypeError):
|
||||
weakref.ref(a)
|
||||
del a.__dict__
|
||||
self.assertNotHasAttr(a, "foo")
|
||||
self.assertEqual(a.__dict__, {})
|
||||
self.assertEqual(list(a), [1, 2, 3])
|
||||
|
||||
class W(_testcapi.HeapCCollection):
|
||||
__slots__ = ["__weakref__"]
|
||||
a = W(1, 2, 3)
|
||||
self.assertHasAttr(a, "__weakref__")
|
||||
self.assertNotHasAttr(a, "__dict__")
|
||||
with self.assertRaises(AttributeError):
|
||||
a.foo = 42
|
||||
self.assertIs(weakref.ref(a)(), a)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
class X(_testcapi.HeapCCollection):
|
||||
__slots__ = ['x']
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
class X(_testcapi.HeapCCollection):
|
||||
__slots__ = ['__dict__', 'x']
|
||||
|
||||
@support.subTests(('base', 'arg'), [
|
||||
(tuple, (1, 2, 3)),
|
||||
(int, 9876543210**2),
|
||||
(bytes, b'ab'),
|
||||
])
|
||||
def test_slots_special_after_items(self, base, arg):
|
||||
class D(base):
|
||||
__slots__ = ["__dict__"]
|
||||
a = D(arg)
|
||||
self.assertHasAttr(a, "__dict__")
|
||||
self.assertNotHasAttr(a, "__weakref__")
|
||||
a.foo = 42
|
||||
self.assertEqual(a.__dict__, {"foo": 42})
|
||||
with self.assertRaises(TypeError):
|
||||
weakref.ref(a)
|
||||
del a.__dict__
|
||||
self.assertNotHasAttr(a, "foo")
|
||||
self.assertEqual(a.__dict__, {})
|
||||
self.assertEqual(a, base(arg))
|
||||
|
||||
class W(base):
|
||||
__slots__ = ["__weakref__"]
|
||||
a = W(arg)
|
||||
self.assertHasAttr(a, "__weakref__")
|
||||
self.assertNotHasAttr(a, "__dict__")
|
||||
with self.assertRaises(AttributeError):
|
||||
a.foo = 42
|
||||
self.assertIs(weakref.ref(a)(), a)
|
||||
self.assertEqual(a, base(arg))
|
||||
|
||||
@support.subTests('base', [int, bytes] +
|
||||
([_testcapi.HeapCCollection] if _testcapi else []))
|
||||
def test_unsupported_slots(self, base):
|
||||
with self.assertRaises(TypeError):
|
||||
class X(base):
|
||||
__slots__ = ['x']
|
||||
with self.assertRaises(TypeError):
|
||||
class X(base):
|
||||
__slots__ = ['__dict__', 'x']
|
||||
|
||||
def test_slots_special2(self):
|
||||
# Testing __qualname__ and __classcell__ in __slots__
|
||||
@@ -1650,6 +1736,18 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
del method.__annotate__
|
||||
self.assertIs(method.__annotate__, original_annotate)
|
||||
|
||||
def test_classmethod_without_dict_access(self):
|
||||
class Spam:
|
||||
@classmethod
|
||||
def method(cls, x, y):
|
||||
pass
|
||||
|
||||
obj = Spam.__dict__['method']
|
||||
self.assertIsInstance(obj, classmethod)
|
||||
self.assertEqual(obj.__annotations__, {})
|
||||
self.assertEqual(obj.__name__, 'method')
|
||||
self.assertEqual(obj.__module__, __name__)
|
||||
|
||||
def test_staticmethod_annotations_without_dict_access(self):
|
||||
# gh-125017: this used to crash
|
||||
class Spam:
|
||||
@@ -1660,15 +1758,8 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
obj = Spam.__dict__['__new__']
|
||||
self.assertIsInstance(obj, staticmethod)
|
||||
self.assertEqual(obj.__annotations__, {})
|
||||
|
||||
@support.refcount_test
|
||||
def test_refleaks_in_classmethod___init__(self):
|
||||
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
|
||||
cm = classmethod(None)
|
||||
refs_before = gettotalrefcount()
|
||||
for i in range(100):
|
||||
cm.__init__(None)
|
||||
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
|
||||
self.assertEqual(obj.__name__, '__new__')
|
||||
self.assertEqual(obj.__module__, __name__)
|
||||
|
||||
@support.impl_detail("the module 'xxsubtype' is internal")
|
||||
@unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
|
||||
@@ -1767,15 +1858,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
del sm.x
|
||||
self.assertNotHasAttr(sm, "x")
|
||||
|
||||
@support.refcount_test
|
||||
def test_refleaks_in_staticmethod___init__(self):
|
||||
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
|
||||
sm = staticmethod(None)
|
||||
refs_before = gettotalrefcount()
|
||||
for i in range(100):
|
||||
sm.__init__(None)
|
||||
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
|
||||
|
||||
@support.impl_detail("the module 'xxsubtype' is internal")
|
||||
@unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
|
||||
def test_staticmethods_in_c(self):
|
||||
@@ -4111,42 +4193,195 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
self.assertEqual(e.a, 2)
|
||||
self.assertEqual(C2.__subclasses__(), [D])
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError,
|
||||
"cannot delete '__bases__' attribute of type 'D'"):
|
||||
del D.__bases__
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
else:
|
||||
self.fail("shouldn't be able to delete .__bases__")
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError, 'can only assign non-empty tuple'):
|
||||
D.__bases__ = ()
|
||||
except TypeError as msg:
|
||||
if str(msg) == "a new-style class can't have only classic bases":
|
||||
self.fail("wrong error message for .__bases__ = ()")
|
||||
else:
|
||||
self.fail("shouldn't be able to set .__bases__ to ()")
|
||||
|
||||
try:
|
||||
D.__bases__ = (D,)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
# actually, we'll have crashed by here...
|
||||
self.fail("shouldn't be able to create inheritance cycles")
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError, 'can only assign tuple'):
|
||||
D.__bases__ = [C]
|
||||
with self.assertRaisesRegex(TypeError, 'duplicate base class'):
|
||||
D.__bases__ = (C, C)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.fail("didn't detect repeated base classes")
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
|
||||
D.__bases__ = (D,)
|
||||
with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
|
||||
D.__bases__ = (E,)
|
||||
except TypeError:
|
||||
|
||||
class A:
|
||||
__slots__ = ()
|
||||
def __repr__(self):
|
||||
return '<A>'
|
||||
class A_with_dict:
|
||||
__slots__ = ('__dict__',)
|
||||
def __repr__(self):
|
||||
return '<A_with_dict>'
|
||||
class A_with_dict_weakref:
|
||||
def __repr__(self):
|
||||
return '<A_with_dict_weakref>'
|
||||
class A_with_slots:
|
||||
__slots__ = ('x',)
|
||||
def __repr__(self):
|
||||
return '<A_with_slots>'
|
||||
class A_with_slots_dict:
|
||||
__slots__ = ('x', '__dict__')
|
||||
def __repr__(self):
|
||||
return '<A_with_slots_dict>'
|
||||
|
||||
class B:
|
||||
__slots__ = ()
|
||||
b = B()
|
||||
r = repr(b)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B.__bases__ = (int,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B.__bases__ = (A_with_dict_weakref,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B.__bases__ = (A_with_dict,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B.__bases__ = (A_with_slots,)
|
||||
B.__bases__ = (A,)
|
||||
self.assertNotHasAttr(b, '__dict__')
|
||||
self.assertNotHasAttr(b, '__weakref__')
|
||||
self.assertEqual(repr(b), '<A>')
|
||||
B.__bases__ = (object,)
|
||||
self.assertEqual(repr(b), r)
|
||||
|
||||
class B_with_dict_weakref:
|
||||
pass
|
||||
else:
|
||||
self.fail("shouldn't be able to create inheritance cycles")
|
||||
b = B_with_dict_weakref()
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B.__bases__ = (A_with_slots,)
|
||||
B_with_dict_weakref.__bases__ = (A_with_dict_weakref,)
|
||||
self.assertEqual(repr(b), '<A_with_dict_weakref>')
|
||||
B_with_dict_weakref.__bases__ = (A_with_dict,)
|
||||
self.assertEqual(repr(b), '<A_with_dict>')
|
||||
B_with_dict_weakref.__bases__ = (A,)
|
||||
self.assertEqual(repr(b), '<A>')
|
||||
B_with_dict_weakref.__bases__ = (object,)
|
||||
|
||||
class B_with_slots:
|
||||
__slots__ = ('x',)
|
||||
b = B_with_slots()
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_with_slots.__bases__ = (A_with_dict_weakref,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_with_slots.__bases__ = (A_with_dict,)
|
||||
B_with_slots.__bases__ = (A,)
|
||||
self.assertEqual(repr(b), '<A>')
|
||||
|
||||
class B_with_slots_dict:
|
||||
__slots__ = ('x', '__dict__')
|
||||
b = B_with_slots_dict()
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_with_slots_dict.__bases__ = (A_with_dict_weakref,)
|
||||
B_with_slots_dict.__bases__ = (A_with_dict,)
|
||||
self.assertEqual(repr(b), '<A_with_dict>')
|
||||
B_with_slots_dict.__bases__ = (A,)
|
||||
self.assertEqual(repr(b), '<A>')
|
||||
|
||||
class B_with_slots_dict_weakref:
|
||||
__slots__ = ('x', '__dict__', '__weakref__')
|
||||
b = B_with_slots_dict_weakref()
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_with_slots_dict_weakref.__bases__ = (A_with_slots_dict,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_with_slots_dict_weakref.__bases__ = (A_with_slots,)
|
||||
B_with_slots_dict_weakref.__bases__ = (A_with_dict_weakref,)
|
||||
self.assertEqual(repr(b), '<A_with_dict_weakref>')
|
||||
B_with_slots_dict_weakref.__bases__ = (A_with_dict,)
|
||||
self.assertEqual(repr(b), '<A_with_dict>')
|
||||
B_with_slots_dict_weakref.__bases__ = (A,)
|
||||
self.assertEqual(repr(b), '<A>')
|
||||
|
||||
class C_with_slots(A_with_slots):
|
||||
__slots__ = ()
|
||||
c = C_with_slots()
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots.__bases__ = (A_with_slots_dict,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots.__bases__ = (A_with_dict_weakref,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots.__bases__ = (A_with_dict,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots.__bases__ = (A,)
|
||||
C_with_slots.__bases__ = (A_with_slots,)
|
||||
self.assertEqual(repr(c), '<A_with_slots>')
|
||||
|
||||
class C_with_slots_dict(A_with_slots):
|
||||
pass
|
||||
c = C_with_slots_dict()
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots_dict.__bases__ = (A_with_dict_weakref,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots_dict.__bases__ = (A_with_dict,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
C_with_slots_dict.__bases__ = (A,)
|
||||
C_with_slots_dict.__bases__ = (A_with_slots_dict,)
|
||||
self.assertEqual(repr(c), '<A_with_slots_dict>')
|
||||
C_with_slots_dict.__bases__ = (A_with_slots,)
|
||||
self.assertEqual(repr(c), '<A_with_slots>')
|
||||
|
||||
class A_int(int):
|
||||
__slots__ = ()
|
||||
def __repr__(self):
|
||||
return '<A_int>'
|
||||
class B_int(int):
|
||||
__slots__ = ()
|
||||
b = B_int(42)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_int.__bases__ = (object,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_int.__bases__ = (tuple,)
|
||||
with self.assertRaisesRegex(TypeError, 'is not an acceptable base type'):
|
||||
B_int.__bases__ = (bool,)
|
||||
B_int.__bases__ = (A_int,)
|
||||
self.assertEqual(repr(b), '<A_int>')
|
||||
B_int.__bases__ = (int,)
|
||||
self.assertEqual(repr(b), '42')
|
||||
|
||||
class A_tuple(tuple):
|
||||
__slots__ = ()
|
||||
def __repr__(self):
|
||||
return '<A_tuple>'
|
||||
class B_tuple(tuple):
|
||||
__slots__ = ()
|
||||
b = B_tuple((1, 2))
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_tuple.__bases__ = (object,)
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
B_tuple.__bases__ = (int,)
|
||||
B_tuple.__bases__ = (A_tuple,)
|
||||
self.assertEqual(repr(b), '<A_tuple>')
|
||||
B_tuple.__bases__ = (tuple,)
|
||||
self.assertEqual(repr(b), '(1, 2)')
|
||||
|
||||
def test_assign_bases_many_subclasses(self):
|
||||
# This is intended to check that typeobject.c:queue_slot_update() can
|
||||
# handle updating many subclasses when a slot method is re-assigned.
|
||||
class A:
|
||||
x = 'hello'
|
||||
def __call__(self):
|
||||
return 123
|
||||
def __getitem__(self, index):
|
||||
return None
|
||||
|
||||
class X:
|
||||
x = 'bye'
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
subclasses = []
|
||||
for i in range(1000):
|
||||
sc = type(f'Sub{i}', (B,), {})
|
||||
subclasses.append(sc)
|
||||
|
||||
self.assertEqual(subclasses[0]()(), 123)
|
||||
self.assertEqual(subclasses[0]().x, 'hello')
|
||||
B.__bases__ = (X,)
|
||||
with self.assertRaises(TypeError):
|
||||
subclasses[0]()()
|
||||
self.assertEqual(subclasses[0]().x, 'bye')
|
||||
|
||||
def test_builtin_bases(self):
|
||||
# Make sure all the builtin types can have their base queried without
|
||||
@@ -4171,26 +4406,14 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
class D(C):
|
||||
pass
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
L.__bases__ = (dict,)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.fail("shouldn't turn list subclass into dict subclass")
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError, 'immutable type'):
|
||||
list.__bases__ = (dict,)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.fail("shouldn't be able to assign to list.__bases__")
|
||||
|
||||
try:
|
||||
with self.assertRaisesRegex(TypeError, 'layout differs'):
|
||||
D.__bases__ = (C, list)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.fail("best_base calculation found wanting")
|
||||
|
||||
def test_unsubclassable_types(self):
|
||||
with self.assertRaises(TypeError):
|
||||
@@ -4956,7 +5179,7 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
|
||||
with self.assertRaises(TypeError) as cm:
|
||||
type(X).__dict__["__doc__"].__delete__(X)
|
||||
self.assertIn("cannot delete '__doc__' attribute of immutable type 'X'", str(cm.exception))
|
||||
self.assertIn("cannot delete '__doc__' attribute of type 'X'", str(cm.exception))
|
||||
self.assertEqual(X.__doc__, "banana")
|
||||
|
||||
def test_qualname(self):
|
||||
@@ -5173,25 +5396,30 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
with self.assertRaisesRegex(NotImplementedError, "BAR"):
|
||||
B().foo
|
||||
|
||||
def test_staticmethod_new(self):
|
||||
class MyStaticMethod(staticmethod):
|
||||
def __init__(self, func):
|
||||
pass
|
||||
def func(): pass
|
||||
sm = MyStaticMethod(func)
|
||||
self.assertEqual(repr(sm), '<staticmethod(None)>')
|
||||
self.assertIsNone(sm.__func__)
|
||||
self.assertIsNone(sm.__wrapped__)
|
||||
def test_gh146587(self):
|
||||
# See https://github.com/python/cpython/issues/146587
|
||||
|
||||
def test_classmethod_new(self):
|
||||
class MyClassMethod(classmethod):
|
||||
def __init__(self, func):
|
||||
pass
|
||||
def func(): pass
|
||||
cm = MyClassMethod(func)
|
||||
self.assertEqual(repr(cm), '<classmethod(None)>')
|
||||
self.assertIsNone(cm.__func__)
|
||||
self.assertIsNone(cm.__wrapped__)
|
||||
class A:
|
||||
def __radd__(self, other): ...
|
||||
|
||||
class B(tuple): ...
|
||||
|
||||
self.assertIsNone(() + A())
|
||||
self.assertIsNone(B() + A())
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
class T(NamedTuple):
|
||||
x: int
|
||||
|
||||
class A:
|
||||
def __init__(self, *args):
|
||||
self.lst = list(args)
|
||||
def __radd__(self, other):
|
||||
return A(*self.lst, other)
|
||||
|
||||
self.assertEqual(((1,)+A()).lst, [(1,)])
|
||||
self.assertEqual((T(x=1)+A()).lst, [T(x=1)])
|
||||
|
||||
|
||||
class DictProxyTests(unittest.TestCase):
|
||||
@@ -6040,5 +6268,69 @@ class MroTest(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestGenericDescriptors(unittest.TestCase):
|
||||
def test___dict__(self):
|
||||
class CustomClass:
|
||||
pass
|
||||
class SlotClass:
|
||||
__slots__ = ['foo']
|
||||
class SlotSubClass(SlotClass):
|
||||
pass
|
||||
class IntSubclass(int):
|
||||
pass
|
||||
|
||||
dict_descriptor = CustomClass.__dict__['__dict__']
|
||||
self.assertEqual(dict_descriptor.__objclass__, object)
|
||||
|
||||
for cls in CustomClass, SlotSubClass, IntSubclass:
|
||||
with self.subTest(cls=cls):
|
||||
self.assertIs(cls.__dict__['__dict__'], dict_descriptor)
|
||||
instance = cls()
|
||||
instance.attr = 123
|
||||
self.assertEqual(
|
||||
dict_descriptor.__get__(instance, cls),
|
||||
{'attr': 123},
|
||||
)
|
||||
with self.assertRaises(AttributeError):
|
||||
print(dict_descriptor.__get__(True, bool))
|
||||
with self.assertRaises(AttributeError):
|
||||
print(dict_descriptor.__get__(SlotClass(), SlotClass))
|
||||
|
||||
# delegation to type.__dict__
|
||||
self.assertIsInstance(
|
||||
dict_descriptor.__get__(type, type),
|
||||
types.MappingProxyType,
|
||||
)
|
||||
|
||||
def test___weakref__(self):
|
||||
class CustomClass:
|
||||
pass
|
||||
class SlotClass:
|
||||
__slots__ = ['foo']
|
||||
class SlotSubClass(SlotClass):
|
||||
pass
|
||||
class IntSubclass(int):
|
||||
pass
|
||||
|
||||
weakref_descriptor = CustomClass.__dict__['__weakref__']
|
||||
self.assertEqual(weakref_descriptor.__objclass__, object)
|
||||
|
||||
for cls in CustomClass, SlotSubClass:
|
||||
with self.subTest(cls=cls):
|
||||
self.assertIs(cls.__dict__['__weakref__'], weakref_descriptor)
|
||||
instance = cls()
|
||||
instance.attr = 123
|
||||
self.assertEqual(
|
||||
weakref_descriptor.__get__(instance, cls),
|
||||
None,
|
||||
)
|
||||
with self.assertRaises(AttributeError):
|
||||
weakref_descriptor.__get__(True, bool)
|
||||
with self.assertRaises(AttributeError):
|
||||
weakref_descriptor.__get__(SlotClass(), SlotClass)
|
||||
with self.assertRaises(AttributeError):
|
||||
weakref_descriptor.__get__(IntSubclass(), IntSubclass)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -460,7 +460,13 @@ impl PyBaseObject {
|
||||
&& !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE);
|
||||
// FIXME(#1979) cls instances might have a payload
|
||||
if both_mutable || both_module {
|
||||
super::type_::compatible_for_assignment(current_cls, &cls, "__class__", vm)?;
|
||||
super::type_::compatible_for_assignment(
|
||||
current_cls,
|
||||
&cls,
|
||||
"__class__",
|
||||
true,
|
||||
vm,
|
||||
)?;
|
||||
instance.set_class(cls, vm);
|
||||
crate::stdlib::_testinternalcapi::note_set_class();
|
||||
Ok(())
|
||||
|
||||
+116
-69
@@ -47,6 +47,32 @@ use std::collections::HashSet;
|
||||
|
||||
pub(crate) type PyTypeTupleRef = PyRef<PyTuple<PyTypeRef>>;
|
||||
|
||||
pub(crate) struct CommonTypeDescriptors {
|
||||
dict: PyRef<super::PyGetSet>,
|
||||
weakref: PyRef<super::PyGetSet>,
|
||||
}
|
||||
|
||||
impl CommonTypeDescriptors {
|
||||
pub(crate) fn new(ctx: &Context) -> Self {
|
||||
// These descriptors operate on the instance's layout, not the class
|
||||
// that first exposes them. Keeping object as their owner also lets an
|
||||
// annotation closure retain a class namespace without retaining the class.
|
||||
let dict = super::PyGetSet::new("__dict__", ctx.types.object_type, ctx)
|
||||
.with_get(subtype_get_dict)
|
||||
.with_set(subtype_set_dict)
|
||||
.with_doc(ItemDoc::static_text("dictionary for instance variables"));
|
||||
let weakref = super::PyGetSet::new("__weakref__", ctx.types.object_type, ctx)
|
||||
.with_get(subtype_get_weakref)
|
||||
.with_doc(ItemDoc::static_text(
|
||||
"list of weak references to the object",
|
||||
));
|
||||
Self {
|
||||
dict: ctx.new_pyref(dict),
|
||||
weakref: ctx.new_pyref(weakref),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(module = false, name = "type", traverse = "manual")]
|
||||
pub struct PyType {
|
||||
// tp_base. Written under the type lock (see `set_bases`); read lock-free.
|
||||
@@ -1841,11 +1867,11 @@ impl Constructor for PyType {
|
||||
// Search the bases for the proper metatype to deal with this:
|
||||
let winner = calculate_meta_class(metatype.clone(), bases.as_slice(), vm)?;
|
||||
let metatype = if !winner.is(&metatype) {
|
||||
if let Some(ref slot_new) = winner.slots.new.load() {
|
||||
// Pass it to the winner
|
||||
return slot_new(winner, args, vm);
|
||||
}
|
||||
winner
|
||||
let slot_new = winner.slots.new.load().ok_or_else(|| {
|
||||
vm.new_type_error(format!("cannot create '{}' instances", winner.slot_name()))
|
||||
})?;
|
||||
// Pass it to the winner.
|
||||
return slot_new(winner, args, vm);
|
||||
} else {
|
||||
metatype
|
||||
};
|
||||
@@ -1974,16 +2000,6 @@ impl Constructor for PyType {
|
||||
tuple.try_into_typed(vm)?
|
||||
};
|
||||
|
||||
// Any nonempty __slots__ is rejected when the base has a variable
|
||||
// item size, including a tuple of only `__dict__` or `__weakref__`.
|
||||
// Types like weakref.ref have itemsize 0 and do allow slots.
|
||||
if !slots.as_slice().is_empty() && base.slots.itemsize > 0 {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"nonempty __slots__ not supported for subtype of '{}'",
|
||||
base.name()
|
||||
)));
|
||||
}
|
||||
|
||||
// Validate slot names and track duplicates
|
||||
let mut seen_dict = false;
|
||||
let mut seen_weakref = false;
|
||||
@@ -2055,6 +2071,19 @@ impl Constructor for PyType {
|
||||
.cloned()
|
||||
.collect();
|
||||
filtered.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
|
||||
// CPython 3.15 permits named slots on tuple subclasses. Our
|
||||
// member cells live in the object prefix, independently of the
|
||||
// tuple's elements. Other variable-size bases still only accept
|
||||
// the managed __dict__ and __weakref__ slots filtered above.
|
||||
if !filtered.is_empty()
|
||||
&& base.slots.itemsize > 0
|
||||
&& !base.fast_issubclass(vm.ctx.types.tuple_type)
|
||||
{
|
||||
return Err(vm.new_type_error(format!(
|
||||
"arbitrary __slots__ not supported for subtype of '{}'",
|
||||
base.name()
|
||||
)));
|
||||
}
|
||||
let filtered_slots = PyTuple::new_ref_typed(filtered, &vm.ctx);
|
||||
|
||||
(Some(filtered_slots), has_dict, add_weakref)
|
||||
@@ -2222,7 +2251,7 @@ impl Constructor for PyType {
|
||||
// Also, type subclasses don't need their own __dict__ descriptor
|
||||
// since they inherit it from type
|
||||
|
||||
// Add __dict__ descriptor after type creation to ensure correct __objclass__
|
||||
// Share the interpreter's object-owned __dict__ descriptor.
|
||||
// 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__)
|
||||
@@ -2235,12 +2264,10 @@ impl Constructor for PyType {
|
||||
.iter()
|
||||
.any(|base| base.attributes.contains(__dict__));
|
||||
if !typ.attributes.contains(__dict__) && !has_inherited_dict {
|
||||
let getset = super::PyGetSet::new("__dict__", &typ, &vm.ctx)
|
||||
.with_get(subtype_get_dict)
|
||||
.with_set(subtype_set_dict)
|
||||
.with_doc(ItemDoc::static_text("dictionary for instance variables"));
|
||||
let descriptor = PyRef::new_ref(getset, vm.ctx.types.getset_type.to_owned(), None);
|
||||
typ.attributes.set(__dict__, descriptor.into());
|
||||
typ.attributes.set(
|
||||
__dict__,
|
||||
vm.state.common_type_descriptors.dict.clone().into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2253,14 +2280,10 @@ impl Constructor for PyType {
|
||||
.iter()
|
||||
.any(|base| base.attributes.contains(__weakref__));
|
||||
if !typ.attributes.contains(__weakref__) && !has_inherited_weakref {
|
||||
let getset = super::PyGetSet::new("__weakref__", &typ, &vm.ctx)
|
||||
.with_get(subtype_get_weakref)
|
||||
.with_set(subtype_set_weakref)
|
||||
.with_doc(ItemDoc::static_text(
|
||||
"list of weak references to the object",
|
||||
));
|
||||
let descriptor = PyRef::new_ref(getset, vm.ctx.types.getset_type.to_owned(), None);
|
||||
typ.attributes.set(__weakref__, descriptor.into());
|
||||
typ.attributes.set(
|
||||
__weakref__,
|
||||
vm.state.common_type_descriptors.weakref.clone().into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,8 +2511,8 @@ impl Py<PyType> {
|
||||
// Similar to CPython's type_set_doc
|
||||
let value = value.ok_or_else(|| {
|
||||
vm.new_type_error(format!(
|
||||
"cannot delete '__doc__' attribute of immutable type '{}'",
|
||||
self.name()
|
||||
"cannot delete '__doc__' attribute of type '{}'",
|
||||
self.slot_name()
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -2545,15 +2568,23 @@ impl Py<PyType> {
|
||||
PyType::with_type_lock(vm, || self.bases.read().clone().into_untyped())
|
||||
}
|
||||
#[pygetset(setter, name = "__bases__")]
|
||||
fn set_bases(zelf: &Self, bases_tuple: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
|
||||
fn set_bases(zelf: &Self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
|
||||
// TODO: Assigning to __bases__ is only used in typing.NamedTupleMeta.__new__
|
||||
// Rather than correctly re-initializing the class, we are skipping a few steps for now
|
||||
if zelf.slots().flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"cannot set '__bases__' attribute of immutable type '{}'",
|
||||
zelf.name()
|
||||
)));
|
||||
}
|
||||
let value = value.ok_or_else(|| {
|
||||
vm.new_type_error(format!(
|
||||
"cannot delete '__bases__' attribute of type '{}'",
|
||||
zelf.slot_name()
|
||||
))
|
||||
})?;
|
||||
zelf.check_set_special_type_attr(identifier!(vm, __bases__), vm)?;
|
||||
let bases_tuple = value.downcast::<PyTuple>().map_err(|value| {
|
||||
vm.new_type_error(format!(
|
||||
"can only assign tuple to {}.__bases__, not {}",
|
||||
zelf.slot_name(),
|
||||
value.class().slot_name()
|
||||
))
|
||||
})?;
|
||||
if bases_tuple.as_slice().is_empty() {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"can only assign non-empty tuple to {}.__bases__, not ()",
|
||||
@@ -2578,11 +2609,7 @@ impl Py<PyType> {
|
||||
|
||||
// Reject reparenting onto a base whose instances have an incompatible
|
||||
// object layout.
|
||||
let old_base_owned = zelf.base.load_owned();
|
||||
let old_base = old_base_owned
|
||||
.as_deref()
|
||||
.unwrap_or(vm.ctx.types.object_type);
|
||||
compatible_for_assignment(old_base, &new_base, "__bases__", vm)?;
|
||||
compatible_for_assignment(zelf, &new_base, "__bases__", false, vm)?;
|
||||
|
||||
// References released inside the critical section are collected here
|
||||
// and dropped after the lock: dropping them inside can run arbitrary
|
||||
@@ -3382,18 +3409,18 @@ fn subtype_set_dict(obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine)
|
||||
}
|
||||
|
||||
// subtype_get_weakref
|
||||
fn subtype_get_weakref(obj: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
|
||||
fn subtype_get_weakref(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
||||
if !obj
|
||||
.class()
|
||||
.slots
|
||||
.flags
|
||||
.has_feature(PyTypeFlags::HAS_WEAKREF)
|
||||
{
|
||||
return Err(vm.new_attribute_error("This object has no __weakref__"));
|
||||
}
|
||||
// Return the first weakref in the weakref list, or None
|
||||
let weakref = obj.get_weakrefs();
|
||||
weakref.unwrap_or_else(|| vm.ctx.none())
|
||||
}
|
||||
|
||||
// subtype_set_weakref: __weakref__ is read-only
|
||||
fn subtype_set_weakref(obj: PyObjectRef, _value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
||||
Err(vm.new_attribute_error(format!(
|
||||
"attribute '__weakref__' of '{}' objects is not writable",
|
||||
obj.class().name()
|
||||
)))
|
||||
Ok(weakref.unwrap_or_else(|| vm.ctx.none()))
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -3738,20 +3765,21 @@ fn type_has_weakref(typ: &Py<PyType>) -> bool {
|
||||
|
||||
/// Returns true if `child` adds no instance layout of its own beyond its base,
|
||||
/// so the base can stand in for it when comparing object layouts.
|
||||
fn compatible_with_base(child: &Py<PyType>) -> bool {
|
||||
fn compatible_with_base(child: &Py<PyType>, set_class: bool) -> bool {
|
||||
let Some(parent) = child.base.deref() else {
|
||||
return false;
|
||||
};
|
||||
child.slots.basicsize == parent.slots.basicsize
|
||||
&& child.slots.itemsize == parent.slots.itemsize
|
||||
&& child.slots.member_count == parent.slots.member_count
|
||||
&& type_has_dict(child) == type_has_dict(parent)
|
||||
&& type_has_weakref(child) == type_has_weakref(parent)
|
||||
&& (!set_class
|
||||
|| (type_has_dict(child) == type_has_dict(parent)
|
||||
&& type_has_weakref(child) == type_has_weakref(parent)))
|
||||
}
|
||||
|
||||
/// Walk up to the most derived base that actually fixes the instance layout.
|
||||
fn layout_solid_base(mut typ: &Py<PyType>) -> &Py<PyType> {
|
||||
while compatible_with_base(typ) {
|
||||
fn layout_solid_base(mut typ: &Py<PyType>, set_class: bool) -> &Py<PyType> {
|
||||
while compatible_with_base(typ, set_class) {
|
||||
typ = typ.base.deref().unwrap();
|
||||
}
|
||||
typ
|
||||
@@ -3759,12 +3787,12 @@ fn layout_solid_base(mut typ: &Py<PyType>) -> &Py<PyType> {
|
||||
|
||||
/// Returns true if `a` and `b`, which share the same base, added the same
|
||||
/// instance layout (`__dict__`, `__weakref__`, and `__slots__`).
|
||||
fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>) -> bool {
|
||||
fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>, set_class: bool) -> bool {
|
||||
if a.slots.basicsize != b.slots.basicsize
|
||||
|| a.slots.itemsize != b.slots.itemsize
|
||||
|| a.slots.member_count != b.slots.member_count
|
||||
|| type_has_dict(a) != type_has_dict(b)
|
||||
|| type_has_weakref(a) != type_has_weakref(b)
|
||||
|| (set_class
|
||||
&& (type_has_dict(a) != type_has_dict(b) || type_has_weakref(a) != type_has_weakref(b)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -3783,25 +3811,44 @@ fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that instances of `old_to` and `new_to` share an interchangeable
|
||||
/// object layout, the check `__class__` and `__bases__` assignment perform.
|
||||
/// Validates the layout for `__class__` or `__bases__` assignment. For a base
|
||||
/// change, `original` is the existing class whose allocation must remain valid.
|
||||
///
|
||||
/// `attr` names the attribute being assigned for the error message; the
|
||||
/// message reports `new_to` first and `old_to` second.
|
||||
/// message reports the new type first and the replaced type or base second.
|
||||
pub(crate) fn compatible_for_assignment(
|
||||
old_to: &Py<PyType>,
|
||||
original: &Py<PyType>,
|
||||
new_to: &Py<PyType>,
|
||||
attr: &str,
|
||||
set_class: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let newbase = layout_solid_base(new_to);
|
||||
let oldbase = layout_solid_base(old_to);
|
||||
let old_base = if set_class {
|
||||
None
|
||||
} else {
|
||||
original.base.load_owned()
|
||||
};
|
||||
let old_to = old_base.as_deref().unwrap_or(original);
|
||||
// Reparenting leaves the class's prefix allocation unchanged. A new base
|
||||
// may use its existing dict/weakref storage, but cannot require storage
|
||||
// the class never allocated. Payload size and member slots are checked
|
||||
// separately against the old base, not against the class's own additions.
|
||||
let storage_compatible = set_class
|
||||
|| ((!type_has_dict(new_to) || type_has_dict(original))
|
||||
&& (!type_has_weakref(new_to) || type_has_weakref(original)));
|
||||
let newbase = layout_solid_base(new_to, set_class);
|
||||
let oldbase = layout_solid_base(old_to, set_class);
|
||||
let bases_equal = match (newbase.base.deref(), oldbase.base.deref()) {
|
||||
(Some(x), Some(y)) => x.is(y),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
let compatible = newbase.is(oldbase) || (bases_equal && same_slots_added(newbase, oldbase));
|
||||
let compatible = storage_compatible
|
||||
&& (newbase.is(oldbase)
|
||||
|| (bases_equal
|
||||
&& (set_class
|
||||
|| (newbase.heaptype_ext.is_some() && oldbase.heaptype_ext.is_some()))
|
||||
&& same_slots_added(newbase, oldbase, set_class)));
|
||||
if compatible {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ where
|
||||
global_trace_func: PyMutex::default(),
|
||||
global_profile_func: PyMutex::default(),
|
||||
type_mutex: PyMutex::default(),
|
||||
common_type_descriptors: crate::builtins::type_::CommonTypeDescriptors::new(&ctx),
|
||||
#[cfg(feature = "threading")]
|
||||
main_thread_ident,
|
||||
#[cfg(feature = "threading")]
|
||||
|
||||
@@ -886,6 +886,8 @@ pub struct PyGlobalState {
|
||||
pub global_profile_func: PyMutex<Option<PyObjectRef>>,
|
||||
/// Global type mutation/versioning mutex for CPython-style FT type operations.
|
||||
pub type_mutex: PyMutex<()>,
|
||||
/// Object-owned descriptors shared by heap types in this interpreter.
|
||||
pub(crate) common_type_descriptors: builtins::type_::CommonTypeDescriptors,
|
||||
/// Main thread identifier (pthread_self on Unix)
|
||||
#[cfg(feature = "threading")]
|
||||
pub main_thread_ident: AtomicCell<u64>,
|
||||
|
||||
Reference in new issue
Block a user