[3.14] gh-132775: Support Fallbacks in _PyObject_GetXIData() (gh-134418)

It now supports a "full" fallback to _PyFunction_GetXIData() and then `_PyPickle_GetXIData()`.
There's also room for other fallback modes if that later makes sense.

(cherry picked from commit 88f8102a8f, AKA gh-133482)

Co-authored-by: Eric Snow <ericsnowcurrently@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-05-21 16:47:56 +02:00 committed by GitHub
parent cd3395a8b1
commit 2ffc10bd39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 32041 additions and 31540 deletions

View file

@ -5,6 +5,7 @@ import itertools
import sys
import types
import unittest
import warnings
from test.support import import_helper
@ -16,13 +17,281 @@ from test import _code_definitions as code_defs
from test import _crossinterp_definitions as defs
BUILTIN_TYPES = [o for _, o in __builtins__.items()
if isinstance(o, type)]
EXCEPTION_TYPES = [cls for cls in BUILTIN_TYPES
@contextlib.contextmanager
def ignore_byteswarning():
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=BytesWarning)
yield
# builtin types
BUILTINS_TYPES = [o for _, o in __builtins__.items() if isinstance(o, type)]
EXCEPTION_TYPES = [cls for cls in BUILTINS_TYPES
if issubclass(cls, BaseException)]
OTHER_TYPES = [o for n, o in vars(types).items()
if (isinstance(o, type) and
n not in ('DynamicClassAttribute', '_GeneratorWrapper'))]
n not in ('DynamicClassAttribute', '_GeneratorWrapper'))]
BUILTIN_TYPES = [
*BUILTINS_TYPES,
*OTHER_TYPES,
]
# builtin exceptions
try:
raise Exception
except Exception as exc:
CAUGHT = exc
EXCEPTIONS_WITH_SPECIAL_SIG = {
BaseExceptionGroup: (lambda msg: (msg, [CAUGHT])),
ExceptionGroup: (lambda msg: (msg, [CAUGHT])),
UnicodeError: (lambda msg: (None, msg, None, None, None)),
UnicodeEncodeError: (lambda msg: ('utf-8', '', 1, 3, msg)),
UnicodeDecodeError: (lambda msg: ('utf-8', b'', 1, 3, msg)),
UnicodeTranslateError: (lambda msg: ('', 1, 3, msg)),
}
BUILTIN_EXCEPTIONS = [
*(cls(*sig('error!')) for cls, sig in EXCEPTIONS_WITH_SPECIAL_SIG.items()),
*(cls('error!') for cls in EXCEPTION_TYPES
if cls not in EXCEPTIONS_WITH_SPECIAL_SIG),
]
# other builtin objects
METHOD = defs.SpamOkay().okay
BUILTIN_METHOD = [].append
METHOD_DESCRIPTOR_WRAPPER = str.join
METHOD_WRAPPER = object().__str__
WRAPPER_DESCRIPTOR = object.__init__
BUILTIN_WRAPPERS = {
METHOD: types.MethodType,
BUILTIN_METHOD: types.BuiltinMethodType,
dict.__dict__['fromkeys']: types.ClassMethodDescriptorType,
types.FunctionType.__code__: types.GetSetDescriptorType,
types.FunctionType.__globals__: types.MemberDescriptorType,
METHOD_DESCRIPTOR_WRAPPER: types.MethodDescriptorType,
METHOD_WRAPPER: types.MethodWrapperType,
WRAPPER_DESCRIPTOR: types.WrapperDescriptorType,
staticmethod(defs.SpamOkay.okay): None,
classmethod(defs.SpamOkay.okay): None,
property(defs.SpamOkay.okay): None,
}
BUILTIN_FUNCTIONS = [
# types.BuiltinFunctionType
len,
sys.is_finalizing,
sys.exit,
_testinternalcapi.get_crossinterp_data,
]
assert 'emptymod' not in sys.modules
with import_helper.ready_to_import('emptymod', ''):
import emptymod as EMPTYMOD
MODULES = [
sys,
defs,
unittest,
EMPTYMOD,
]
OBJECT = object()
EXCEPTION = Exception()
LAMBDA = (lambda: None)
BUILTIN_SIMPLE = [
OBJECT,
# singletons
None,
True,
False,
Ellipsis,
NotImplemented,
# bytes
*(i.to_bytes(2, 'little', signed=True)
for i in range(-1, 258)),
# str
'hello world',
'你好世界',
'',
# int
sys.maxsize + 1,
sys.maxsize,
-sys.maxsize - 1,
-sys.maxsize - 2,
*range(-1, 258),
2**1000,
# float
0.0,
1.1,
-1.0,
0.12345678,
-0.12345678,
]
TUPLE_EXCEPTION = (0, 1.0, EXCEPTION)
TUPLE_OBJECT = (0, 1.0, OBJECT)
TUPLE_NESTED_EXCEPTION = (0, 1.0, (EXCEPTION,))
TUPLE_NESTED_OBJECT = (0, 1.0, (OBJECT,))
MEMORYVIEW_EMPTY = memoryview(b'')
MEMORYVIEW_NOT_EMPTY = memoryview(b'spam'*42)
MAPPING_PROXY_EMPTY = types.MappingProxyType({})
BUILTIN_CONTAINERS = [
# tuple (flat)
(),
(1,),
("hello", "world", ),
(1, True, "hello"),
TUPLE_EXCEPTION,
TUPLE_OBJECT,
# tuple (nested)
((1,),),
((1, 2), (3, 4)),
((1, 2), (3, 4), (5, 6)),
TUPLE_NESTED_EXCEPTION,
TUPLE_NESTED_OBJECT,
# buffer
MEMORYVIEW_EMPTY,
MEMORYVIEW_NOT_EMPTY,
# list
[],
[1, 2, 3],
[[1], (2,), {3: 4}],
# dict
{},
{1: 7, 2: 8, 3: 9},
{1: [1], 2: (2,), 3: {3: 4}},
# set
set(),
{1, 2, 3},
{frozenset({1}), (2,)},
# frozenset
frozenset([]),
frozenset({frozenset({1}), (2,)}),
# bytearray
bytearray(b''),
# other
MAPPING_PROXY_EMPTY,
types.SimpleNamespace(),
]
ns = {}
exec("""
try:
raise Exception
except Exception as exc:
TRACEBACK = exc.__traceback__
FRAME = TRACEBACK.tb_frame
""", ns, ns)
BUILTIN_OTHER = [
# types.CellType
types.CellType(),
# types.FrameType
ns['FRAME'],
# types.TracebackType
ns['TRACEBACK'],
]
del ns
# user-defined objects
USER_TOP_INSTANCES = [c(*a) for c, a in defs.TOP_CLASSES.items()]
USER_NESTED_INSTANCES = [c(*a) for c, a in defs.NESTED_CLASSES.items()]
USER_INSTANCES = [
*USER_TOP_INSTANCES,
*USER_NESTED_INSTANCES,
]
USER_EXCEPTIONS = [
defs.MimimalError('error!'),
]
# shareable objects
TUPLES_WITHOUT_EQUALITY = [
TUPLE_EXCEPTION,
TUPLE_OBJECT,
TUPLE_NESTED_EXCEPTION,
TUPLE_NESTED_OBJECT,
]
_UNSHAREABLE_SIMPLE = [
Ellipsis,
NotImplemented,
OBJECT,
sys.maxsize + 1,
-sys.maxsize - 2,
2**1000,
]
with ignore_byteswarning():
_SHAREABLE_SIMPLE = [o for o in BUILTIN_SIMPLE
if o not in _UNSHAREABLE_SIMPLE]
_SHAREABLE_CONTAINERS = [
*(o for o in BUILTIN_CONTAINERS if type(o) is memoryview),
*(o for o in BUILTIN_CONTAINERS
if type(o) is tuple and o not in TUPLES_WITHOUT_EQUALITY),
]
_UNSHAREABLE_CONTAINERS = [o for o in BUILTIN_CONTAINERS
if o not in _SHAREABLE_CONTAINERS]
SHAREABLE = [
*_SHAREABLE_SIMPLE,
*_SHAREABLE_CONTAINERS,
]
NOT_SHAREABLE = [
*_UNSHAREABLE_SIMPLE,
*_UNSHAREABLE_CONTAINERS,
*BUILTIN_TYPES,
*BUILTIN_WRAPPERS,
*BUILTIN_EXCEPTIONS,
*BUILTIN_FUNCTIONS,
*MODULES,
*BUILTIN_OTHER,
# types.CodeType
*(f.__code__ for f in defs.FUNCTIONS),
*(f.__code__ for f in defs.FUNCTION_LIKE),
# types.FunctionType
*defs.FUNCTIONS,
defs.SpamOkay.okay,
LAMBDA,
*defs.FUNCTION_LIKE,
# coroutines and generators
*defs.FUNCTION_LIKE_APPLIED,
# user classes
*defs.CLASSES,
*USER_INSTANCES,
# user exceptions
*USER_EXCEPTIONS,
]
# pickleable objects
PICKLEABLE = [
*BUILTIN_SIMPLE,
*(o for o in BUILTIN_CONTAINERS if o not in [
MEMORYVIEW_EMPTY,
MEMORYVIEW_NOT_EMPTY,
MAPPING_PROXY_EMPTY,
] or type(o) is dict),
*BUILTINS_TYPES,
*BUILTIN_EXCEPTIONS,
*BUILTIN_FUNCTIONS,
*defs.TOP_FUNCTIONS,
defs.SpamOkay.okay,
*defs.FUNCTION_LIKE,
*defs.TOP_CLASSES,
*USER_TOP_INSTANCES,
*USER_EXCEPTIONS,
# from OTHER_TYPES
types.NoneType,
types.EllipsisType,
types.NotImplementedType,
types.GenericAlias,
types.UnionType,
types.SimpleNamespace,
# from BUILTIN_WRAPPERS
METHOD,
BUILTIN_METHOD,
METHOD_DESCRIPTOR_WRAPPER,
METHOD_WRAPPER,
WRAPPER_DESCRIPTOR,
]
assert not any(isinstance(o, types.MappingProxyType) for o in PICKLEABLE)
# helpers
DEFS = defs
with open(code_defs.__file__) as infile:
@ -111,6 +380,77 @@ class _GetXIDataTests(unittest.TestCase):
MODE = None
def assert_functions_equal(self, func1, func2):
assert type(func1) is types.FunctionType, repr(func1)
assert type(func2) is types.FunctionType, repr(func2)
self.assertEqual(func1.__name__, func2.__name__)
self.assertEqual(func1.__code__, func2.__code__)
self.assertEqual(func1.__defaults__, func2.__defaults__)
self.assertEqual(func1.__kwdefaults__, func2.__kwdefaults__)
# We don't worry about __globals__ for now.
def assert_exc_args_equal(self, exc1, exc2):
args1 = exc1.args
args2 = exc2.args
if isinstance(exc1, ExceptionGroup):
self.assertIs(type(args1), type(args2))
self.assertEqual(len(args1), 2)
self.assertEqual(len(args1), len(args2))
self.assertEqual(args1[0], args2[0])
group1 = args1[1]
group2 = args2[1]
self.assertEqual(len(group1), len(group2))
for grouped1, grouped2 in zip(group1, group2):
# Currently the "extra" attrs are not preserved
# (via __reduce__).
self.assertIs(type(exc1), type(exc2))
self.assert_exc_equal(grouped1, grouped2)
else:
self.assertEqual(args1, args2)
def assert_exc_equal(self, exc1, exc2):
self.assertIs(type(exc1), type(exc2))
if type(exc1).__eq__ is not object.__eq__:
self.assertEqual(exc1, exc2)
self.assert_exc_args_equal(exc1, exc2)
# XXX For now we do not preserve tracebacks.
if exc1.__traceback__ is not None:
self.assertEqual(exc1.__traceback__, exc2.__traceback__)
self.assertEqual(
getattr(exc1, '__notes__', None),
getattr(exc2, '__notes__', None),
)
# We assume there are no cycles.
if exc1.__cause__ is None:
self.assertIs(exc1.__cause__, exc2.__cause__)
else:
self.assert_exc_equal(exc1.__cause__, exc2.__cause__)
if exc1.__context__ is None:
self.assertIs(exc1.__context__, exc2.__context__)
else:
self.assert_exc_equal(exc1.__context__, exc2.__context__)
def assert_equal_or_equalish(self, obj, expected):
cls = type(expected)
if cls.__eq__ is not object.__eq__:
self.assertEqual(obj, expected)
elif cls is types.FunctionType:
self.assert_functions_equal(obj, expected)
elif isinstance(expected, BaseException):
self.assert_exc_equal(obj, expected)
elif cls is types.MethodType:
raise NotImplementedError(cls)
elif cls is types.BuiltinMethodType:
raise NotImplementedError(cls)
elif cls is types.MethodWrapperType:
raise NotImplementedError(cls)
elif cls.__bases__ == (object,):
self.assertEqual(obj.__dict__, expected.__dict__)
else:
raise NotImplementedError(cls)
def get_xidata(self, obj, *, mode=None):
mode = self._resolve_mode(mode)
return _testinternalcapi.get_crossinterp_data(obj, mode)
@ -126,35 +466,37 @@ class _GetXIDataTests(unittest.TestCase):
def assert_roundtrip_identical(self, values, *, mode=None):
mode = self._resolve_mode(mode)
for obj in values:
with self.subTest(obj):
with self.subTest(repr(obj)):
got = self._get_roundtrip(obj, mode)
self.assertIs(got, obj)
def assert_roundtrip_equal(self, values, *, mode=None, expecttype=None):
mode = self._resolve_mode(mode)
for obj in values:
with self.subTest(obj):
with self.subTest(repr(obj)):
got = self._get_roundtrip(obj, mode)
self.assertEqual(got, obj)
if got is obj:
continue
self.assertIs(type(got),
type(obj) if expecttype is None else expecttype)
self.assert_equal_or_equalish(got, obj)
def assert_roundtrip_equal_not_identical(self, values, *,
mode=None, expecttype=None):
mode = self._resolve_mode(mode)
for obj in values:
with self.subTest(obj):
with self.subTest(repr(obj)):
got = self._get_roundtrip(obj, mode)
self.assertIsNot(got, obj)
self.assertIs(type(got),
type(obj) if expecttype is None else expecttype)
self.assertEqual(got, obj)
self.assert_equal_or_equalish(got, obj)
def assert_roundtrip_not_equal(self, values, *,
mode=None, expecttype=None):
mode = self._resolve_mode(mode)
for obj in values:
with self.subTest(obj):
with self.subTest(repr(obj)):
got = self._get_roundtrip(obj, mode)
self.assertIsNot(got, obj)
self.assertIs(type(got),
@ -164,7 +506,7 @@ class _GetXIDataTests(unittest.TestCase):
def assert_not_shareable(self, values, exctype=None, *, mode=None):
mode = self._resolve_mode(mode)
for obj in values:
with self.subTest(obj):
with self.subTest(repr(obj)):
with self.assertRaises(NotShareableError) as cm:
_testinternalcapi.get_crossinterp_data(obj, mode)
if exctype is not None:
@ -182,49 +524,26 @@ class PickleTests(_GetXIDataTests):
MODE = 'pickle'
def test_shareable(self):
self.assert_roundtrip_equal([
# singletons
None,
True,
False,
# bytes
*(i.to_bytes(2, 'little', signed=True)
for i in range(-1, 258)),
# str
'hello world',
'你好世界',
'',
# int
sys.maxsize,
-sys.maxsize - 1,
*range(-1, 258),
# float
0.0,
1.1,
-1.0,
0.12345678,
-0.12345678,
# tuple
(),
(1,),
("hello", "world", ),
(1, True, "hello"),
((1,),),
((1, 2), (3, 4)),
((1, 2), (3, 4), (5, 6)),
])
# not shareable using xidata
self.assert_roundtrip_equal([
# int
sys.maxsize + 1,
-sys.maxsize - 2,
2**1000,
# tuple
(0, 1.0, []),
(0, 1.0, {}),
(0, 1.0, ([],)),
(0, 1.0, ({},)),
])
with ignore_byteswarning():
for obj in SHAREABLE:
if obj in PICKLEABLE:
self.assert_roundtrip_equal([obj])
else:
self.assert_not_shareable([obj])
def test_not_shareable(self):
with ignore_byteswarning():
for obj in NOT_SHAREABLE:
if type(obj) is types.MappingProxyType:
self.assert_not_shareable([obj])
elif obj in PICKLEABLE:
with self.subTest(repr(obj)):
# We don't worry about checking the actual value.
# The other tests should cover that well enough.
got = self.get_roundtrip(obj)
self.assertIs(type(got), type(obj))
else:
self.assert_not_shareable([obj])
def test_list(self):
self.assert_roundtrip_equal_not_identical([
@ -266,7 +585,7 @@ class PickleTests(_GetXIDataTests):
if cls not in defs.CLASSES_WITHOUT_EQUALITY:
continue
instances.append(cls(*args))
self.assert_roundtrip_not_equal(instances)
self.assert_roundtrip_equal(instances)
def assert_class_defs_other_pickle(self, defs, mod):
# Pickle relative to a different module than the original.
@ -286,7 +605,7 @@ class PickleTests(_GetXIDataTests):
instances = []
for cls, args in defs.TOP_CLASSES.items():
with self.subTest(cls):
with self.subTest(repr(cls)):
setattr(mod, cls.__name__, cls)
xid = self.get_xidata(cls)
inst = cls(*args)
@ -295,7 +614,7 @@ class PickleTests(_GetXIDataTests):
(cls, xid, inst, instxid))
for cls, xid, inst, instxid in instances:
with self.subTest(cls):
with self.subTest(repr(cls)):
delattr(mod, cls.__name__)
if fail:
with self.assertRaises(NotShareableError):
@ -403,13 +722,13 @@ class PickleTests(_GetXIDataTests):
def assert_func_defs_other_pickle(self, defs, mod):
# Pickle relative to a different module than the original.
for func in defs.TOP_FUNCTIONS:
assert not hasattr(mod, func.__name__), (cls, getattr(mod, func.__name__))
assert not hasattr(mod, func.__name__), (getattr(mod, func.__name__),)
self.assert_not_shareable(defs.TOP_FUNCTIONS)
def assert_func_defs_other_unpickle(self, defs, mod, *, fail=False):
# Unpickle relative to a different module than the original.
for func in defs.TOP_FUNCTIONS:
assert not hasattr(mod, func.__name__), (cls, getattr(mod, func.__name__))
assert not hasattr(mod, func.__name__), (getattr(mod, func.__name__),)
captured = []
for func in defs.TOP_FUNCTIONS:
@ -434,7 +753,7 @@ class PickleTests(_GetXIDataTests):
self.assert_not_shareable(defs.TOP_FUNCTIONS)
def test_user_function_normal(self):
# self.assert_roundtrip_equal(defs.TOP_FUNCTIONS)
self.assert_roundtrip_equal(defs.TOP_FUNCTIONS)
self.assert_func_defs_same(defs)
def test_user_func_in___main__(self):
@ -505,7 +824,7 @@ class PickleTests(_GetXIDataTests):
# exceptions
def test_user_exception_normal(self):
self.assert_roundtrip_not_equal([
self.assert_roundtrip_equal([
defs.MimimalError('error!'),
])
self.assert_roundtrip_equal_not_identical([
@ -521,7 +840,7 @@ class PickleTests(_GetXIDataTests):
special = {
BaseExceptionGroup: (msg, [caught]),
ExceptionGroup: (msg, [caught]),
# UnicodeError: (None, msg, None, None, None),
UnicodeError: (None, msg, None, None, None),
UnicodeEncodeError: ('utf-8', '', 1, 3, msg),
UnicodeDecodeError: ('utf-8', b'', 1, 3, msg),
UnicodeTranslateError: ('', 1, 3, msg),
@ -531,7 +850,7 @@ class PickleTests(_GetXIDataTests):
args = special.get(cls) or (msg,)
exceptions.append(cls(*args))
self.assert_roundtrip_not_equal(exceptions)
self.assert_roundtrip_equal(exceptions)
class MarshalTests(_GetXIDataTests):
@ -576,7 +895,7 @@ class MarshalTests(_GetXIDataTests):
'',
])
self.assert_not_shareable([
object(),
OBJECT,
types.SimpleNamespace(),
])
@ -647,10 +966,7 @@ class MarshalTests(_GetXIDataTests):
shareable = [
StopIteration,
]
types = [
*BUILTIN_TYPES,
*OTHER_TYPES,
]
types = BUILTIN_TYPES
self.assert_not_shareable(cls for cls in types
if cls not in shareable)
self.assert_roundtrip_identical(cls for cls in types
@ -763,7 +1079,7 @@ class ShareableFuncTests(_GetXIDataTests):
MODE = 'func'
def test_stateless(self):
self.assert_roundtrip_not_equal([
self.assert_roundtrip_equal([
*defs.STATELESS_FUNCTIONS,
# Generators can be stateless too.
*defs.FUNCTION_LIKE,
@ -912,10 +1228,49 @@ class ShareableScriptTests(PureShareableScriptTests):
], expecttype=types.CodeType)
class ShareableFallbackTests(_GetXIDataTests):
MODE = 'fallback'
def test_shareable(self):
self.assert_roundtrip_equal(SHAREABLE)
def test_not_shareable(self):
okay = [
*PICKLEABLE,
*defs.STATELESS_FUNCTIONS,
LAMBDA,
]
ignored = [
*TUPLES_WITHOUT_EQUALITY,
OBJECT,
METHOD,
BUILTIN_METHOD,
METHOD_WRAPPER,
]
with ignore_byteswarning():
self.assert_roundtrip_equal([
*(o for o in NOT_SHAREABLE
if o in okay and o not in ignored
and o is not MAPPING_PROXY_EMPTY),
])
self.assert_roundtrip_not_equal([
*(o for o in NOT_SHAREABLE
if o in ignored and o is not MAPPING_PROXY_EMPTY),
])
self.assert_not_shareable([
*(o for o in NOT_SHAREABLE if o not in okay),
MAPPING_PROXY_EMPTY,
])
class ShareableTypeTests(_GetXIDataTests):
MODE = 'xidata'
def test_shareable(self):
self.assert_roundtrip_equal(SHAREABLE)
def test_singletons(self):
self.assert_roundtrip_identical([
None,
@ -983,8 +1338,8 @@ class ShareableTypeTests(_GetXIDataTests):
def test_tuples_containing_non_shareable_types(self):
non_shareables = [
Exception(),
object(),
EXCEPTION,
OBJECT,
]
for s in non_shareables:
value = tuple([0, 1.0, s])
@ -999,6 +1354,9 @@ class ShareableTypeTests(_GetXIDataTests):
# The rest are not shareable.
def test_not_shareable(self):
self.assert_not_shareable(NOT_SHAREABLE)
def test_object(self):
self.assert_not_shareable([
object(),
@ -1015,12 +1373,12 @@ class ShareableTypeTests(_GetXIDataTests):
for func in defs.FUNCTIONS:
assert type(func) is types.FunctionType, func
assert type(defs.SpamOkay.okay) is types.FunctionType, func
assert type(lambda: None) is types.LambdaType
assert type(LAMBDA) is types.LambdaType
self.assert_not_shareable([
*defs.FUNCTIONS,
defs.SpamOkay.okay,
(lambda: None),
LAMBDA,
])
def test_builtin_function(self):
@ -1085,10 +1443,7 @@ class ShareableTypeTests(_GetXIDataTests):
self.assert_not_shareable(instances)
def test_builtin_type(self):
self.assert_not_shareable([
*BUILTIN_TYPES,
*OTHER_TYPES,
])
self.assert_not_shareable(BUILTIN_TYPES)
def test_exception(self):
self.assert_not_shareable([
@ -1127,7 +1482,7 @@ class ShareableTypeTests(_GetXIDataTests):
""", ns, ns)
self.assert_not_shareable([
types.MappingProxyType({}),
MAPPING_PROXY_EMPTY,
types.SimpleNamespace(),
# types.CellType
types.CellType(),