diff --git a/Include/cpython/immutability.h b/Include/cpython/immutability.h index d63aadff1da3a4..1ead57440f4d54 100644 --- a/Include/cpython/immutability.h +++ b/Include/cpython/immutability.h @@ -8,3 +8,5 @@ PyAPI_FUNC(int) _PyImmutability_Freeze(PyObject*); PyAPI_FUNC(int) _PyImmutability_RegisterFreezable(PyTypeObject*); PyAPI_FUNC(int) _PyImmutability_RegisterShallowImmutable(PyTypeObject*); PyAPI_FUNC(int) _PyImmutability_CanViewAsImmutable(PyObject*); +PyAPI_FUNC(int) _PyImmutability_SetFreezable(PyObject *, int); +PyAPI_FUNC(int) _PyImmutability_GetFreezable(PyObject *); diff --git a/Include/internal/pycore_immutability.h b/Include/internal/pycore_immutability.h index 92289afcafcc7d..27aceacb594e60 100644 --- a/Include/internal/pycore_immutability.h +++ b/Include/internal/pycore_immutability.h @@ -10,9 +10,15 @@ extern "C" { typedef struct _Py_hashtable_t _Py_hashtable_t; +typedef enum { + _Py_FREEZABLE_YES = 0, + _Py_FREEZABLE_NO = 1, + _Py_FREEZABLE_EXPLICIT = 2, + _Py_FREEZABLE_PROXY = 3, +} _Py_freezable_status; + struct _Py_immutability_state { - PyObject *module_locks; - PyObject *blocking_on; + int late_init_done; PyObject *freezable_types; _Py_hashtable_t *shallow_immutable_types; PyObject *destroy_cb; diff --git a/Include/refcount.h b/Include/refcount.h index 97e2c2df1c8c01..4279a8f690b317 100644 --- a/Include/refcount.h +++ b/Include/refcount.h @@ -67,6 +67,13 @@ increase over time until it reaches _Py_IMMORTAL_INITIAL_REFCNT. #define _Py_IMMUTABLE_DIRECT (_Py_IMMUTABLE_FLAG) #define _Py_IMMUTABLE_INDIRECT _Py_IMMUTABLE_MASK #define _Py_IMMUTABLE_PENDING (_Py_IMMUTABLE_SCC_FLAG) + +// Per-object freezable status stored in ob_flags (64-bit only). +// Bit 5-6: 2-bit enum value (_Py_freezable_status) +// Bit 7: set flag (1 = freezable status has been explicitly set) +#define _Py_FREEZABLE_SET_FLAG (1 << 7) +#define _Py_FREEZABLE_STATUS_SHIFT 5 +#define _Py_FREEZABLE_STATUS_MASK (0x3 << _Py_FREEZABLE_STATUS_SHIFT) #else /* In 32 bit systems, an object will be treated as immortal if its reference diff --git a/Lib/immutable.py b/Lib/immutable.py index 6a0f9b7e8e2b60..85ff4e1df8d5c5 100644 --- a/Lib/immutable.py +++ b/Lib/immutable.py @@ -11,17 +11,27 @@ register_freezable = _c.register_freezable freeze = _c.freeze isfrozen = _c.isfrozen +set_freezable = _c.set_freezable NotFreezable = getattr(_c, "NotFreezable", None) NotFreezableError = _c.NotFreezableError ImmutableModule = _c.ImmutableModule +FREEZABLE_YES = _c.FREEZABLE_YES +FREEZABLE_NO = _c.FREEZABLE_NO +FREEZABLE_EXPLICIT = _c.FREEZABLE_EXPLICIT +FREEZABLE_PROXY = _c.FREEZABLE_PROXY __all__ = [ "register_freezable", "freeze", "isfrozen", + "set_freezable", "NotFreezable", "NotFreezableError", "ImmutableModule", + "FREEZABLE_YES", + "FREEZABLE_NO", + "FREEZABLE_EXPLICIT", + "FREEZABLE_PROXY", ] __version__ = getattr(_c, "__version__", "1.0") diff --git a/Lib/test/test_freeze/test_set_freezable.py b/Lib/test/test_freeze/test_set_freezable.py new file mode 100644 index 00000000000000..0c8cfdbff708c8 --- /dev/null +++ b/Lib/test/test_freeze/test_set_freezable.py @@ -0,0 +1,279 @@ +import gc +import unittest +import weakref +from immutable import ( + freeze, isfrozen, register_freezable, set_freezable, + FREEZABLE_YES, FREEZABLE_NO, FREEZABLE_EXPLICIT, FREEZABLE_PROXY, +) + + +def make_freezable_class(): + """Create a fresh class registered as freezable.""" + class C: + pass + register_freezable(C) + return C + + +class TestSetFreezableYes(unittest.TestCase): + """FREEZABLE_YES: object is always freezable.""" + + def test_freeze_succeeds(self): + C = make_freezable_class() + obj = C() + set_freezable(obj, FREEZABLE_YES) + freeze(obj) + self.assertTrue(isfrozen(obj)) + + def test_freeze_as_child_succeeds(self): + C = make_freezable_class() + parent = C() + child = C() + parent.child = child + set_freezable(child, FREEZABLE_YES) + freeze(parent) + self.assertTrue(isfrozen(child)) + + +class TestSetFreezableNo(unittest.TestCase): + """FREEZABLE_NO: object can never be frozen.""" + + def test_freeze_raises(self): + C = make_freezable_class() + obj = C() + set_freezable(obj, FREEZABLE_NO) + with self.assertRaises(TypeError): + freeze(obj) + self.assertFalse(isfrozen(obj)) + + def test_freeze_as_child_raises(self): + C = make_freezable_class() + parent = C() + child = C() + parent.child = child + set_freezable(child, FREEZABLE_NO) + with self.assertRaises(TypeError): + freeze(parent) + self.assertFalse(isfrozen(child)) + self.assertFalse(isfrozen(parent)) + + +class TestSetFreezableExplicit(unittest.TestCase): + """FREEZABLE_EXPLICIT: freezable only when freeze() is called directly on it.""" + + def test_direct_freeze_succeeds(self): + C = make_freezable_class() + obj = C() + set_freezable(obj, FREEZABLE_EXPLICIT) + freeze(obj) + self.assertTrue(isfrozen(obj)) + + def test_child_freeze_raises(self): + C = make_freezable_class() + parent = C() + child = C() + parent.child = child + set_freezable(child, FREEZABLE_EXPLICIT) + with self.assertRaises(TypeError): + freeze(parent) + self.assertFalse(isfrozen(child)) + + +class TestSetFreezableProxy(unittest.TestCase): + """FREEZABLE_PROXY: only allowed on module objects.""" + + def test_proxy_rejected_on_non_module(self): + C = make_freezable_class() + obj = C() + with self.assertRaises(TypeError): + set_freezable(obj, FREEZABLE_PROXY) + + def test_proxy_allowed_on_module(self): + import types + mod = types.ModuleType('test_proxy_mod') + set_freezable(mod, FREEZABLE_PROXY) + + +class TestSetFreezableEdgeCases(unittest.TestCase): + """Edge cases and error handling.""" + + def test_invalid_status_raises(self): + C = make_freezable_class() + obj = C() + with self.assertRaises(ValueError): + set_freezable(obj, 99) + with self.assertRaises(ValueError): + set_freezable(obj, -1) + + def test_object_without_dict_uses_ob_flags(self): + # Built-in ints don't support attributes, but ob_flags fallback + # should work on 64-bit. + import sys + if sys.maxsize <= 2**31: + self.skipTest("ob_flags fallback not available on 32-bit") + set_freezable(42, FREEZABLE_NO) + # Can't easily verify the flags directly, but it shouldn't raise. + + def test_gc_collects_tracked_object(self): + C = make_freezable_class() + obj = C() + ref = weakref.ref(obj) + set_freezable(obj, FREEZABLE_NO) + del obj + gc.collect() + self.assertIsNone(ref()) + + def test_override_status(self): + C = make_freezable_class() + obj = C() + set_freezable(obj, FREEZABLE_NO) + with self.assertRaises(TypeError): + freeze(obj) + # Override to YES + set_freezable(obj, FREEZABLE_YES) + freeze(obj) + self.assertTrue(isfrozen(obj)) + + def test_unset_object_uses_default(self): + # An object with no set_freezable should use existing freeze logic. + C = make_freezable_class() + obj = C() + freeze(obj) + self.assertTrue(isfrozen(obj)) + + +class TestSetFreezableStorage(unittest.TestCase): + """Test the attribute-first, weakref-fallback storage strategy.""" + + def test_attr_storage_for_normal_objects(self): + # Objects with __dict__ should get __freezable__ attribute set. + C = make_freezable_class() + obj = C() + set_freezable(obj, FREEZABLE_NO) + self.assertEqual(obj.__freezable__, FREEZABLE_NO) + + def test_attr_stores_each_status(self): + C = make_freezable_class() + for status in (FREEZABLE_YES, FREEZABLE_NO, + FREEZABLE_EXPLICIT): + obj = C() + set_freezable(obj, status) + self.assertEqual(obj.__freezable__, status, + f"__freezable__ should be {status}") + + def test_attr_storage_updates_on_override(self): + C = make_freezable_class() + obj = C() + set_freezable(obj, FREEZABLE_NO) + self.assertEqual(obj.__freezable__, FREEZABLE_NO) + set_freezable(obj, FREEZABLE_YES) + self.assertEqual(obj.__freezable__, FREEZABLE_YES) + + def test_ob_flags_fallback_for_slots_only(self): + # Objects with __slots__ but no __dict__ should fall back + # to ob_flags on 64-bit. + import sys + if sys.maxsize <= 2**31: + self.skipTest("ob_flags fallback not available on 32-bit") + class S: + __slots__ = ('__weakref__', 'x') + register_freezable(S) + obj = S() + set_freezable(obj, FREEZABLE_NO) + # No __freezable__ attribute should be set. + self.assertFalse(hasattr(obj, '__freezable__')) + # But the status should still be queryable during freeze. + with self.assertRaises(TypeError): + freeze(obj) + + def test_manual_freezable_attr_respected(self): + # Manually setting __freezable__ on an object should be respected. + C = make_freezable_class() + obj = C() + obj.__freezable__ = FREEZABLE_NO + with self.assertRaises(TypeError): + freeze(obj) + + +class TestSetFreezableLifetime(unittest.TestCase): + """Ensure set_freezable does not keep objects alive longer than expected.""" + + def test_attr_path_no_prevent_gc(self): + # Objects with __dict__ use attribute storage. + # set_freezable should not prevent collection. + C = make_freezable_class() + obj = C() + ref = weakref.ref(obj) + set_freezable(obj, FREEZABLE_YES) + del obj + gc.collect() + self.assertIsNone(ref()) + + def test_ob_flags_path_no_prevent_gc(self): + # Objects with __slots__ use ob_flags storage. + # set_freezable should not prevent collection. + class S: + __slots__ = ('__weakref__', 'x') + register_freezable(S) + obj = S() + ref = weakref.ref(obj) + set_freezable(obj, FREEZABLE_NO) + del obj + gc.collect() + self.assertIsNone(ref()) + + def test_each_status_no_prevent_gc(self): + # Verify for every status value that the object is collected. + C = make_freezable_class() + for status in (FREEZABLE_YES, FREEZABLE_NO, + FREEZABLE_EXPLICIT): + obj = C() + ref = weakref.ref(obj) + set_freezable(obj, status) + del obj + gc.collect() + self.assertIsNone(ref(), + f"Object with status {status} was kept alive") + + def test_overwritten_status_no_prevent_gc(self): + # Override status multiple times, then delete. + C = make_freezable_class() + obj = C() + ref = weakref.ref(obj) + set_freezable(obj, FREEZABLE_NO) + set_freezable(obj, FREEZABLE_YES) + set_freezable(obj, FREEZABLE_EXPLICIT) + del obj + gc.collect() + self.assertIsNone(ref()) + + def test_cyclic_reference_with_set_freezable(self): + # Objects in a reference cycle with set_freezable should + # still be collected by the cycle detector. + C = make_freezable_class() + a = C() + b = C() + a.other = b + b.other = a + ref_a = weakref.ref(a) + ref_b = weakref.ref(b) + set_freezable(a, FREEZABLE_NO) + set_freezable(b, FREEZABLE_NO) + del a, b + gc.collect() + self.assertIsNone(ref_a()) + self.assertIsNone(ref_b()) + + +class TestConstants(unittest.TestCase): + """Verify the constant values are exposed correctly.""" + + def test_constant_values(self): + self.assertEqual(FREEZABLE_YES, 0) + self.assertEqual(FREEZABLE_NO, 1) + self.assertEqual(FREEZABLE_EXPLICIT, 2) + self.assertEqual(FREEZABLE_PROXY, 3) + + +if __name__ == '__main__': + unittest.main() diff --git a/Modules/_immutablemodule.c b/Modules/_immutablemodule.c index fc770262843745..e5fafeb731d9bb 100644 --- a/Modules/_immutablemodule.c +++ b/Modules/_immutablemodule.c @@ -9,6 +9,7 @@ #include "Python.h" #include #include "pycore_object.h" +#include "pycore_immutability.h" /*[clinic input] module _immutable @@ -121,6 +122,31 @@ _immutable_isfrozen(PyObject *module, PyObject *obj) Py_RETURN_FALSE; } +/*[clinic input] +_immutable.set_freezable + obj: object + status: int + / + +Set the freezable status of an object. + +Status values: + FREEZABLE_YES (0): always freezable + FREEZABLE_NO (1): never freezable + FREEZABLE_EXPLICIT (2): freezable only when freeze() is called directly on it + FREEZABLE_PROXY (3): reserved for future use +[clinic start generated code]*/ + +static PyObject * +_immutable_set_freezable_impl(PyObject *module, PyObject *obj, int status) +/*[clinic end generated code: output=73cad0b4df9a46f9 input=63df024c940ba301]*/ +{ + if (_PyImmutability_SetFreezable(obj, status) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + static PyType_Slot not_freezable_error_slots[] = { {0, NULL}, }; @@ -149,6 +175,7 @@ static struct PyMethodDef immutable_methods[] = { _IMMUTABLE_REGISTER_FREEZABLE_METHODDEF _IMMUTABLE_FREEZE_METHODDEF _IMMUTABLE_ISFROZEN_METHODDEF + _IMMUTABLE_SET_FREEZABLE_METHODDEF { NULL, NULL } }; @@ -186,6 +213,23 @@ immutable_exec(PyObject *module) { return -1; } + if (PyModule_AddIntConstant(module, "FREEZABLE_YES", + _Py_FREEZABLE_YES) != 0) { + return -1; + } + if (PyModule_AddIntConstant(module, "FREEZABLE_NO", + _Py_FREEZABLE_NO) != 0) { + return -1; + } + if (PyModule_AddIntConstant(module, "FREEZABLE_EXPLICIT", + _Py_FREEZABLE_EXPLICIT) != 0) { + return -1; + } + if (PyModule_AddIntConstant(module, "FREEZABLE_PROXY", + _Py_FREEZABLE_PROXY) != 0) { + return -1; + } + return 0; } diff --git a/Modules/clinic/_immutablemodule.c.h b/Modules/clinic/_immutablemodule.c.h index d37bbb0f54301b..9b8371d8422943 100644 --- a/Modules/clinic/_immutablemodule.c.h +++ b/Modules/clinic/_immutablemodule.c.h @@ -2,6 +2,8 @@ preserve [clinic start generated code]*/ +#include "pycore_modsupport.h" // _PyArg_CheckPositional() + PyDoc_STRVAR(_immutable_register_freezable__doc__, "register_freezable($module, obj, /)\n" "--\n" @@ -31,4 +33,43 @@ PyDoc_STRVAR(_immutable_isfrozen__doc__, #define _IMMUTABLE_ISFROZEN_METHODDEF \ {"isfrozen", (PyCFunction)_immutable_isfrozen, METH_O, _immutable_isfrozen__doc__}, -/*[clinic end generated code: output=b08aeb9c23cc30c8 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(_immutable_set_freezable__doc__, +"set_freezable($module, obj, status, /)\n" +"--\n" +"\n" +"Set the freezable status of an object.\n" +"\n" +"Status values:\n" +" FREEZABLE_YES (0): always freezable\n" +" FREEZABLE_NO (1): never freezable\n" +" FREEZABLE_EXPLICIT (2): freezable only when freeze() is called directly on it\n" +" FREEZABLE_PROXY (3): reserved for future use"); + +#define _IMMUTABLE_SET_FREEZABLE_METHODDEF \ + {"set_freezable", _PyCFunction_CAST(_immutable_set_freezable), METH_FASTCALL, _immutable_set_freezable__doc__}, + +static PyObject * +_immutable_set_freezable_impl(PyObject *module, PyObject *obj, int status); + +static PyObject * +_immutable_set_freezable(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *obj; + int status; + + if (!_PyArg_CheckPositional("set_freezable", nargs, 2, 2)) { + goto exit; + } + obj = args[0]; + status = PyLong_AsInt(args[1]); + if (status == -1 && PyErr_Occurred()) { + goto exit; + } + return_value = _immutable_set_freezable_impl(module, obj, status); + +exit: + return return_value; +} +/*[clinic end generated code: output=6ccbbe8cca58c3bb input=a9049054013a1b77]*/ diff --git a/Python/immutability.c b/Python/immutability.c index 0dc1491f4f5e17..6fd06a312ab0be 100644 --- a/Python/immutability.c +++ b/Python/immutability.c @@ -123,28 +123,6 @@ type_weakref(struct _Py_immutability_state *state, PyObject *obj) static int init_state(struct _Py_immutability_state *state) { - // TODO(Immutable): Should we have the following code given the updates to the PEP? - // PyObject* frozen_importlib = NULL; - - // frozen_importlib = PyImport_ImportModule("_frozen_importlib"); - // if(frozen_importlib == NULL){ - // return -1; - // } - - // state->module_locks = PyObject_GetAttrString(frozen_importlib, "_module_locks"); - // if(state->module_locks == NULL){ - // Py_DECREF(frozen_importlib); - // return -1; - // } - - // state->blocking_on = PyObject_GetAttrString(frozen_importlib, "_blocking_on"); - // if(state->blocking_on == NULL){ - // Py_DECREF(frozen_importlib); - // return -1; - // } - - // Py_DECREF(frozen_importlib); - state->freezable_types = PySet_New(NULL); if(state->freezable_types == NULL){ return -1; @@ -194,21 +172,6 @@ int init_state(struct _Py_immutability_state *state) return 0; } -// This is separate to the previous init as it depends on the traceback -// module being available, and can cause a circular import if it is -// called during register freezable. -#ifdef Py_DEBUG -static -void init_traceback_state(struct _Py_immutability_state *state) -{ - PyObject *traceback_module = PyImport_ImportModule("traceback"); - if (traceback_module != NULL) { - state->traceback_func = PyObject_GetAttrString(traceback_module, "format_stack"); - Py_DECREF(traceback_module); - } -} -#endif - static struct _Py_immutability_state* get_immutable_state(void) { PyInterpreterState* interp = PyInterpreterState_Get(); @@ -372,6 +335,9 @@ struct FreezeState { // It is also used to track nodes in GIL_DISABLED builds. _Py_hashtable_t *visited; + // The object that freeze() was called directly on. + PyObject *root; + #ifdef Py_DEBUG // For debugging, track the stack trace of the freeze operation. PyObject* freeze_location; @@ -1243,10 +1209,30 @@ is_explicitly_freezable(struct _Py_immutability_state *state, PyObject *obj) } -static int check_freezable(struct _Py_immutability_state *state, PyObject* obj) +static int check_freezable(struct _Py_immutability_state *state, PyObject* obj, + struct FreezeState *freeze_state) { debug_obj("check_freezable %s (%p)\n", obj); + // Check per-object freezable status set via set_freezable(). + int obj_status = _PyImmutability_GetFreezable(obj); + if (obj_status >= 0) { + switch (obj_status) { + case _Py_FREEZABLE_YES: + return 0; + case _Py_FREEZABLE_NO: + goto error; + case _Py_FREEZABLE_EXPLICIT: + if (freeze_state != NULL && obj == freeze_state->root) { + return 0; + } + goto error; + case _Py_FREEZABLE_PROXY: + // Reserved for future use — fall through to existing checks. + break; + } + } + // Check is object is subclass of NotFreezable // TODO: Would be nice for this to be faster. if (PyObject_IsInstance(obj, (PyObject *)&_PyNotFreezable_Type) == 1){ @@ -1303,6 +1289,123 @@ int _PyImmutability_RegisterFreezable(PyTypeObject* tp) return result; } + +int _PyImmutability_SetFreezable(PyObject *obj, int status) +{ + if (status < _Py_FREEZABLE_YES || status > _Py_FREEZABLE_PROXY) { + PyErr_Format(PyExc_ValueError, + "Invalid freezable status: %d", status); + return -1; + } + + if (status == _Py_FREEZABLE_PROXY && !PyModule_Check(obj)) { + PyErr_SetString(PyExc_TypeError, + "FREEZABLE_PROXY can only be set on module objects"); + return -1; + } + + // Try setting __freezable__ attribute on the object. + PyObject *value = PyLong_FromLong(status); + if (value == NULL) { + return -1; + } + + int rc = PyObject_SetAttr(obj, &_Py_ID(__freezable__), value); + Py_DECREF(value); + if (rc == 0) { + return 0; + } + // If the object doesn't support attribute setting, fall back + // to ob_flags (64-bit only). + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) { + return -1; + } + PyErr_Clear(); + +#if SIZEOF_VOID_P > 4 + // Store the freezable status in ob_flags bits 5-7. + uint16_t flags = obj->ob_flags; + flags &= ~(_Py_FREEZABLE_SET_FLAG | _Py_FREEZABLE_STATUS_MASK); + flags |= _Py_FREEZABLE_SET_FLAG | + ((status << _Py_FREEZABLE_STATUS_SHIFT) & _Py_FREEZABLE_STATUS_MASK); + obj->ob_flags = flags; + return 0; +#else + // 32-bit builds do not have ob_flags for freezable status. + assert(0 && "set_freezable ob_flags fallback not supported on 32-bit"); + PyErr_SetString(PyExc_TypeError, + "Cannot set freezable status: object has no attribute " + "support and ob_flags fallback is not available on 32-bit"); + return -1; +#endif +} + + +// Read the freezable status from ob_flags (64-bit only). +// Returns the status if set, or -1 if not set. +static inline int +_get_freezable_from_flags(PyObject *obj) +{ +#if SIZEOF_VOID_P > 4 + uint16_t flags = obj->ob_flags; + if (flags & _Py_FREEZABLE_SET_FLAG) { + return (flags & _Py_FREEZABLE_STATUS_MASK) >> _Py_FREEZABLE_STATUS_SHIFT; + } +#endif + return -1; +} + +int _PyImmutability_GetFreezable(PyObject *obj) +{ + // First, check for a __freezable__ attribute on the object. + PyObject *attr = NULL; + int found = PyObject_GetOptionalAttr(obj, &_Py_ID(__freezable__), &attr); + if (found == 1) { + int status = (int)PyLong_AsLong(attr); + Py_DECREF(attr); + if (status == -1 && PyErr_Occurred()) { + return -2; + } + return status; + } + if (found == -1) { + return -2; + } + + // Check ob_flags for the object. + int flags_status = _get_freezable_from_flags(obj); + if (flags_status >= 0) { + return flags_status; + } + + // Not found for the object itself — check the object's type. + PyObject *type_obj = (PyObject *)Py_TYPE(obj); + PyObject *type_attr = NULL; + int type_found = PyObject_GetOptionalAttr(type_obj, + &_Py_ID(__freezable__), + &type_attr); + if (type_found == 1) { + int status = (int)PyLong_AsLong(type_attr); + Py_DECREF(type_attr); + if (status == -1 && PyErr_Occurred()) { + return -2; + } + return status; + } + if (type_found == -1) { + return -2; + } + + // Check ob_flags for the type. + flags_status = _get_freezable_from_flags(type_obj); + if (flags_status >= 0) { + return flags_status; + } + + return -1; // Not found. +} + + static int is_shallow_immutable_type(struct _Py_immutability_state *state, PyTypeObject *tp) { @@ -1593,19 +1696,6 @@ static int traverse_freeze(PyObject* obj, struct FreezeState* freeze_state) } PyObject *attr = NULL; - if (PyObject_GetOptionalAttr(obj, &_Py_ID(__freezable__), &attr) == 1 - && Py_IsFalse(attr)) - { - PyErr_Format( - PyExc_TypeError, - "A object of type %s is marked as unfreezable", - Py_TYPE(obj)->tp_name); - Py_XDECREF(attr); - return -1; - } - Py_XDECREF(attr); - - attr = NULL; if (PyObject_GetOptionalAttr(obj, &_Py_ID(__pre_freeze__), &attr) == 1) { PyErr_SetString(PyExc_TypeError, "Pre-freeze hocks are currently WIP"); @@ -1651,6 +1741,58 @@ static int traverse_freeze(PyObject* obj, struct FreezeState* freeze_state) return -1; } +// Mark importlib's mutable state as not freezable. +// Separated from init_state because _frozen_importlib is not +// available during early interpreter startup. +static void +late_init(struct _Py_immutability_state *state) +{ + state->late_init_done = true; + + PyObject *frozen_importlib = PyImport_ImportModule("_frozen_importlib"); + if (frozen_importlib == NULL) { + PyErr_Clear(); + return; + } + + PyObject *module_locks = PyObject_GetAttrString(frozen_importlib, + "_module_locks"); + if (module_locks != NULL) { + if (_PyImmutability_SetFreezable(module_locks, + _Py_FREEZABLE_NO) < 0) { + PyErr_Clear(); + } + Py_DECREF(module_locks); + } else { + PyErr_Clear(); + } + + PyObject *blocking_on = PyObject_GetAttrString(frozen_importlib, + "_blocking_on"); + if (blocking_on != NULL) { + if (_PyImmutability_SetFreezable(blocking_on, + _Py_FREEZABLE_NO) < 0) { + PyErr_Clear(); + } + Py_DECREF(blocking_on); + } else { + PyErr_Clear(); + } + + Py_DECREF(frozen_importlib); + +#ifdef Py_DEBUG + PyObject *traceback_module = PyImport_ImportModule("traceback"); + if (traceback_module != NULL) { + state->traceback_func = PyObject_GetAttrString(traceback_module, + "format_stack"); + Py_DECREF(traceback_module); + } else { + PyErr_Clear(); + } +#endif +} + // Main entry point to freeze an object and everything it can reach. int _PyImmutability_Freeze(PyObject* obj) { @@ -1663,18 +1805,20 @@ int _PyImmutability_Freeze(PyObject* obj) struct FreezeState freeze_state; // Initialize the freeze state SUCCEEDS(init_freeze_state(&freeze_state)); + freeze_state.root = obj; struct _Py_immutability_state* imm_state = get_immutable_state(); if(imm_state == NULL){ goto error; } + // Late-init: mark importlib mutable state as not freezable. + if (!imm_state->late_init_done) { + late_init(imm_state); + } + #ifdef Py_DEBUG // In debug mode, we can set a freeze location for debugging purposes. // Get a traceback object to use as the freeze location. - if (imm_state->traceback_func == NULL) { - init_traceback_state(imm_state); - } - if (imm_state->traceback_func != NULL) { PyObject *stack = PyObject_CallFunctionObjArgs(imm_state->traceback_func, NULL); if (stack != NULL) { @@ -1724,7 +1868,7 @@ int _PyImmutability_Freeze(PyObject* obj) } // New object, check if freezable - SUCCEEDS(check_freezable(imm_state, item)); + SUCCEEDS(check_freezable(imm_state, item, &freeze_state)); // Add to visited before putting in internal datastructures, so don't have // to account of internal RC manipulations. diff --git a/Python/pystate.c b/Python/pystate.c index 681e494db774be..d13cf4ba96dee6 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -792,8 +792,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) assert(interp->imports.importlib == NULL); assert(interp->imports.import_func == NULL); - Py_CLEAR(interp->immutability.module_locks); - Py_CLEAR(interp->immutability.blocking_on); Py_CLEAR(interp->immutability.freezable_types); Py_CLEAR(interp->immutability.destroy_cb); if (interp->immutability.warned_types != NULL) {