summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_descr.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/test/test_descr.py')
-rw-r--r--Lib/test/test_descr.py227
1 files changed, 226 insertions, 1 deletions
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 26c273a..1de2a99 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -4996,11 +4996,236 @@ class SharedKeyTests(unittest.TestCase):
self.assertLess(sys.getsizeof(vars(b)), sys.getsizeof({}))
+class DebugHelperMeta(type):
+ """
+ Sets default __doc__ and simplifies repr() output.
+ """
+ def __new__(mcls, name, bases, attrs):
+ if attrs.get('__doc__') is None:
+ attrs['__doc__'] = name # helps when debugging with gdb
+ return type.__new__(mcls, name, bases, attrs)
+ def __repr__(cls):
+ return repr(cls.__name__)
+
+
+class MroTest(unittest.TestCase):
+ """
+ Regressions for some bugs revealed through
+ mcsl.mro() customization (typeobject.c: mro_internal()) and
+ cls.__bases__ assignment (typeobject.c: type_set_bases()).
+ """
+
+ def setUp(self):
+ self.step = 0
+ self.ready = False
+
+ def step_until(self, limit):
+ ret = (self.step < limit)
+ if ret:
+ self.step += 1
+ return ret
+
+ def test_incomplete_set_bases_on_self(self):
+ """
+ type_set_bases must be aware that type->tp_mro can be NULL.
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if self.step_until(1):
+ assert cls.__mro__ is None
+ cls.__bases__ += ()
+
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+
+ def test_reent_set_bases_on_base(self):
+ """
+ Deep reentrancy must not over-decref old_mro.
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if cls.__mro__ is not None and cls.__name__ == 'B':
+ # 4-5 steps are usually enough to make it crash somewhere
+ if self.step_until(10):
+ A.__bases__ += ()
+
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+ class B(A):
+ pass
+ B.__bases__ += ()
+
+ def test_reent_set_bases_on_direct_base(self):
+ """
+ Similar to test_reent_set_bases_on_base, but may crash differently.
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ base = cls.__bases__[0]
+ if base is not object:
+ if self.step_until(5):
+ base.__bases__ += ()
+
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+ class B(A):
+ pass
+ class C(B):
+ pass
+
+ def test_reent_set_bases_tp_base_cycle(self):
+ """
+ type_set_bases must check for an inheritance cycle not only through
+ MRO of the type, which may be not yet updated in case of reentrance,
+ but also through tp_base chain, which is assigned before diving into
+ inner calls to mro().
+
+ Otherwise, the following snippet can loop forever:
+ do {
+ // ...
+ type = type->tp_base;
+ } while (type != NULL);
+
+ Functions that rely on tp_base (like solid_base and PyType_IsSubtype)
+ would not be happy in that case, causing a stack overflow.
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if self.ready:
+ if cls.__name__ == 'B1':
+ B2.__bases__ = (B1,)
+ if cls.__name__ == 'B2':
+ B1.__bases__ = (B2,)
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+ class B1(A):
+ pass
+ class B2(A):
+ pass
+
+ self.ready = True
+ with self.assertRaises(TypeError):
+ B1.__bases__ += ()
+
+ def test_tp_subclasses_cycle_in_update_slots(self):
+ """
+ type_set_bases must check for reentrancy upon finishing its job
+ by updating tp_subclasses of old/new bases of the type.
+ Otherwise, an implicit inheritance cycle through tp_subclasses
+ can break functions that recurse on elements of that field
+ (like recurse_down_subclasses and mro_hierarchy) eventually
+ leading to a stack overflow.
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if self.ready and cls.__name__ == 'C':
+ self.ready = False
+ C.__bases__ = (B2,)
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+ class B1(A):
+ pass
+ class B2(A):
+ pass
+ class C(A):
+ pass
+
+ self.ready = True
+ C.__bases__ = (B1,)
+ B1.__bases__ = (C,)
+
+ self.assertEqual(C.__bases__, (B2,))
+ self.assertEqual(B2.__subclasses__(), [C])
+ self.assertEqual(B1.__subclasses__(), [])
+
+ self.assertEqual(B1.__bases__, (C,))
+ self.assertEqual(C.__subclasses__(), [B1])
+
+ def test_tp_subclasses_cycle_error_return_path(self):
+ """
+ The same as test_tp_subclasses_cycle_in_update_slots, but tests
+ a code path executed on error (goto bail).
+ """
+ class E(Exception):
+ pass
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if self.ready and cls.__name__ == 'C':
+ if C.__bases__ == (B2,):
+ self.ready = False
+ else:
+ C.__bases__ = (B2,)
+ raise E
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+ class B1(A):
+ pass
+ class B2(A):
+ pass
+ class C(A):
+ pass
+
+ self.ready = True
+ with self.assertRaises(E):
+ C.__bases__ = (B1,)
+ B1.__bases__ = (C,)
+
+ self.assertEqual(C.__bases__, (B2,))
+ self.assertEqual(C.__mro__, tuple(type.mro(C)))
+
+ def test_incomplete_extend(self):
+ """
+ Extending an unitialized type with type->tp_mro == NULL must
+ throw a reasonable TypeError exception, instead of failing
+ with PyErr_BadInternalCall.
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if cls.__mro__ is None and cls.__name__ != 'X':
+ with self.assertRaises(TypeError):
+ class X(cls):
+ pass
+
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+
+ def test_incomplete_super(self):
+ """
+ Attrubute lookup on a super object must be aware that
+ its target type can be uninitialized (type->tp_mro == NULL).
+ """
+ class M(DebugHelperMeta):
+ def mro(cls):
+ if cls.__mro__ is None:
+ with self.assertRaises(AttributeError):
+ super(cls, cls).xxx
+
+ return type.mro(cls)
+
+ class A(metaclass=M):
+ pass
+
+
def test_main():
# Run all local test cases, with PTypesLongInitTest first.
support.run_unittest(PTypesLongInitTest, OperatorsTest,
ClassPropertiesAndMethods, DictProxyTests,
- MiscTests, PicklingTests, SharedKeyTests)
+ MiscTests, PicklingTests, SharedKeyTests,
+ MroTest)
if __name__ == "__main__":
test_main()