https://github.com/python/cpython/commit/bdd23c0bb95faa37130fdf7af82f0fdddb41bae0
commit: bdd23c0bb95faa37130fdf7af82f0fdddb41bae0
branch: main
author: Eric Snow <ericsnowcurren...@gmail.com>
committer: ericsnowcurrently <ericsnowcurren...@gmail.com>
date: 2025-04-28T17:23:46-06:00
summary:

gh-132775: Add _PyMarshal_GetXIData() (gh-133108)

Note that the bulk of this change is tests.

files:
M Include/internal/pycore_crossinterp.h
M Lib/test/_crossinterp_definitions.py
M Lib/test/test_crossinterp.py
M Modules/_testinternalcapi.c
M Python/crossinterp.c

diff --git a/Include/internal/pycore_crossinterp.h 
b/Include/internal/pycore_crossinterp.h
index 5cf9f8fb5a0388..4b7446a1f40ccf 100644
--- a/Include/internal/pycore_crossinterp.h
+++ b/Include/internal/pycore_crossinterp.h
@@ -171,6 +171,13 @@ PyAPI_FUNC(_PyBytes_data_t *) _PyBytes_GetXIDataWrapped(
         xid_newobjfunc,
         _PyXIData_t *);
 
+// _PyObject_GetXIData() for marshal
+PyAPI_FUNC(PyObject *) _PyMarshal_ReadObjectFromXIData(_PyXIData_t *);
+PyAPI_FUNC(int) _PyMarshal_GetXIData(
+        PyThreadState *,
+        PyObject *,
+        _PyXIData_t *);
+
 
 /* using cross-interpreter data */
 
diff --git a/Lib/test/_crossinterp_definitions.py 
b/Lib/test/_crossinterp_definitions.py
index 9b52aea39522f5..0d5f6c7db064d3 100644
--- a/Lib/test/_crossinterp_definitions.py
+++ b/Lib/test/_crossinterp_definitions.py
@@ -100,7 +100,7 @@ def ham_C_closure(z):
 ham_C_closure, *_ = eggs_closure_C(2)
 
 
-FUNCTIONS = [
+TOP_FUNCTIONS = [
     # shallow
     spam_minimal,
     spam_full,
@@ -112,6 +112,8 @@ def ham_C_closure(z):
     spam_NC,
     spam_CN,
     spam_CC,
+]
+NESTED_FUNCTIONS = [
     # inner func
     eggs_nested,
     eggs_closure,
@@ -125,6 +127,10 @@ def ham_C_closure(z):
     ham_C_nested,
     ham_C_closure,
 ]
+FUNCTIONS = [
+    *TOP_FUNCTIONS,
+    *NESTED_FUNCTIONS,
+]
 
 
 #######################################
@@ -157,8 +163,10 @@ async def asyncgen_spam(*args):
     gen_spam_1,
     gen_spam_2,
     async_spam,
-    coro_spam,  # actually FunctionType?
     asyncgen_spam,
+]
+FUNCTION_LIKE_APPLIED = [
+    coro_spam,  # actually FunctionType?
     asynccoro_spam,  # actually FunctionType?
 ]
 
@@ -202,6 +210,13 @@ def __init__(self, a, b, c):
     # __str__
     # ...
 
+    def __eq__(self, other):
+        if not isinstance(other, SpamFull):
+            return NotImplemented
+        return (self.a == other.a and
+                self.b == other.b and
+                self.c == other.c)
+
     @property
     def prop(self):
         return True
@@ -222,9 +237,47 @@ class EggsNested:
 EggsNested = class_eggs_inner()
 
 
+TOP_CLASSES = {
+    Spam: (),
+    SpamOkay: (),
+    SpamFull: (1, 2, 3),
+    SubSpamFull: (1, 2, 3),
+    SubTuple: ([1, 2, 3],),
+}
+CLASSES_WITHOUT_EQUALITY = [
+    Spam,
+    SpamOkay,
+]
+BUILTIN_SUBCLASSES = [
+    SubTuple,
+]
+NESTED_CLASSES = {
+    EggsNested: (),
+}
+CLASSES = {
+    **TOP_CLASSES,
+    **NESTED_CLASSES,
+}
+
 
 #######################################
 # exceptions
 
 class MimimalError(Exception):
     pass
