Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.8", "3.9", "3.10", "3.11"]
python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- name: Checkout code
Expand All @@ -49,7 +49,16 @@ jobs:
echo PYTHONHOME=$(python -c 'import sys; print(sys.prefix)') >> $GITHUB_ENV

- name: Embedding tests
run: dotnet test --runtime any-x64 --logger "console;verbosity=detailed" src/embed_tests/
run: dotnet test --runtime any-x64 --logger "console;verbosity=detailed" src/embed_tests/ --filter "FullyQualifiedName!~SetPythonPath"

# SetPythonPath exercises PythonEngine.PythonPath, which uses the deprecated Py_SetPath
# to pin a fixed module search path. CPython 3.13+ keeps that path config in _PyRuntime
# across Py_Finalize and provides no way to reset it back to auto-computation without the
# PyConfig API, so once this test runs, every later interpreter re-initialization in the
# same process is forced onto the pinned path and eventually fails to import encodings.
# Run it in its own process so it cannot pollute the rest of the suite.
- name: Embedding tests (SetPythonPath, isolated process)
run: dotnet test --runtime any-x64 --logger "console;verbosity=detailed" src/embed_tests/ --filter "FullyQualifiedName~SetPythonPath"

- name: Python Tests (.NET Core)
run: pytest --runtime netcore tests
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies = [
"clr_loader>=0.2.2,<0.3.0"
]

requires-python = ">=3.7, <3.12"
requires-python = ">=3.7, <3.15"

classifiers = [
"Development Status :: 5 - Production/Stable",
Expand Down
9 changes: 8 additions & 1 deletion src/embed_tests/ClassManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,14 @@ private class TestClass2 : TestClass1
{
public PyObject Get(PyObject o)
{
return "PyObject Get(PyObject o)".ToPython();
// This managed method is invoked by pythonnet with the GIL released
// (MethodBinder uses allow_threads around managed calls). Re-entering
// Python here - creating a str via ToPython() - requires re-acquiring
// the GIL; on CPython 3.12+ allocating without the GIL is fatal.
using (Py.GIL())
{
return "PyObject Get(PyObject o)".ToPython();
}
}

public dynamic Get(Type t)
Expand Down
8 changes: 8 additions & 0 deletions src/embed_tests/Codecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,14 @@ from datetime import datetime
[Test]
public void ExceptionDecodedNoInstance()
{
if (Runtime.PyVersion >= new Version(3, 12))
{
// Python 3.12+ eagerly normalizes the error indicator, so an exception
// always reaches the decoder with an instance ("value"). The instanceless
// error scenario this decoder targets can no longer be produced by CPython.
Assert.Ignore("Instanceless exceptions are not produced on Python 3.12+ (eager normalization).");
}

PyObjectConversions.RegisterDecoder(new InstancelessExceptionDecoder());
using var scope = Py.CreateScope();
var error = Assert.Throws<ValueErrorWrapper>(() => PythonEngine.Exec(
Expand Down
Loading
Loading