diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b4a7cf2..714052893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Fixed +- When `BinaryFormatter` is unavailable, skip publishing `sys.clr_data` during + `Shutdown` so the next `Initialize` does not deserialize an empty capsule (#2282) ## 3.2.0 - 2026-09-29 diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 8697e7aaf..eb23a755a 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -19,6 +19,11 @@ + + + + + $(DefineConstants);$(ConfiguredConstants) diff --git a/src/embed_tests/StateSerialization/ShutdownStash.cs b/src/embed_tests/StateSerialization/ShutdownStash.cs new file mode 100644 index 000000000..bd61a951b --- /dev/null +++ b/src/embed_tests/StateSerialization/ShutdownStash.cs @@ -0,0 +1,57 @@ +using System; + +using NUnit.Framework; + +using Python.Runtime; + +namespace Python.EmbeddingTest.StateSerialization; + +/// +/// Regression for https://github.com/pythonnet/pythonnet/issues/2282. +/// When BinaryFormatter cannot run, Shutdown must not throw and must not +/// leave a sys.clr_data capsule behind. +/// +public class ShutdownStash +{ + [Test] + public void ShutdownLeavesNoClrData() + { + if (!PythonEngine.IsInitialized) + { + PythonEngine.Initialize(); + } + + Assert.That(RuntimeData.CreateFormatter(), Is.InstanceOf()); + + using (Py.GIL()) + { + using var wrapped = new UriBuilder().ToPython(); + Assert.That(RuntimeData.HasStashData(), Is.False); + } + + try + { + PythonEngine.Shutdown(); + + using (Py.GIL()) + { + Assert.That(RuntimeData.HasStashData(), Is.False); + } + } + finally + { + // Shutdown leaves the interpreter alive. A second Shutdown in this + // process then deallocates types from the previous runtime and + // trips MetaType.tp_dealloc. Finish the interpreter first so the + // suite, and GlobalTestsSetup, start from a new one. + if (Runtime.Runtime.Py_IsInitialized() != 0) + { + Runtime.Runtime.Py_Finalize(); + } + if (!PythonEngine.IsInitialized) + { + PythonEngine.Initialize(); + } + } + } +} diff --git a/src/runtime/StateSerialization/RuntimeData.cs b/src/runtime/StateSerialization/RuntimeData.cs index c3e5d2287..e6e0a8f9e 100644 --- a/src/runtime/StateSerialization/RuntimeData.cs +++ b/src/runtime/StateSerialization/RuntimeData.cs @@ -84,6 +84,19 @@ static void ClearCLRData () internal static void Stash() { + IFormatter formatter = CreateFormatter(); + // NoopFormatter.Serialize writes nothing. An empty sys.clr_data capsule + // makes the next Initialize call Deserialize and throw (#2282). + if (formatter is NoopFormatter) + { + if (HasStashData()) + { + ClearCLRData(); + ClearStash(); + } + return; + } + var runtimeStorage = new PythonNetState { Metatype = MetaType.SaveRuntimeData(), @@ -93,7 +106,6 @@ internal static void Stash() SharedObjects = SaveRuntimeDataObjects(), }; - IFormatter formatter = CreateFormatter(); var ms = new MemoryStream(); formatter.Serialize(ms, runtimeStorage);