+
+
+class RichError(Exception):
+    def __init__(self, msg, value=None):
+        super().__init__(msg, value)
+        self.msg = msg
+        self.value = value
+
+    def __eq__(self, other):
+        if not isinstance(other, RichError):
+            return NotImplemented
+        if self.msg != other.msg:
+            return False
+        if self.value != other.value:
+            return False
+        return True
diff --git a/Lib/test/test_crossinterp.py b/Lib/test/test_crossinterp.py
index e1d1998fefc7fb..5ebb78b0ea9e3b 100644
--- a/Lib/test/test_crossinterp.py
+++ b/Lib/test/test_crossinterp.py
@@ -17,6 +17,9 @@
                  if isinstance(o, type)]
 EXCEPTION_TYPES = [cls for cls in BUILTIN_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'))]
 
 
 class _GetXIDataTests(unittest.TestCase):
@@ -40,16 +43,42 @@ def iter_roundtrip_values(self, values, *, mode=None):
                 got = _testinternalcapi.restore_crossinterp_data(xid)
                 yield obj, got
 
-    def assert_roundtrip_equal(self, values, *, mode=None):
-        for obj, got in self.iter_roundtrip_values(values, mode=mode):
-             self.assertEqual(got, obj)
-             self.assertIs(type(got), type(obj))
-
     def assert_roundtrip_identical(self, values, *, mode=None):
         for obj, got in self.iter_roundtrip_values(values, mode=mode):
             # XXX What about between interpreters?
             self.assertIs(got, obj)
 
+    def assert_roundtrip_equal(self, values, *, mode=None, expecttype=None):
+        for obj, got in self.iter_roundtrip_values(values, mode=mode):
+            self.assertEqual(got, obj)
+            self.assertIs(type(got),
+                          type(obj) if expecttype is None else expecttype)
+
+#    def assert_roundtrip_equal_not_identical(self, values, *,
+#                                            mode=None, expecttype=None):
+#        mode = self._resolve_mode(mode)
+#        for obj in values:
+#            cls = type(obj)
+#            with self.subTest(obj):
+#                got = self._get_roundtrip(obj, mode)
+#                self.assertIsNot(got, obj)
+#                self.assertIs(type(got), type(obj))
+#                self.assertEqual(got, obj)
+#                self.assertIs(type(got),
+#                              cls if expecttype is None else expecttype)
+#
+#    def assert_roundtrip_not_equal(self, values, *, mode=None, 
expecttype=None):
+#        mode = self._resolve_mode(mode)
+#        for obj in values:
+#            cls = type(obj)
+#            with self.subTest(obj):
+#                got = self._get_roundtrip(obj, mode)
+#                self.assertIsNot(got, obj)
+#                self.assertIs(type(got), type(obj))
+#                self.assertNotEqual(got, obj)
+#                self.assertIs(type(got),
+#                              cls if expecttype is None else expecttype)
+
     def assert_not_shareable(self, values, exctype=None, *, mode=None):
         mode = self._resolve_mode(mode)
         for obj in values:
@@ -66,6 +95,197 @@ def _resolve_mode(self, mode):
         return mode
 
 
