mirror of
https://github.com/python/cpython
synced 2026-11-14 16:47:16 +03:00
gh-149044: Improve Py_tp_base[s] docs & error message for non-type bases (GH-151252)
The initial implementation of PEP 820 worsened the error message
when non-types are given as base types in Py_tp_bases & Py_tp_base.
Bring back the 'bases must be types' wording and add a 'got' note for
easier debugging.
Improve slot ID documentation, and soft-deprecate Py_tp_base
(as per the PEP).
(cherry picked from commit 16185e9fe2)
Co-authored-by: Petr Viktorin <encukou@gmail.com>
349 lines
14 KiB
Python
349 lines
14 KiB
Python
from test.support import import_helper, subTests
|
|
import contextlib
|
|
import unittest
|
|
import types
|
|
import sys
|
|
|
|
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
|
|
_testcapi = import_helper.import_module('_testcapi')
|
|
|
|
class FakeSpec:
|
|
name = 'module'
|
|
|
|
|
|
# See Modules/_testlimitedcapi/slots.c for the definitions.
|
|
# This module is full of "magic constants" which simply need to match
|
|
# between the C and Python part of the tests.
|
|
|
|
class TypeSlotsTests(unittest.TestCase):
|
|
def test_basic_type_slots(self):
|
|
cls = _testlimitedcapi.type_from_slots("basic")
|
|
self.assertEqual(cls.__name__, "MyType")
|
|
|
|
# Py_TPFLAGS_IMMUTABLETYPE is *not* set
|
|
cls.attr = 123
|
|
|
|
# Py_TPFLAGS_BASETYPE is *not* set
|
|
with self.assertRaises(TypeError):
|
|
class Sub(cls):
|
|
pass
|
|
|
|
def test_mod_slot_in_type(self):
|
|
with self.assertRaisesRegex(SystemError, "invalid.* 100 .*Py_mod_name"):
|
|
_testlimitedcapi.type_from_slots("foreign_slot")
|
|
|
|
def test_size_slots(self):
|
|
cls = _testlimitedcapi.type_from_slots("basicsize")
|
|
self.assertGreaterEqual(cls.__basicsize__, 256)
|
|
|
|
cls = _testlimitedcapi.type_from_slots("extra_basicsize")
|
|
self.assertGreaterEqual(cls.__basicsize__, object.__basicsize__ + 256)
|
|
|
|
cls = _testlimitedcapi.type_from_slots("itemsize")
|
|
self.assertGreaterEqual(cls.__itemsize__, 16)
|
|
|
|
def test_flag_slots(self):
|
|
cls = _testlimitedcapi.type_from_slots("flags")
|
|
with self.assertRaises(TypeError):
|
|
# Py_TPFLAGS_IMMUTABLETYPE is set
|
|
cls.attr = 123
|
|
class Sub(cls):
|
|
# Py_TPFLAGS_BASETYPE is set
|
|
pass
|
|
|
|
def test_func_slots(self):
|
|
cls = _testlimitedcapi.type_from_slots("matmul_123")
|
|
self.assertEqual(cls() @ None, 123)
|
|
|
|
def test_optional_end(self):
|
|
with self.assertRaisesRegex(SystemError, "invalid flags.*Py_slot_end"):
|
|
cls = _testlimitedcapi.type_from_slots("optional_end")
|
|
|
|
def test_invalid(self):
|
|
with self.assertRaisesRegex(SystemError, "Py_slot_invalid"):
|
|
cls = _testlimitedcapi.type_from_slots("invalid")
|
|
with self.assertRaisesRegex(SystemError, f"slot ID {0xfbad}"):
|
|
cls = _testlimitedcapi.type_from_slots("invalid_fbad")
|
|
|
|
cls = _testlimitedcapi.type_from_slots("optional_invalid")
|
|
self.assertGreaterEqual(cls.__basicsize__, object.__basicsize__ + 256)
|
|
|
|
cls = _testlimitedcapi.type_from_slots("optional_invalid_fbad")
|
|
self.assertGreaterEqual(cls.__basicsize__, object.__basicsize__ + 256)
|
|
|
|
@subTests("case_name", ["old_slot_numbers", "new_slot_numbers"])
|
|
def test_compat_slot_numbers(self, case_name):
|
|
cls = _testlimitedcapi.type_from_slots(case_name)
|
|
obj = cls()
|
|
|
|
# Py_bf_getbuffer (1), Py_bf_releasebuffer (2)
|
|
self.assertEqual(obj.buf_counter, 0)
|
|
mem = memoryview(obj)
|
|
self.assertEqual(bytes(mem), b"buf\0")
|
|
self.assertEqual(obj.buf_counter, 1)
|
|
mem.release()
|
|
self.assertEqual(obj.buf_counter, 0)
|
|
|
|
# Py_mp_ass_subscript (3)
|
|
with self.assertRaises(KeyError):
|
|
obj["key"] = "value"
|
|
|
|
# Py_mp_length (4)
|
|
self.assertEqual(len(obj), 456)
|
|
|
|
def test_nonstatic_tp_members(self):
|
|
with self.assertRaisesRegex(SystemError, "Py_tp_members.*STATIC"):
|
|
_testlimitedcapi.type_from_slots("nonstatic_tp_members")
|
|
|
|
def test_intptr_flags(self):
|
|
cls = _testlimitedcapi.type_from_slots("intptr_flags_macro")
|
|
with self.assertRaises(TypeError):
|
|
# Py_TPFLAGS_IMMUTABLETYPE is set
|
|
cls.attr = 123
|
|
|
|
cls = _testlimitedcapi.type_from_slots("intptr_flags_struct")
|
|
with self.assertRaises(TypeError):
|
|
# Py_TPFLAGS_IMMUTABLETYPE is set
|
|
cls.attr = 123
|
|
|
|
cls = _testlimitedcapi.type_from_slots("intptr_static")
|
|
cls.attribute = 123
|
|
|
|
def test_nested(self):
|
|
cls = _testlimitedcapi.type_from_slots("nested")
|
|
self.assertEqual(cls() + 1, 123)
|
|
self.assertEqual(cls() - 1, 234)
|
|
|
|
cls = _testlimitedcapi.type_from_slots("nested_max")
|
|
self.assertEqual(cls() + 1, 123)
|
|
self.assertEqual(cls() - 1, 234)
|
|
self.assertEqual(cls() * 1, 345)
|
|
self.assertEqual(cls() / 1, 456)
|
|
self.assertEqual(cls() % 1, 567)
|
|
|
|
with self.assertRaisesRegex(SystemError, "too many levels"):
|
|
_testlimitedcapi.type_from_slots("nested_over_limit")
|
|
|
|
cls = _testlimitedcapi.type_from_slots("nested_old")
|
|
self.assertEqual(cls() + 1, 123)
|
|
self.assertEqual(cls() - 1, 234)
|
|
|
|
cls = _testlimitedcapi.type_from_slots("nested_old_max")
|
|
self.assertEqual(cls() + 1, 123)
|
|
self.assertEqual(cls() - 1, 234)
|
|
self.assertEqual(cls() * 1, 345)
|
|
self.assertEqual(cls() / 1, 456)
|
|
self.assertEqual(cls() % 1, 567)
|
|
|
|
with self.assertRaisesRegex(SystemError, "too many levels"):
|
|
_testlimitedcapi.type_from_slots("nested_old_over_limit")
|
|
|
|
cls = _testlimitedcapi.type_from_slots("nested_pingpong")
|
|
self.assertEqual(cls() + 1, 123)
|
|
self.assertEqual(cls() - 1, 234)
|
|
self.assertEqual(cls() * 1, 345)
|
|
self.assertEqual(cls() / 1, 456)
|
|
self.assertEqual(cls() % 1, 567)
|
|
|
|
# Slot names aren't exposed to Python yet; see Include/slots_generated.h
|
|
# for the definitions.
|
|
|
|
@subTests("slot_number", [
|
|
*range(1, 83), # Original slots
|
|
*range(88, 92), # New compat slot values
|
|
*range(95, 99), # Slots for PyType_Spec fields
|
|
*range(107, 109), # Slots for PyType_FromMetaclass args
|
|
])
|
|
def test_null_slot_handling(self, slot_number):
|
|
if slot_number == 56:
|
|
# Py_tp_doc
|
|
return
|
|
elif slot_number == 72 or slot_number >= 95:
|
|
# Py_tp_members; all new slots
|
|
ctx = self.assertRaisesRegex(
|
|
SystemError, "NULL not allowed|must be positive")
|
|
ctx_old = ctx
|
|
else:
|
|
ctx = self.assertWarnsRegex(DeprecationWarning, "NULL")
|
|
ctx_old = contextlib.nullcontext()
|
|
with ctx:
|
|
_testlimitedcapi.type_from_null_slot(slot_number)
|
|
if slot_number < 95:
|
|
with ctx_old:
|
|
_testlimitedcapi.type_from_null_spec_slot(slot_number)
|
|
|
|
def test_repeat_warning(self):
|
|
with self.assertWarnsRegex(DeprecationWarning, "multiple"):
|
|
cls = _testlimitedcapi.type_from_slots("repeat_add")
|
|
self.assertEqual(cls() + 1, 456)
|
|
|
|
def test_repeat_error(self):
|
|
with self.assertRaisesRegex(SystemError, "multiple"):
|
|
cls = _testlimitedcapi.type_from_slots("repeat_module")
|
|
|
|
class ModuleSlotsTests(unittest.TestCase):
|
|
def test_basic_module_slots(self):
|
|
mod = _testlimitedcapi.module_from_slots("basic", FakeSpec())
|
|
self.assertIsInstance(mod, types.ModuleType)
|
|
|
|
def test_type_slot_in_module(self):
|
|
with self.assertRaisesRegex(SystemError, "invalid.* 95 .*Py_tp_name"):
|
|
_testlimitedcapi.module_from_slots("foreign_slot", FakeSpec())
|
|
|
|
def test_size_slots(self):
|
|
mod = _testlimitedcapi.module_from_slots("state_size", FakeSpec())
|
|
self.assertEqual(mod.state_size, 42)
|
|
|
|
def test_flag_slots(self):
|
|
mod = _testlimitedcapi.module_from_slots("multi_interp", FakeSpec())
|
|
|
|
def test_exec_slot(self):
|
|
mod = _testlimitedcapi.module_from_slots("exec", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
|
|
def test_optional_end(self):
|
|
with self.assertRaisesRegex(SystemError, "invalid flags.*Py_slot_end"):
|
|
_testlimitedcapi.module_from_slots("optional_end", FakeSpec())
|
|
|
|
def test_invalid(self):
|
|
with self.assertRaisesRegex(SystemError, "Py_slot_invalid"):
|
|
_testlimitedcapi.module_from_slots("invalid", FakeSpec())
|
|
with self.assertRaisesRegex(SystemError, f"slot ID {0xfbad}"):
|
|
_testlimitedcapi.module_from_slots("invalid_fbad", FakeSpec())
|
|
|
|
mod = _testlimitedcapi.module_from_slots("optional_invalid", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
|
|
mod = _testlimitedcapi.module_from_slots("optional_invalid_fbad", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
|
|
@subTests("case_name", ["old_slot_numbers", "new_slot_numbers"])
|
|
def test_compat_slot_numbers(self, case_name):
|
|
mod = _testlimitedcapi.module_from_slots(case_name, FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
|
|
@subTests("case_name", ["old_slot_number_create", "new_slot_number_create"])
|
|
def test_compat_slot_number_create(self, case_name):
|
|
spec = FakeSpec()
|
|
mod = _testlimitedcapi.module_from_slots(case_name, spec)
|
|
self.assertIs(mod, spec)
|
|
|
|
@subTests("slot_number", [4, 87])
|
|
def test_compat_slot_number_gil(self, slot_number):
|
|
spec = FakeSpec()
|
|
gil_enabled = sys._is_gil_enabled()
|
|
mod = _testlimitedcapi.module_from_gil_slot(slot_number, spec)
|
|
self.assertEqual(gil_enabled, sys._is_gil_enabled())
|
|
|
|
def test_nonstatic_mod_methods(self):
|
|
with self.assertRaisesRegex(SystemError, "Py_mod_methods.*STATIC"):
|
|
_testlimitedcapi.module_from_slots("nonstatic_mod_methods",
|
|
FakeSpec())
|
|
|
|
def test_intptr_methods(self):
|
|
mod = _testlimitedcapi.module_from_slots("intptr_methods",
|
|
FakeSpec())
|
|
self.assertEqual(mod.type_from_slots.__name__, "type_from_slots")
|
|
|
|
def test_nested(self):
|
|
mod = _testlimitedcapi.module_from_slots("nested", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
self.assertEqual(mod.__doc__, "doc")
|
|
|
|
mod = _testlimitedcapi.module_from_slots("nested_max", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
self.assertEqual(mod.state_size, 53)
|
|
self.assertEqual(mod.__doc__, "doc")
|
|
|
|
with self.assertRaisesRegex(SystemError, "too many levels"):
|
|
_testlimitedcapi.module_from_slots("nested_over_limit", FakeSpec())
|
|
|
|
mod = _testlimitedcapi.module_from_slots("nested_old", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
self.assertEqual(mod.__doc__, "doc")
|
|
|
|
mod = _testlimitedcapi.module_from_slots("nested_old_max", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
self.assertEqual(mod.state_size, 53)
|
|
self.assertEqual(mod.__doc__, "doc")
|
|
|
|
with self.assertRaisesRegex(SystemError, "too many levels"):
|
|
_testlimitedcapi.module_from_slots("nested_old_over_limit", FakeSpec())
|
|
|
|
mod = _testlimitedcapi.module_from_slots("nested_pingpong", FakeSpec())
|
|
self.assertEqual(mod.exec_done, "yes")
|
|
self.assertEqual(mod.state_size, 53)
|
|
self.assertEqual(mod.__doc__, "doc")
|
|
|
|
def test_nested_nonstatic_from_def(self):
|
|
with self.assertRaisesRegex(SystemError, "must be static"):
|
|
_testcapi.module_from_def_nonstatic_nested(FakeSpec())
|
|
|
|
# Slot names aren't exposed to Python yet; see Include/slots_generated.h
|
|
# for the definitions.
|
|
|
|
@subTests("slot_number", [
|
|
*range(1, 5), # Old compat slot values
|
|
*range(84, 88), # New compat slot values
|
|
*range(100, 107), # Slots for PyModuleDef fields
|
|
*range(109, 111), # Slots new in 3.15
|
|
])
|
|
def test_null_slot_handling(self, slot_number):
|
|
if slot_number in {3, 86, 4, 87, 102}:
|
|
# Py_mod_mult.interp., Py_mod_gil, Py_mod_state_size
|
|
return
|
|
elif slot_number in {2, 85} or slot_number > 85:
|
|
# Py_mod_exec, new slots
|
|
ctx = self.assertRaisesRegex(SystemError, "NULL not allowed")
|
|
ctx_old = ctx
|
|
else:
|
|
ctx = self.assertWarnsRegex(DeprecationWarning, "NULL")
|
|
ctx_old = contextlib.nullcontext()
|
|
with ctx:
|
|
_testlimitedcapi.module_from_null_slot(slot_number, FakeSpec())
|
|
with ctx_old:
|
|
_testcapi.module_from_null_def_slot(slot_number,
|
|
FakeSpec())
|
|
|
|
def test_repeat_error(self):
|
|
with self.assertRaisesRegex(SystemError, "multiple"):
|
|
_testlimitedcapi.module_from_slots("repeat_create", FakeSpec())
|
|
with self.assertRaisesRegex(SystemError, "multiple"):
|
|
_testlimitedcapi.module_from_slots("repeat_exec", FakeSpec())
|
|
with self.assertRaisesRegex(SystemError, "multiple"):
|
|
_testlimitedcapi.module_from_slots("repeat_gil", FakeSpec())
|
|
|
|
def test_bases_slots(self):
|
|
create = _testlimitedcapi.type_from_base_slots
|
|
|
|
# Py_tp_bases overrides Py_tp_base
|
|
cls = create(base=int, bases=float)
|
|
self.assertEqual(cls.mro(), [cls, float, object])
|
|
|
|
# type is equivalent to one-element tuple
|
|
cls = create(base=None, bases=int)
|
|
self.assertEqual(cls.mro(), [cls, int, object])
|
|
|
|
cls = create(base=None, bases=(int,))
|
|
self.assertEqual(cls.mro(), [cls, int, object])
|
|
|
|
cls = create(base=int)
|
|
self.assertEqual(cls.mro(), [cls, int, object])
|
|
|
|
cls = create(base=(int,))
|
|
self.assertEqual(cls.mro(), [cls, int, object])
|
|
|
|
# Tuple of bases works
|
|
class Custom:
|
|
pass
|
|
cls = create(bases=int)
|
|
sub = create(base=float, bases=(Custom, cls, int))
|
|
self.assertEqual(sub.mro(), [sub, Custom, cls, int, object])
|
|
|
|
# Reasonable error message for non-types
|
|
with self.assertRaisesRegex(TypeError,
|
|
"bases must be types; got 'NoneType'"):
|
|
create(base=None)
|
|
with self.assertRaisesRegex(TypeError,
|
|
"bases must be types; got 'str'"):
|
|
create(bases="a string")
|