Skip to content

Commit d1cccc3

Browse files
Make embed tests compatible with Python 3.12+ behavior changes
Three CPython behavior changes surfaced as failures/crashes once the overload-resolution crash was fixed, all on 3.12+: - ClassManagerTests.BindsCorrectOverloadForClassName crashed the host with "Python memory allocator called without holding the GIL". TestClass2's Get(PyObject o) re-enters Python via ToPython() while MethodBinder has released the GIL (allow_threads) around the managed call. A managed callback that re-enters Python must re-acquire the GIL; tolerated on <=3.11, fatal on 3.12+. Wrap the body in using (Py.GIL()). - TestGetsPythonCodeInfoInStackTrace[ForNestedInterop]: 3.12+ adds caret indicator lines (e.g. "~~~^^^") under source lines in tracebacks, shifting the positional assertions. Drop caret-only lines before asserting (no-op on <=3.11). - Codecs.ExceptionDecodedNoInstance: 3.12 eagerly normalizes exceptions, so the error indicator always carries an instance ("value"); the instanceless scenario this decoder targets can no longer be produced. Guard the test to <3.12. Verified: full embed suite green on 3.11 (910/910) with these changes; the three previously-failing tests pass on 3.14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bfdfebb commit d1cccc3

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎src/embed_tests/ClassManagerTests.cs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,14 @@ private class TestClass2 : TestClass1
965965
{
966966
public PyObject Get(PyObject o)
967967
{
968-
return "PyObject Get(PyObject o)".ToPython();
968+
// This managed method is invoked by pythonnet with the GIL released
969+
// (MethodBinder uses allow_threads around managed calls). Re-entering
970+
// Python here - creating a str via ToPython() - requires re-acquiring
971+
// the GIL; on CPython 3.12+ allocating without the GIL is fatal.
972+
using (Py.GIL())
973+
{
974+
return "PyObject Get(PyObject o)".ToPython();
975+
}
969976
}
970977

971978
public dynamic Get(Type t)

‎src/embed_tests/Codecs.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,14 @@ from datetime import datetime
361361
[Test]
362362
public void ExceptionDecodedNoInstance()
363363
{
364+
if (Runtime.PyVersion >= new Version(3, 12))
365+
{
366+
// Python 3.12+ eagerly normalizes the error indicator, so an exception
367+
// always reaches the decoder with an instance ("value"). The instanceless
368+
// error scenario this decoder targets can no longer be produced by CPython.
369+
Assert.Ignore("Instanceless exceptions are not produced on Python 3.12+ (eager normalization).");
370+
}
371+
364372
PyObjectConversions.RegisterDecoder(new InstancelessExceptionDecoder());
365373
using var scope = Py.CreateScope();
366374
var error = Assert.Throws<ValueErrorWrapper>(() => PythonEngine.Exec(

‎src/embed_tests/TestPythonException.cs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,12 @@ def CallThrow(self):
235235
{
236236
Assert.AreEqual("Test Exception Message", ex.InnerException.Message);
237237

238-
var pythonTracebackLines = ex.PythonTraceback.TrimEnd('\n').Split('\n').Select(x => x.Trim()).ToList();
238+
var pythonTracebackLines = ex.PythonTraceback.TrimEnd('\n').Split('\n').Select(x => x.Trim())
239+
// Python 3.12+ adds caret indicator lines (e.g. "~~~^^^") under the offending
240+
// source code in tracebacks. Drop those so positional assertions below stay
241+
// version-agnostic (no-op on <=3.11, which doesn't emit them).
242+
.Where(x => !(x.Length > 0 && x.All(c => c == '~' || c == '^')))
243+
.ToList();
239244
Assert.AreEqual(5, pythonTracebackLines.Count);
240245

241246
Assert.AreEqual("File \"none\", line 9, in CallThrow", pythonTracebackLines[0]);
@@ -298,7 +303,12 @@ def CallThrow():
298303
{
299304
Assert.AreEqual("Test Exception Message", ex.InnerException.Message);
300305

301-
var pythonTracebackLines = ex.PythonTraceback.TrimEnd('\n').Split('\n').Select(x => x.Trim()).ToList();
306+
var pythonTracebackLines = ex.PythonTraceback.TrimEnd('\n').Split('\n').Select(x => x.Trim())
307+
// Python 3.12+ adds caret indicator lines (e.g. "~~~^^^") under the offending
308+
// source code in tracebacks. Drop those so positional assertions below stay
309+
// version-agnostic (no-op on <=3.11, which doesn't emit them).
310+
.Where(x => !(x.Length > 0 && x.All(c => c == '~' || c == '^')))
311+
.ToList();
302312
Assert.AreEqual(4, pythonTracebackLines.Count);
303313

304314
Assert.IsTrue(new[]

0 commit comments

Comments
 (0)