+class MarshalTests(_GetXIDataTests):
+
+    MODE = 'marshal'
+
+    def test_simple_builtin_singletons(self):
+        self.assert_roundtrip_identical([
+            True,
+            False,
+            None,
+            Ellipsis,
+        ])
+        self.assert_not_shareable([
+            NotImplemented,
+        ])
+
+    def test_simple_builtin_objects(self):
+        self.assert_roundtrip_equal([
+            # int
+            *range(-1, 258),
+            sys.maxsize + 1,
+            sys.maxsize,
+            -sys.maxsize - 1,
+            -sys.maxsize - 2,
+            2**1000,
+            # complex
+            1+2j,
+            # float
+            0.0,
+            1.1,
+            -1.0,
+            0.12345678,
+            -0.12345678,
+            # bytes
+            *(i.to_bytes(2, 'little', signed=True)
+              for i in range(-1, 258)),
+            b'hello world',
+            # str
+            'hello world',
+            '你好世界',
+            '',
+        ])
+        self.assert_not_shareable([
+            object(),
+            types.SimpleNamespace(),
+        ])
+
+    def test_bytearray(self):
+        # bytearray is special because it unmarshals to bytes, not bytearray.
+        self.assert_roundtrip_equal([
+            bytearray(),
+            bytearray(b'hello world'),
+        ], expecttype=bytes)
+
+    def test_compound_immutable_builtin_objects(self):
+        self.assert_roundtrip_equal([
+            # tuple
+            (),
+            (1,),
+            ("hello", "world"),
+            (1, True, "hello"),
+            # frozenset
+            frozenset([1, 2, 3]),
+        ])
+        # nested
+        self.assert_roundtrip_equal([
+            # tuple
+            ((1,),),
+            ((1, 2), (3, 4)),
+            ((1, 2), (3, 4), (5, 6)),
+            # frozenset
+            frozenset([frozenset([1]), frozenset([2]), frozenset([3])]),
+        ])
+
+    def test_compound_mutable_builtin_objects(self):
+        self.assert_roundtrip_equal([
+            # list
+            [],
+            [1, 2, 3],
+            # dict
+            {},
+            {1: 7, 2: 8, 3: 9},
+            # set
+            set(),
+            {1, 2, 3},
+        ])
+        # nested
+        self.assert_roundtrip_equal([
+            [[1], [2], [3]],
+            {1: {'a': True}, 2: {'b': False}},
+            {(1, 2, 3,)},
+        ])
+
+    def test_compound_builtin_objects_with_bad_items(self):
+        bogus = object()
+        self.assert_not_shareable([
+            (bogus,),
+            frozenset([bogus]),
+            [bogus],
+            {bogus: True},
+            {True: bogus},
+            {bogus},
+        ])
+
+    def test_builtin_code(self):
+        self.assert_roundtrip_equal([
+            *(f.__code__ for f in defs.FUNCTIONS),
+            *(f.__code__ for f in defs.FUNCTION_LIKE),
+        ])
+
+    def test_builtin_type(self):
+        shareable = [
+            StopIteration,
+        ]
+        types = [
+            *BUILTIN_TYPES,
+            *OTHER_TYPES,
+        ]
+        self.assert_not_shareable(cls for cls in types
+                                  if cls not in shareable)
+        self.assert_roundtrip_identical(cls for cls in types
+                                        if cls in shareable)
+
+    def test_builtin_function(self):
+        functions = [
+            len,
+            sys.is_finalizing,
+            sys.exit,
+            _testinternalcapi.get_crossinterp_data,
+        ]
+        for func in functions:
+            assert type(func) is types.BuiltinFunctionType, func
+
+        self.assert_not_shareable(functions)
+
+    def test_builtin_exception(self):
+        msg = 'error!'
+        try:
+            raise Exception
+        except Exception as exc:
+            caught = exc
+        special = {
+            BaseExceptionGroup: (msg, [caught]),
+            ExceptionGroup: (msg, [caught]),
+#            UnicodeError: (None, msg, None, None, None),
+            UnicodeEncodeError: ('utf-8', '', 1, 3, msg),
+            UnicodeDecodeError: ('utf-8', b'', 1, 3, msg),
+            UnicodeTranslateError: ('', 1, 3, msg),
+        }
+        exceptions = []
+        for cls in EXCEPTION_TYPES:
+            args = special.get(cls) or (msg,)
+            exceptions.append(cls(*args))
+
+        self.assert_not_shareable(exceptions)
+        # Note that StopIteration (the type) can be marshalled,
+        # but its instances cannot.
+
+    def test_module(self):
+        assert type(sys) is types.ModuleType, type(sys)
+        assert type(defs) is types.ModuleType, type(defs)
+        assert type(unittest) is types.ModuleType, type(defs)
+
+        assert 'emptymod' not in sys.modules
+        with import_helper.ready_to_import('emptymod', ''):
+            import emptymod
+
+        self.assert_not_shareable([
+            sys,
+            defs,
+            unittest,
+            emptymod,
+        ])
+
+    def test_user_class(self):
+        self.assert_not_shareable(defs.TOP_CLASSES)
+
+        instances = []
+        for cls, args in defs.TOP_CLASSES.items():
+            instances.append(cls(*args))
+        self.assert_not_shareable(instances)
+
+    def test_user_function(self):
+        self.assert_not_shareable(defs.TOP_FUNCTIONS)
+
+    def test_user_exception(self):
+        self.assert_not_shareable([
+            defs.MimimalError('error!'),
+            defs.RichError('error!', 42),
+        ])
+
+
 class ShareableTypeTests(_GetXIDataTests):
 
     MODE = 'xidata'
