diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bae02377f..146acadde 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,6 +52,8 @@ jobs: env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + # Apply to every Mono host, including pytest and the .NET Python test runner. + MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 steps: - name: Checkout code uses: actions/checkout@v7 @@ -84,8 +86,6 @@ jobs: - name: Embedding tests (Mono/.NET Framework) if: always() run: dotnet test --runtime any-${{ matrix.os.platform }} --framework net472 --logger "console;verbosity=detailed" src/embed_tests/ - env: - MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 - name: Embedding tests (.NET Core) if: always() @@ -97,7 +97,7 @@ jobs: - name: Python Tests (Mono) if: always() - run: pytest --runtime mono + run: uv run pytest --runtime mono -v -o faulthandler_timeout=60 - name: Python Tests (.NET Framework) if: ${{ matrix.os.category == 'windows' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 737472caa..561b8a86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. +## Unreleased + +### Added + +### Changed + +### Fixed + +### Removed + +## 3.2.1 - 2026-10-08 + +### Fixed + +- Fix shutdown serialization of CLR namespace modules on .NET 8 while preserving + namespace name tracking across runtime restarts (#2750) + ## 3.2.0 - 2026-09-29 ### Added diff --git a/doc/source/threading.rst b/doc/source/threading.rst index 02f7bc91a..4d25ac9f0 100644 --- a/doc/source/threading.rst +++ b/doc/source/threading.rst @@ -93,6 +93,20 @@ Forgetting this is the most common pythonnet threading bug. Symptoms range from immediate segfaults to subtle refcount corruption that crashes much later. +Mono thread suspension +---------------------- + +On Linux and macOS, Mono's cooperative/hybrid thread suspension can hang +when Python and managed threads interact. If affected, set +``MONO_THREADS_SUSPEND=preemptive`` before starting the process, as described +in `mono/mono#21466 `_. +CI uses this workaround for both embedding and Python-hosted tests. + +To run the Mono Python tests with per-test output and a Python thread +stack dump if a test takes longer than 60 seconds:: + + MONO_THREADS_SUSPEND=preemptive uv run pytest --runtime mono -v -o faulthandler_timeout=60 + Reference counting and finalizers --------------------------------- diff --git a/src/embed_tests/StateSerialization/ModuleSerialization.cs b/src/embed_tests/StateSerialization/ModuleSerialization.cs new file mode 100644 index 000000000..80e010a45 --- /dev/null +++ b/src/embed_tests/StateSerialization/ModuleSerialization.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest.StateSerialization; + +[NonParallelizable] +public class ModuleSerialization +{ + [TestCase(false)] + [TestCase(true)] + public void NamespaceModuleRoundtrip(bool loadNames) + { + var formatter = RuntimeData.CreateFormatter(); + if (formatter is NoopFormatter) + { + Assert.Inconclusive("NoopFormatter in use, cannot perform serialization test."); + } + + using var gil = Py.GIL(); + using var pyModule = ModuleObject.Create("System").MoveToPyObject(); + var module = (ModuleObject)ManagedType.GetManagedObject(pyModule)!; + using (var int32 = module.GetAttribute("Int32", true)) + { + Assert.That(int32.IsNull(), Is.False); + } + + string[] names = null; + if (loadNames) + { + using var all = pyModule.GetAttr("__all__"); + names = all.As(); + Assert.That(names, Does.Contain("Int32")); + } + + for (int i = 0; i < 2; i++) + { + var context = module.Save(pyModule); + using var buffer = new MemoryStream(); + formatter.Serialize(buffer, new object[] { module, context }); + buffer.Position = 0; + var restored = (object[])formatter.Deserialize(buffer); + ExtensionType.tp_clear(pyModule); + module = (ModuleObject)restored[0]; + module.Load(pyModule, (Dictionary)restored[1]); + + using (var int32 = module.GetAttribute("Int32", true)) + { + Assert.That(int32.IsNull(), Is.False); + } + using (var collections = module.GetAttribute("Collections", true)) + { + Assert.That(collections.IsNull(), Is.False); + } + + using var all = pyModule.GetAttr("__all__"); + var restoredNames = all.As(); + Assert.That(restoredNames, Does.Contain("Int32")); + Assert.That(restoredNames, Is.Unique); + if (names is not null) + { + Assert.That(restoredNames, Is.EquivalentTo(names)); + } + names = restoredNames; + } + } +} diff --git a/src/runtime/Types/ModuleObject.cs b/src/runtime/Types/ModuleObject.cs index c9508aeed..3e020e563 100644 --- a/src/runtime/Types/ModuleObject.cs +++ b/src/runtime/Types/ModuleObject.cs @@ -14,13 +14,15 @@ namespace Python.Runtime [Serializable] internal class ModuleObject : ExtensionType { - private readonly ConcurrentDictionary cache = new(); + [NonSerialized] + private ConcurrentDictionary cache = new(); internal string moduleName; internal PyDict dict; protected string _namespace; private readonly PyList __all__ = new (); - private readonly ConcurrentDictionary allNames = new(); + [NonSerialized] + private ConcurrentDictionary allNames = new(); // Attributes to be set on the module according to PEP302 and 451 // by the import machinery. @@ -378,7 +380,8 @@ public static int tp_traverse(BorrowedReference ob, IntPtr visit, IntPtr arg) protected override Dictionary? OnSave(BorrowedReference ob) { - var context = base.OnSave(ob); + var context = base.OnSave(ob) ?? new(); + context[nameof(allNames)] = allNames.Keys.ToArray(); System.Diagnostics.Debug.Assert(dict == GetObjectDict(ob)); // destroy the cache(s) foreach (var pair in cache) @@ -402,6 +405,9 @@ public static int tp_traverse(BorrowedReference ob, IntPtr visit, IntPtr arg) protected override void OnLoad(BorrowedReference ob, Dictionary? context) { + cache = new(); + allNames = new(((string[])context![nameof(allNames)]!) + .Select(name => new KeyValuePair(name, 0))); base.OnLoad(ob, context); SetObjectDict(ob, new NewReference(dict).Steal()); } diff --git a/uv.lock b/uv.lock index 0081894af..593345c81 100644 --- a/uv.lock +++ b/uv.lock @@ -926,9 +926,9 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] diff --git a/version.txt b/version.txt index 944880fa1..6d7d65910 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.2.0 +3.3.0-dev