mirror of https://github.com/python/cpython
gh-117482: Make the Slot Wrapper Inheritance Tests Much More Thorough (gh-122867)
There were a still a number of gaps in the tests, including not looking at all the builtin types and not checking wrappers in subinterpreters that weren't in the main interpreter. This fixes all that. I considered incorporating the names of the PyTypeObject fields (a la gh-122866), but figured doing so doesn't add much value.
This commit is contained in:
parent
ab094d1b2b
commit
503af8fe9a
|
@ -183,6 +183,9 @@ PyAPI_FUNC(int) _PyStaticType_InitForExtension(
|
||||||
PyInterpreterState *interp,
|
PyInterpreterState *interp,
|
||||||
PyTypeObject *self);
|
PyTypeObject *self);
|
||||||
|
|
||||||
|
// Export for _testinternalcapi extension.
|
||||||
|
PyAPI_FUNC(PyObject *) _PyStaticType_GetBuiltins(void);
|
||||||
|
|
||||||
|
|
||||||
/* Like PyType_GetModuleState, but skips verification
|
/* Like PyType_GetModuleState, but skips verification
|
||||||
* that type is a heap type with an associated module */
|
* that type is a heap type with an associated module */
|
||||||
|
@ -209,6 +212,9 @@ extern PyObject* _PyType_GetSubclasses(PyTypeObject *);
|
||||||
extern int _PyType_HasSubclasses(PyTypeObject *);
|
extern int _PyType_HasSubclasses(PyTypeObject *);
|
||||||
PyAPI_FUNC(PyObject *) _PyType_GetModuleByDef2(PyTypeObject *, PyTypeObject *, PyModuleDef *);
|
PyAPI_FUNC(PyObject *) _PyType_GetModuleByDef2(PyTypeObject *, PyTypeObject *, PyModuleDef *);
|
||||||
|
|
||||||
|
// Export for _testinternalcapi extension.
|
||||||
|
PyAPI_FUNC(PyObject *) _PyType_GetSlotWrapperNames(void);
|
||||||
|
|
||||||
// PyType_Ready() must be called if _PyType_IsReady() is false.
|
// PyType_Ready() must be called if _PyType_IsReady() is false.
|
||||||
// See also the Py_TPFLAGS_READY flag.
|
// See also the Py_TPFLAGS_READY flag.
|
||||||
static inline int
|
static inline int
|
||||||
|
|
|
@ -5,6 +5,7 @@ if __name__ != 'test.support':
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
|
import inspect
|
||||||
import _opcode
|
import _opcode
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -892,8 +893,16 @@ def calcvobjsize(fmt):
|
||||||
return struct.calcsize(_vheader + fmt + _align)
|
return struct.calcsize(_vheader + fmt + _align)
|
||||||
|
|
||||||
|
|
||||||
_TPFLAGS_HAVE_GC = 1<<14
|
_TPFLAGS_STATIC_BUILTIN = 1<<1
|
||||||
|
_TPFLAGS_DISALLOW_INSTANTIATION = 1<<7
|
||||||
|
_TPFLAGS_IMMUTABLETYPE = 1<<8
|
||||||
_TPFLAGS_HEAPTYPE = 1<<9
|
_TPFLAGS_HEAPTYPE = 1<<9
|
||||||
|
_TPFLAGS_BASETYPE = 1<<10
|
||||||
|
_TPFLAGS_READY = 1<<12
|
||||||
|
_TPFLAGS_READYING = 1<<13
|
||||||
|
_TPFLAGS_HAVE_GC = 1<<14
|
||||||
|
_TPFLAGS_BASE_EXC_SUBCLASS = 1<<30
|
||||||
|
_TPFLAGS_TYPE_SUBCLASS = 1<<31
|
||||||
|
|
||||||
def check_sizeof(test, o, size):
|
def check_sizeof(test, o, size):
|
||||||
try:
|
try:
|
||||||
|
@ -2608,19 +2617,121 @@ def copy_python_src_ignore(path, names):
|
||||||
return ignored
|
return ignored
|
||||||
|
|
||||||
|
|
||||||
|
# XXX Move this to the inspect module?
|
||||||
|
def walk_class_hierarchy(top, *, topdown=True):
|
||||||
|
# This is based on the logic in os.walk().
|
||||||
|
assert isinstance(top, type), repr(top)
|
||||||
|
stack = [top]
|
||||||
|
while stack:
|
||||||
|
top = stack.pop()
|
||||||
|
if isinstance(top, tuple):
|
||||||
|
yield top
|
||||||
|
continue
|
||||||
|
|
||||||
|
subs = type(top).__subclasses__(top)
|
||||||
|
if topdown:
|
||||||
|
# Yield before subclass traversal if going top down.
|
||||||
|
yield top, subs
|
||||||
|
# Traverse into subclasses.
|
||||||
|
for sub in reversed(subs):
|
||||||
|
stack.append(sub)
|
||||||
|
else:
|
||||||
|
# Yield after subclass traversal if going bottom up.
|
||||||
|
stack.append((top, subs))
|
||||||
|
# Traverse into subclasses.
|
||||||
|
for sub in reversed(subs):
|
||||||
|
stack.append(sub)
|
||||||
|
|
||||||
|
|
||||||
def iter_builtin_types():
|
def iter_builtin_types():
|
||||||
for obj in __builtins__.values():
|
# First try the explicit route.
|
||||||
if not isinstance(obj, type):
|
try:
|
||||||
|
import _testinternalcapi
|
||||||
|
except ImportError:
|
||||||
|
_testinternalcapi = None
|
||||||
|
if _testinternalcapi is not None:
|
||||||
|
yield from _testinternalcapi.get_static_builtin_types()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fall back to making a best-effort guess.
|
||||||
|
if hasattr(object, '__flags__'):
|
||||||
|
# Look for any type object with the Py_TPFLAGS_STATIC_BUILTIN flag set.
|
||||||
|
import datetime
|
||||||
|
seen = set()
|
||||||
|
for cls, subs in walk_class_hierarchy(object):
|
||||||
|
if cls in seen:
|
||||||
|
continue
|
||||||
|
seen.add(cls)
|
||||||
|
if not (cls.__flags__ & _TPFLAGS_STATIC_BUILTIN):
|
||||||
|
# Do not walk its subclasses.
|
||||||
|
subs[:] = []
|
||||||
|
continue
|
||||||
|
yield cls
|
||||||
|
else:
|
||||||
|
# Fall back to a naive approach.
|
||||||
|
seen = set()
|
||||||
|
for obj in __builtins__.values():
|
||||||
|
if not isinstance(obj, type):
|
||||||
|
continue
|
||||||
|
cls = obj
|
||||||
|
# XXX?
|
||||||
|
if cls.__module__ != 'builtins':
|
||||||
|
continue
|
||||||
|
if cls == ExceptionGroup:
|
||||||
|
# It's a heap type.
|
||||||
|
continue
|
||||||
|
if cls in seen:
|
||||||
|
continue
|
||||||
|
seen.add(cls)
|
||||||
|
yield cls
|
||||||
|
|
||||||
|
|
||||||
|
# XXX Move this to the inspect module?
|
||||||
|
def iter_name_in_mro(cls, name):
|
||||||
|
"""Yield matching items found in base.__dict__ across the MRO.
|
||||||
|
|
||||||
|
The descriptor protocol is not invoked.
|
||||||
|
|
||||||
|
list(iter_name_in_mro(cls, name))[0] is roughly equivalent to
|
||||||
|
find_name_in_mro() in Objects/typeobject.c (AKA PyType_Lookup()).
|
||||||
|
|
||||||
|
inspect.getattr_static() is similar.
|
||||||
|
"""
|
||||||
|
# This can fail if "cls" is weird.
|
||||||
|
for base in inspect._static_getmro(cls):
|
||||||
|
# This can fail if "base" is weird.
|
||||||
|
ns = inspect._get_dunder_dict_of_class(base)
|
||||||
|
try:
|
||||||
|
obj = ns[name]
|
||||||
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
cls = obj
|
yield obj, base
|
||||||
if cls.__module__ != 'builtins':
|
|
||||||
continue
|
|
||||||
yield cls
|
# XXX Move this to the inspect module?
|
||||||
|
def find_name_in_mro(cls, name, default=inspect._sentinel):
|
||||||
|
for res in iter_name_in_mro(cls, name):
|
||||||
|
# Return the first one.
|
||||||
|
return res
|
||||||
|
if default is not inspect._sentinel:
|
||||||
|
return default, None
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
|
||||||
|
# XXX The return value should always be exactly the same...
|
||||||
|
def identify_type_slot_wrappers():
|
||||||
|
try:
|
||||||
|
import _testinternalcapi
|
||||||
|
except ImportError:
|
||||||
|
_testinternalcapi = None
|
||||||
|
if _testinternalcapi is not None:
|
||||||
|
names = {n: None for n in _testinternalcapi.identify_type_slot_wrappers()}
|
||||||
|
return list(names)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def iter_slot_wrappers(cls):
|
def iter_slot_wrappers(cls):
|
||||||
assert cls.__module__ == 'builtins', cls
|
|
||||||
|
|
||||||
def is_slot_wrapper(name, value):
|
def is_slot_wrapper(name, value):
|
||||||
if not isinstance(value, types.WrapperDescriptorType):
|
if not isinstance(value, types.WrapperDescriptorType):
|
||||||
assert not repr(value).startswith('<slot wrapper '), (cls, name, value)
|
assert not repr(value).startswith('<slot wrapper '), (cls, name, value)
|
||||||
|
@ -2630,6 +2741,19 @@ def iter_slot_wrappers(cls):
|
||||||
assert name.startswith('__') and name.endswith('__'), (cls, name, value)
|
assert name.startswith('__') and name.endswith('__'), (cls, name, value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
attrs = identify_type_slot_wrappers()
|
||||||
|
except NotImplementedError:
|
||||||
|
attrs = None
|
||||||
|
if attrs is not None:
|
||||||
|
for attr in sorted(attrs):
|
||||||
|
obj, base = find_name_in_mro(cls, attr, None)
|
||||||
|
if obj is not None and is_slot_wrapper(attr, obj):
|
||||||
|
yield attr, base is cls
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fall back to a naive best-effort approach.
|
||||||
|
|
||||||
ns = vars(cls)
|
ns = vars(cls)
|
||||||
unused = set(ns)
|
unused = set(ns)
|
||||||
for name in dir(cls):
|
for name in dir(cls):
|
||||||
|
|
|
@ -420,45 +420,54 @@ class EmbeddingTests(EmbeddingTestsMixin, unittest.TestCase):
|
||||||
def test_static_types_inherited_slots(self):
|
def test_static_types_inherited_slots(self):
|
||||||
script = textwrap.dedent("""
|
script = textwrap.dedent("""
|
||||||
import test.support
|
import test.support
|
||||||
|
results = []
|
||||||
results = {}
|
|
||||||
def add(cls, slot, own):
|
|
||||||
value = getattr(cls, slot)
|
|
||||||
try:
|
|
||||||
subresults = results[cls.__name__]
|
|
||||||
except KeyError:
|
|
||||||
subresults = results[cls.__name__] = {}
|
|
||||||
subresults[slot] = [repr(value), own]
|
|
||||||
|
|
||||||
for cls in test.support.iter_builtin_types():
|
for cls in test.support.iter_builtin_types():
|
||||||
for slot, own in test.support.iter_slot_wrappers(cls):
|
for attr, _ in test.support.iter_slot_wrappers(cls):
|
||||||
add(cls, slot, own)
|
wrapper = getattr(cls, attr)
|
||||||
|
res = (cls, attr, wrapper)
|
||||||
|
results.append(res)
|
||||||
|
results = ((repr(c), a, repr(w)) for c, a, w in results)
|
||||||
""")
|
""")
|
||||||
|
def collate_results(raw):
|
||||||
|
results = {}
|
||||||
|
for cls, attr, wrapper in raw:
|
||||||
|
key = cls, attr
|
||||||
|
assert key not in results, (results, key, wrapper)
|
||||||
|
results[key] = wrapper
|
||||||
|
return results
|
||||||
|
|
||||||
ns = {}
|
ns = {}
|
||||||
exec(script, ns, ns)
|
exec(script, ns, ns)
|
||||||
all_expected = ns['results']
|
main_results = collate_results(ns['results'])
|
||||||
del ns
|
del ns
|
||||||
|
|
||||||
script += textwrap.dedent("""
|
script += textwrap.dedent("""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
text = json.dumps(results)
|
text = json.dumps(list(results))
|
||||||
print(text, file=sys.stderr)
|
print(text, file=sys.stderr)
|
||||||
""")
|
""")
|
||||||
out, err = self.run_embedded_interpreter(
|
out, err = self.run_embedded_interpreter(
|
||||||
"test_repeated_init_exec", script, script)
|
"test_repeated_init_exec", script, script)
|
||||||
results = err.split('--- Loop #')[1:]
|
_results = err.split('--- Loop #')[1:]
|
||||||
results = [res.rpartition(' ---\n')[-1] for res in results]
|
(_embedded, _reinit,
|
||||||
|
) = [json.loads(res.rpartition(' ---\n')[-1]) for res in _results]
|
||||||
|
embedded_results = collate_results(_embedded)
|
||||||
|
reinit_results = collate_results(_reinit)
|
||||||
|
|
||||||
|
for key, expected in main_results.items():
|
||||||
|
cls, attr = key
|
||||||
|
for src, results in [
|
||||||
|
('embedded', embedded_results),
|
||||||
|
('reinit', reinit_results),
|
||||||
|
]:
|
||||||
|
with self.subTest(src, cls=cls, slotattr=attr):
|
||||||
|
actual = results.pop(key)
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
for i, text in enumerate(results, start=1):
|
self.assertEqual(embedded_results, {})
|
||||||
result = json.loads(text)
|
self.assertEqual(reinit_results, {})
|
||||||
for classname, expected in all_expected.items():
|
|
||||||
with self.subTest(loop=i, cls=classname):
|
|
||||||
slots = result.pop(classname)
|
|
||||||
self.assertEqual(slots, expected)
|
|
||||||
self.assertEqual(result, {})
|
|
||||||
self.assertEqual(out, '')
|
self.assertEqual(out, '')
|
||||||
|
|
||||||
def test_getargs_reset_static_parser(self):
|
def test_getargs_reset_static_parser(self):
|
||||||
|
|
|
@ -2396,35 +2396,53 @@ class SubinterpreterTests(unittest.TestCase):
|
||||||
def test_static_types_inherited_slots(self):
|
def test_static_types_inherited_slots(self):
|
||||||
rch, sch = interpreters.channels.create()
|
rch, sch = interpreters.channels.create()
|
||||||
|
|
||||||
slots = []
|
script = textwrap.dedent("""
|
||||||
script = ''
|
import test.support
|
||||||
for cls in iter_builtin_types():
|
results = []
|
||||||
for slot, own in iter_slot_wrappers(cls):
|
for cls in test.support.iter_builtin_types():
|
||||||
if cls is bool and slot in self.NUMERIC_METHODS:
|
for attr, _ in test.support.iter_slot_wrappers(cls):
|
||||||
|
wrapper = getattr(cls, attr)
|
||||||
|
res = (cls, attr, wrapper)
|
||||||
|
results.append(res)
|
||||||
|
results = tuple((repr(c), a, repr(w)) for c, a, w in results)
|
||||||
|
sch.send_nowait(results)
|
||||||
|
""")
|
||||||
|
def collate_results(raw):
|
||||||
|
results = {}
|
||||||
|
for cls, attr, wrapper in raw:
|
||||||
|
# XXX This should not be necessary.
|
||||||
|
if cls == repr(bool) and attr in self.NUMERIC_METHODS:
|
||||||
continue
|
continue
|
||||||
slots.append((cls, slot, own))
|
key = cls, attr
|
||||||
script += textwrap.dedent(f"""
|
assert key not in results, (results, key, wrapper)
|
||||||
text = repr({cls.__name__}.{slot})
|
results[key] = wrapper
|
||||||
sch.send_nowait(({cls.__name__!r}, {slot!r}, text))
|
return results
|
||||||
""")
|
|
||||||
|
|
||||||
exec(script)
|
exec(script)
|
||||||
all_expected = []
|
raw = rch.recv_nowait()
|
||||||
for cls, slot, _ in slots:
|
main_results = collate_results(raw)
|
||||||
result = rch.recv()
|
|
||||||
assert result == (cls.__name__, slot, result[-1]), (cls, slot, result)
|
|
||||||
all_expected.append(result)
|
|
||||||
|
|
||||||
interp = interpreters.create()
|
interp = interpreters.create()
|
||||||
interp.exec('from test.support import interpreters')
|
interp.exec('from test.support import interpreters')
|
||||||
interp.prepare_main(sch=sch)
|
interp.prepare_main(sch=sch)
|
||||||
interp.exec(script)
|
interp.exec(script)
|
||||||
|
raw = rch.recv_nowait()
|
||||||
|
interp_results = collate_results(raw)
|
||||||
|
|
||||||
for i, (cls, slot, _) in enumerate(slots):
|
for key, expected in main_results.items():
|
||||||
with self.subTest(cls=cls, slot=slot):
|
cls, attr = key
|
||||||
expected = all_expected[i]
|
with self.subTest(cls=cls, slotattr=attr):
|
||||||
result = rch.recv()
|
actual = interp_results.pop(key)
|
||||||
self.assertEqual(result, expected)
|
# XXX This should not be necessary.
|
||||||
|
if cls == "<class 'collections.OrderedDict'>" and attr == '__len__':
|
||||||
|
continue
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
# XXX This should not be necessary.
|
||||||
|
interp_results = {k: v for k, v in interp_results.items() if k[1] != '__hash__'}
|
||||||
|
# XXX This should not be necessary.
|
||||||
|
interp_results.pop(("<class 'collections.OrderedDict'>", '__getitem__'), None)
|
||||||
|
self.maxDiff = None
|
||||||
|
self.assertEqual(interp_results, {})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -2035,6 +2035,20 @@ gh_119213_getargs_impl(PyObject *module, PyObject *spam)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
get_static_builtin_types(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
return _PyStaticType_GetBuiltins();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
identify_type_slot_wrappers(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
return _PyType_GetSlotWrapperNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static PyMethodDef module_functions[] = {
|
static PyMethodDef module_functions[] = {
|
||||||
{"get_configs", get_configs, METH_NOARGS},
|
{"get_configs", get_configs, METH_NOARGS},
|
||||||
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
|
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
|
||||||
|
@ -2129,6 +2143,8 @@ static PyMethodDef module_functions[] = {
|
||||||
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
|
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
|
||||||
#endif
|
#endif
|
||||||
GH_119213_GETARGS_METHODDEF
|
GH_119213_GETARGS_METHODDEF
|
||||||
|
{"get_static_builtin_types", get_static_builtin_types, METH_NOARGS},
|
||||||
|
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
|
||||||
{NULL, NULL} /* sentinel */
|
{NULL, NULL} /* sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -324,6 +324,29 @@ managed_static_type_get_def(PyTypeObject *self, int isbuiltin)
|
||||||
return &_PyRuntime.types.managed_static.types[full_index].def;
|
return &_PyRuntime.types.managed_static.types[full_index].def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_PyStaticType_GetBuiltins(void)
|
||||||
|
{
|
||||||
|
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||||||
|
Py_ssize_t count = (Py_ssize_t)interp->types.builtins.num_initialized;
|
||||||
|
assert(count <= _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES);
|
||||||
|
|
||||||
|
PyObject *results = PyList_New(count);
|
||||||
|
if (results == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (Py_ssize_t i = 0; i < count; i++) {
|
||||||
|
PyTypeObject *cls = interp->types.builtins.initialized[i].type;
|
||||||
|
assert(cls != NULL);
|
||||||
|
assert(interp->types.builtins.initialized[i].isbuiltin);
|
||||||
|
PyList_SET_ITEM(results, i, Py_NewRef((PyObject *)cls));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Also see _PyStaticType_InitBuiltin() and _PyStaticType_FiniBuiltin().
|
// Also see _PyStaticType_InitBuiltin() and _PyStaticType_FiniBuiltin().
|
||||||
|
|
||||||
/* end static builtin helpers */
|
/* end static builtin helpers */
|
||||||
|
@ -10927,6 +10950,24 @@ update_all_slots(PyTypeObject* type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_PyType_GetSlotWrapperNames(void)
|
||||||
|
{
|
||||||
|
size_t len = Py_ARRAY_LENGTH(slotdefs) - 1;
|
||||||
|
PyObject *names = PyList_New(len);
|
||||||
|
if (names == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
assert(slotdefs[len].name == NULL);
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
pytype_slotdef *slotdef = &slotdefs[i];
|
||||||
|
assert(slotdef->name != NULL);
|
||||||
|
PyList_SET_ITEM(names, i, Py_NewRef(slotdef->name_strobj));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Call __set_name__ on all attributes (including descriptors)
|
/* Call __set_name__ on all attributes (including descriptors)
|
||||||
in a newly generated type */
|
in a newly generated type */
|
||||||
static int
|
static int
|
||||||
|
|
Loading…
Reference in New Issue