@@ -184,6 +404,7 @@ def test_builtin_function(self):
 
     def test_function_like(self):
         self.assert_not_shareable(defs.FUNCTION_LIKE)
+        self.assert_not_shareable(defs.FUNCTION_LIKE_APPLIED)
 
     def test_builtin_wrapper(self):
         _wrappers = {
@@ -243,9 +464,7 @@ def test_class(self):
     def test_builtin_type(self):
         self.assert_not_shareable([
             *BUILTIN_TYPES,
-            *(o for n, o in vars(types).items()
-              if (isinstance(o, type) and
-                  n not in ('DynamicClassAttribute', '_GeneratorWrapper'))),
+            *OTHER_TYPES,
         ])
 
     def test_exception(self):
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 353cb630513abc..0ef064fe80d173 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -1730,6 +1730,11 @@ get_crossinterp_data(PyObject *self, PyObject *args, 
PyObject *kwargs)
             goto error;
         }
     }
+    else if (strcmp(mode, "marshal") == 0) {
+        if (_PyMarshal_GetXIData(tstate, obj, xidata) != 0) {
+            goto error;
+        }
+    }
     else {
         PyErr_Format(PyExc_ValueError, "unsupported mode %R", modeobj);
         goto error;
diff --git a/Python/crossinterp.c b/Python/crossinterp.c
index 662c9c72b15eb7..753d784a503467 100644
--- a/Python/crossinterp.c
+++ b/Python/crossinterp.c
@@ -2,6 +2,7 @@
 /* API for managing interactions between isolated interpreters */
 
 #include "Python.h"
+#include "marshal.h"              // PyMarshal_WriteObjectToString()
 #include "pycore_ceval.h"         // _Py_simple_func
 #include "pycore_crossinterp.h"   // _PyXIData_t
 #include "pycore_initconfig.h"    // _PyStatus_OK()
@@ -286,6 +287,48 @@ _PyObject_GetXIData(PyThreadState *tstate,
 }
 
 
+/* marshal wrapper */
+
+PyObject *
+_PyMarshal_ReadObjectFromXIData(_PyXIData_t *xidata)
+{
+    PyThreadState *tstate = _PyThreadState_GET();
+    _PyBytes_data_t *shared = (_PyBytes_data_t *)xidata->data;
+    PyObject *obj = PyMarshal_ReadObjectFromString(shared->bytes, shared->len);
+    if (obj == NULL) {
+        PyObject *cause = _PyErr_GetRaisedException(tstate);
+        assert(cause != NULL);
+        _set_xid_lookup_failure(
+                    tstate, NULL, "object could not be unmarshalled", cause);
+        Py_DECREF(cause);
+        return NULL;
+    }
+    return obj;
+}
+
+int
+_PyMarshal_GetXIData(PyThreadState *tstate, PyObject *obj, _PyXIData_t *xidata)
+{
+    PyObject *bytes = PyMarshal_WriteObjectToString(obj, Py_MARSHAL_VERSION);
+    if (bytes == NULL) {
+        PyObject *cause = _PyErr_GetRaisedException(tstate);
+        assert(cause != NULL);
+        _set_xid_lookup_failure(
+                    tstate, NULL, "object could not be marshalled", cause);
+        Py_DECREF(cause);
+        return -1;
+    }
+    size_t size = sizeof(_PyBytes_data_t);
+    _PyBytes_data_t *shared = _PyBytes_GetXIDataWrapped(
+            tstate, bytes, size, _PyMarshal_ReadObjectFromXIData, xidata);
+    Py_DECREF(bytes);
+    if (shared == NULL) {
+        return -1;
+    }
+    return 0;
+}
+
+
 /* using cross-interpreter data */
 
 PyObject *

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to