Visitar URL original
Fix shutdown regression with BinaryFormatter · pythonnet/pythonnet@83e4b0d · GitHub
Skip to content

Commit 83e4b0d

Browse files
committed
Fix shutdown regression with BinaryFormatter
The `ConcurrentDictionary` fields that were introduced to support free-threaded Python builds are not serializable and thus need to be rebuilt when the runtime data is unstashed.
1 parent deca529 commit 83e4b0d

3 files changed

Lines changed: 79 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1313

1414
### Fixed
1515

16+
- Fix shutdown serialization of CLR namespace modules on .NET 8 while preserving
17+
namespace name tracking across runtime restarts.
1618

1719
## 3.2.0 - 2026-09-29
1820

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using NUnit.Framework;
4+
using Python.Runtime;
5+
6+
namespace Python.EmbeddingTest.StateSerialization;
7+
8+
[NonParallelizable]
9+
public class ModuleSerialization
10+
{
11+
[TestCase(false)]
12+
[TestCase(true)]
13+
public void NamespaceModuleRoundtrip(bool loadNames)
14+
{
15+
var formatter = RuntimeData.CreateFormatter();
16+
if (formatter is NoopFormatter)
17+
{
18+
Assert.Inconclusive("NoopFormatter in use, cannot perform serialization test.");
19+
}
20+
21+
using var gil = Py.GIL();
22+
using var pyModule = ModuleObject.Create("System").MoveToPyObject();
23+
var module = (ModuleObject)ManagedType.GetManagedObject(pyModule)!;
24+
using (var int32 = module.GetAttribute("Int32", true))
25+
{
26+
Assert.That(int32.IsNull(), Is.False);
27+
}
28+
29+
string[] names = null;
30+
if (loadNames)
31+
{
32+
using var all = pyModule.GetAttr("__all__");
33+
names = all.As<string[]>();
34+
Assert.That(names, Does.Contain("Int32"));
35+
}
36+
37+
for (int i = 0; i < 2; i++)
38+
{
39+
var context = module.Save(pyModule);
40+
using var buffer = new MemoryStream();
41+
formatter.Serialize(buffer, new object[] { module, context });
42+
buffer.Position = 0;
43+
var restored = (object[])formatter.Deserialize(buffer);
44+
ExtensionType.tp_clear(pyModule);
45+
module = (ModuleObject)restored[0];
46+
module.Load(pyModule, (Dictionary<string, object>)restored[1]);
47+
48+
using (var int32 = module.GetAttribute("Int32", true))
49+
{
50+
Assert.That(int32.IsNull(), Is.False);
51+
}
52+
using (var collections = module.GetAttribute("Collections", true))
53+
{
54+
Assert.That(collections.IsNull(), Is.False);
55+
}
56+
57+
using var all = pyModule.GetAttr("__all__");
58+
var restoredNames = all.As<string[]>();
59+
Assert.That(restoredNames, Does.Contain("Int32"));
60+
Assert.That(restoredNames, Is.Unique);
61+
if (names is not null)
62+
{
63+
Assert.That(restoredNames, Is.EquivalentTo(names));
64+
}
65+
names = restoredNames;
66+
}
67+
}
68+
}

‎src/runtime/Types/ModuleObject.cs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ namespace Python.Runtime
1414
[Serializable]
1515
internal class ModuleObject : ExtensionType
1616
{
17-
private readonly ConcurrentDictionary<string, PyObject> cache = new();
17+
[NonSerialized]
18+
private ConcurrentDictionary<string, PyObject> cache = new();
1819

1920
internal string moduleName;
2021
internal PyDict dict;
2122
protected string _namespace;
2223
private readonly PyList __all__ = new ();
23-
private readonly ConcurrentDictionary<string, byte> allNames = new();
24+
[NonSerialized]
25+
private ConcurrentDictionary<string, byte> allNames = new();
2426

2527
// Attributes to be set on the module according to PEP302 and 451
2628
// by the import machinery.
@@ -378,7 +380,8 @@ public static int tp_traverse(BorrowedReference ob, IntPtr visit, IntPtr arg)
378380

379381
protected override Dictionary<string, object?>? OnSave(BorrowedReference ob)
380382
{
381-
var context = base.OnSave(ob);
383+
var context = base.OnSave(ob) ?? new();
384+
context[nameof(allNames)] = allNames.Keys.ToArray();
382385
System.Diagnostics.Debug.Assert(dict == GetObjectDict(ob));
383386
// destroy the cache(s)
384387
foreach (var pair in cache)
@@ -402,6 +405,9 @@ public static int tp_traverse(BorrowedReference ob, IntPtr visit, IntPtr arg)
402405

403406
protected override void OnLoad(BorrowedReference ob, Dictionary<string, object?>? context)
404407
{
408+
cache = new();
409+
allNames = new(((string[])context![nameof(allNames)]!)
410+
.Select(name => new KeyValuePair<string, byte>(name, 0)));
405411
base.OnLoad(ob, context);
406412
SetObjectDict(ob, new NewReference(dict).Steal());
407413
}

0 commit comments

Comments
 (0)