diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3937d85e0..54024eaac 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,7 @@ name: Documentation -on: [push, pull_request] +on: + workflow_dispatch: jobs: build: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5e291169..83deb361b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,7 @@ name: Main on: - push: - branches: - - master - pull_request: + workflow_dispatch: jobs: build-test: diff --git a/.github/workflows/windows-build-test-3.14.yml b/.github/workflows/windows-build-test-3.14.yml new file mode 100644 index 000000000..119de2902 --- /dev/null +++ b/.github/workflows/windows-build-test-3.14.yml @@ -0,0 +1,57 @@ +name: Windows Build and Test (3.14) + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +jobs: + build-test: + name: Build and Test + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Set up Python 3.14 + uses: astral-sh/setup-uv@v7 + with: + architecture: x64 + python-version: '3.14' + cache-python: true + activate-environment: true + enable-cache: true + + - name: Synchronize the virtual environment + run: uv sync --managed-python + + - name: Show pyvenv.cfg + run: cat .venv/pyvenv.cfg + + - name: Embedding tests (Mono/.NET Framework) + run: dotnet test --runtime any-x64 --framework net472 --logger "console;verbosity=detailed" src/embed_tests/ + if: always() + env: + MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 + + - name: Embedding tests (.NET Core) + run: dotnet test --runtime any-x64 --framework net8.0 --logger "console;verbosity=detailed" src/embed_tests/ + if: always() + + - name: Python Tests (.NET Core) + run: pytest --runtime coreclr + + - name: Python Tests (.NET Framework) + run: pytest --runtime netfx + + - name: Python tests run from .NET + run: dotnet test --runtime any-x64 src/python_tests_runner/ diff --git a/src/runtime/PythonEngine.cs b/src/runtime/PythonEngine.cs index 13855adef..8bc404991 100644 --- a/src/runtime/PythonEngine.cs +++ b/src/runtime/PythonEngine.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -143,6 +145,29 @@ public static string Version get { return Marshal.PtrToStringAnsi(Runtime.Py_GetVersion()); } } + internal static Version GetPythonVersion() + { + string? versionText = Version; + if (string.IsNullOrWhiteSpace(versionText)) + { + return new Version(0, 0); + } + + Match match = Regex.Match(versionText, @"^(\\d+)\\.(\\d+)(?:\\.(\\d+))?"); + if (!match.Success) + { + return new Version(0, 0); + } + + int major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + int minor = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + int patch = match.Groups[3].Success + ? int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture) + : 0; + + return new Version(major, minor, patch); + } + public static string BuildInfo { get { return Marshal.PtrToStringAnsi(Runtime.Py_GetBuildInfo()); } diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs index dc4a4b0a9..b784cfeda 100644 --- a/src/runtime/Runtime.Delegates.cs +++ b/src/runtime/Runtime.Delegates.cs @@ -25,14 +25,13 @@ static Delegates() PyThreadState_Get = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_Get), GetUnmanagedDll(_PythonDll)); try { - // Up until Python 3.13, this function was private and named - // slightly differently. - PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName("_PyThreadState_UncheckedGet", GetUnmanagedDll(_PythonDll)); + PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_GetUnchecked), GetUnmanagedDll(_PythonDll)); } catch (MissingMethodException) { - - PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_GetUnchecked), GetUnmanagedDll(_PythonDll)); + // Up until Python 3.12, this function was private and named + // slightly differently. + PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName("_PyThreadState_UncheckedGet", GetUnmanagedDll(_PythonDll)); } try { diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index 399608733..b13447249 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -262,7 +262,7 @@ internal static void Shutdown() if (!HostedInPython && !ProcessIsTerminating) { // avoid saving dead objects - TryCollectingGarbage(runs: 3); + TryCollectingGarbage(runs: 3, pythonGC: !ShouldSkipPythonGcOnShutdown); RuntimeData.Stash(); } @@ -275,7 +275,8 @@ internal static void Shutdown() RemoveClrRootModule(); TryCollectingGarbage(MaxCollectRetriesOnShutdown, forceBreakLoops: true, - obj: true, derived: false, buffer: false); + obj: true, derived: false, buffer: false, + pythonGC: !ShouldSkipPythonGcOnShutdown); CLRObject.creationBlocked = true; NullGCHandles(ExtensionType.loadedExtensions); @@ -294,8 +295,12 @@ internal static void Shutdown() DisposeLazyObject(hexCallable); PyObjectConversions.Reset(); - PyGC_Collect(); - bool everythingSeemsCollected = TryCollectingGarbage(MaxCollectRetriesOnShutdown); + if (!ShouldSkipPythonGcOnShutdown) + { + PyGC_Collect(); + } + bool everythingSeemsCollected = TryCollectingGarbage(MaxCollectRetriesOnShutdown, + pythonGC: !ShouldSkipPythonGcOnShutdown); Debug.Assert(everythingSeemsCollected); Finalizer.Shutdown(); @@ -328,7 +333,8 @@ internal static void Shutdown() const int MaxCollectRetriesOnShutdown = 20; internal static int _collected; static bool TryCollectingGarbage(int runs, bool forceBreakLoops, - bool obj = true, bool derived = true, bool buffer = true) + bool obj = true, bool derived = true, bool buffer = true, + bool pythonGC = true) { if (runs <= 0) throw new ArgumentOutOfRangeException(nameof(runs)); @@ -340,7 +346,10 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops, { GC.Collect(); GC.WaitForPendingFinalizers(); - pyCollected += PyGC_Collect(); + if (pythonGC) + { + pyCollected += PyGC_Collect(); + } pyCollected += Finalizer.Instance.DisposeAll(disposeObj: obj, disposeDerived: derived, disposeBuffer: buffer); @@ -366,6 +375,20 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops, public static bool TryCollectingGarbage(int runs) => TryCollectingGarbage(runs, forceBreakLoops: false); + static bool ShouldSkipPythonGcOnShutdown + { + get + { + if (!IsWindows) + { + return false; + } + + Version version = PythonEngine.GetPythonVersion(); + return version >= new Version(3, 14); + } + } + static void DisposeLazyObject(Lazy pyObject) { if (pyObject.IsValueCreated)