From c05f5784f6d2c8bfbc16302b3fd2d8ae4ee39894 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 11 Oct 2023 09:32:09 +0200 Subject: [PATCH 01/77] Reset version to dev --- CHANGELOG.md | 11 ++++++++++- version.txt | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 411356775..fdab9bf64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. +## [Unreleased][] + +### Added + +### Changed + +### Fixed + + ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 ### Added @@ -833,7 +842,7 @@ This version improves performance on benchmarks significantly compared to 2.3. [semantic versioning]: http://semver.org/ -[unreleased]: ../../compare/v2.3.0...HEAD +[unreleased]: ../../compare/v3.0.1...HEAD [2.3.0]: ../../compare/v2.2.2...v2.3.0 diff --git a/version.txt b/version.txt index 75a22a26a..0f9d6b15d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.0.3 +3.1.0-dev From 8e8c3f3c921720076b6eb7ee217a14d115240d9c Mon Sep 17 00:00:00 2001 From: bmello4688 Date: Mon, 16 Oct 2023 06:47:26 -0400 Subject: [PATCH 02/77] Allow setting of the python module file (#2044) Allow the setting of the python module file in order to create a virtual package structure --------- Co-authored-by: Benedikt Reinartz --- src/embed_tests/Modules.cs | 70 ++++++++++++++++++++++++++--- src/runtime/PythonTypes/PyModule.cs | 13 +++++- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/embed_tests/Modules.cs b/src/embed_tests/Modules.cs index a88ab8552..6cab4dd07 100644 --- a/src/embed_tests/Modules.cs +++ b/src/embed_tests/Modules.cs @@ -51,7 +51,7 @@ public void TestEval() ps.Set("a", 1); var result = ps.Eval("a + 2"); Assert.AreEqual(3, result); - } + } } /// @@ -169,6 +169,62 @@ public void TestScopeClass() } } + /// + /// Create a class in the scope, the class can read variables in the scope. + /// Its methods can write the variables with the help of 'global' keyword. + /// + [Test] + public void TestCreateVirtualPackageStructure() + { + using (Py.GIL()) + { + using var _p1 = PyModule.FromString("test", ""); + // Sub-module + using var _p2 = PyModule.FromString("test.scope", + "class Class1():\n" + + " def __init__(self, value):\n" + + " self.value = value\n" + + " def call(self, arg):\n" + + " return self.value + bb + arg\n" + // use scope variables + " def update(self, arg):\n" + + " global bb\n" + + " bb = self.value + arg\n", // update scope variable + "test" + ); + + dynamic ps2 = Py.Import("test.scope"); + ps2.bb = 100; + + dynamic obj1 = ps2.Class1(20); + var result = obj1.call(10).As(); + Assert.AreEqual(130, result); + + obj1.update(10); + result = ps2.Get("bb"); + Assert.AreEqual(30, result); + } + } + + /// + /// Test setting the file attribute via a FromString parameter + /// + [Test] + public void TestCreateModuleWithFilename() + { + using var _gil = Py.GIL(); + + using var mod = PyModule.FromString("mod", ""); + using var modWithoutName = PyModule.FromString("mod_without_name", "", " "); + using var modNullName = PyModule.FromString("mod_null_name", "", null); + + using var modWithName = PyModule.FromString("mod_with_name", "", "some_filename"); + + Assert.AreEqual("none", mod.Get("__file__")); + Assert.AreEqual("none", modWithoutName.Get("__file__")); + Assert.AreEqual("none", modNullName.Get("__file__")); + Assert.AreEqual("some_filename", modWithName.Get("__file__")); + } + /// /// Import a python module into the session. /// Equivalent to the Python "import" statement. @@ -194,7 +250,7 @@ public void TestImportModule() } /// - /// Create a scope and import variables from a scope, + /// Create a scope and import variables from a scope, /// exec Python statements in the scope then discard it. /// [Test] @@ -218,7 +274,7 @@ public void TestImportScope() } /// - /// Create a scope and import variables from a scope, + /// Create a scope and import variables from a scope, /// exec Python statements in the scope then discard it. /// [Test] @@ -241,7 +297,7 @@ public void TestImportAllFromScope() } /// - /// Create a scope and import variables from a scope, + /// Create a scope and import variables from a scope, /// call the function imported. /// [Test] @@ -286,7 +342,7 @@ public void TestImportScopeFunction() public void TestVariables() { using (Py.GIL()) - { + { (ps.Variables() as dynamic)["ee"] = new PyInt(200); var a0 = ps.Get("ee"); Assert.AreEqual(200, a0); @@ -326,8 +382,8 @@ public void TestThread() _ps.res = 0; _ps.bb = 100; _ps.th_cnt = 0; - //add function to the scope - //can be call many times, more efficient than ast + //add function to the scope + //can be call many times, more efficient than ast ps.Exec( "import threading\n"+ "lock = threading.Lock()\n"+ diff --git a/src/runtime/PythonTypes/PyModule.cs b/src/runtime/PythonTypes/PyModule.cs index 4549678ed..243f77ecc 100644 --- a/src/runtime/PythonTypes/PyModule.cs +++ b/src/runtime/PythonTypes/PyModule.cs @@ -82,7 +82,18 @@ public PyModule Reload() public static PyModule FromString(string name, string code) { - using NewReference c = Runtime.Py_CompileString(code, "none", (int)RunFlagType.File); + return FromString(name, code, ""); + } + + public static PyModule FromString(string name, string code, string file) + { + // Force valid value + if (string.IsNullOrWhiteSpace(file)) + { + file = "none"; + } + + using NewReference c = Runtime.Py_CompileString(code, file, (int)RunFlagType.File); NewReference m = Runtime.PyImport_ExecCodeModule(name, c.BorrowOrThrow()); return new PyModule(m.StealOrThrow()); } From eef67db7ff800c550b509fd4aa0eeedd86647801 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 7 Nov 2023 22:27:54 +0900 Subject: [PATCH 03/77] To fix memory access exception when iteration breaks in the middle of the list before reaching end. --- .../CollectionWrappers/IterableWrapper.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/runtime/CollectionWrappers/IterableWrapper.cs b/src/runtime/CollectionWrappers/IterableWrapper.cs index d30e584d3..849e3997b 100644 --- a/src/runtime/CollectionWrappers/IterableWrapper.cs +++ b/src/runtime/CollectionWrappers/IterableWrapper.cs @@ -24,18 +24,22 @@ public IEnumerator GetEnumerator() { iterObject = PyIter.GetIter(pyObject); } - - using var _ = iterObject; - while (true) + try { - using var GIL = Py.GIL(); - - if (!iterObject.MoveNext()) + while (true) { - iterObject.Dispose(); - break; + using var _ = Py.GIL(); + if (!iterObject.MoveNext()) + { + break; + } + yield return iterObject.Current.As()!; } - yield return iterObject.Current.As()!; + } + finally + { + using var _ = Py.GIL(); + iterObject.Dispose(); } } } From 87fc365a5fad5c2a6ab43ca12abd140fe0085443 Mon Sep 17 00:00:00 2001 From: Gert Dreyer Date: Fri, 23 Feb 2024 09:27:01 +0200 Subject: [PATCH 04/77] Tests for operators on type CS type with codec --- src/embed_tests/TestOperator.cs | 230 ++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index a5713274a..1d66903ca 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -15,6 +15,7 @@ public class TestOperator public void SetUp() { PythonEngine.Initialize(); + OwnIntCodec.Setup(); } [OneTimeTearDown] @@ -23,6 +24,120 @@ public void Dispose() PythonEngine.Shutdown(); } + // Mock Integer class to test math ops on non-native dotnet types + public struct OwnInt + { + private int _value; + + public int Num => _value; + + public OwnInt() + { + _value = 0; + } + + public OwnInt(int value) + { + _value = value; + } + + public static OwnInt operator -(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value - p2._value); + } + + public static OwnInt operator +(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value + p2._value); + } + + public static OwnInt operator *(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value * p2._value); + } + + public static OwnInt operator /(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value / p2._value); + } + + public static OwnInt operator %(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value % p2._value); + } + + public static OwnInt operator ^(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value ^ p2._value); + } + + public static bool operator <(OwnInt p1, OwnInt p2) + { + return p1._value < p2._value; + } + + public static bool operator >(OwnInt p1, OwnInt p2) + { + return p1._value > p2._value; + } + + public static bool operator ==(OwnInt p1, OwnInt p2) + { + return p1._value == p2._value; + } + + public static bool operator !=(OwnInt p1, OwnInt p2) + { + return p1._value != p2._value; + } + + public static OwnInt operator |(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value | p2._value); + } + + public static OwnInt operator &(OwnInt p1, OwnInt p2) + { + return new OwnInt(p1._value & p2._value); + } + + public static bool operator <=(OwnInt p1, OwnInt p2) + { + return p1._value <= p2._value; + } + + public static bool operator >=(OwnInt p1, OwnInt p2) + { + return p1._value >= p2._value; + } + } + + // Codec for mock class above. + public class OwnIntCodec : IPyObjectDecoder + { + public static void Setup() + { + PyObjectConversions.RegisterDecoder(new OwnIntCodec()); + } + + public bool CanDecode(PyType objectType, Type targetType) + { + return objectType.Name == "int" && targetType == typeof(OwnInt); + } + + public bool TryDecode(PyObject pyObj, out T? value) + { + if (pyObj.PyType.Name != "int" || typeof(T) != typeof(OwnInt)) + { + value = default(T); + return false; + } + + value = (T)(object)new OwnInt(pyObj.As()); + return true; + } + } + public class OperableObject { public int Num { get; set; } @@ -524,6 +639,121 @@ public void ShiftOperatorOverloads() c = a >> b.Num assert c.Num == a.Num >> b.Num +"); + } + + [Test] + public void ReverseOperatorWithCodec() + { + string name = string.Format("{0}.{1}", + typeof(OwnInt).DeclaringType.Name, + typeof(OwnInt).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = 2 +b = cls(10) + +c = a + b +assert c.Num == a + b.Num + +c = a - b +assert c.Num == a - b.Num + +c = a * b +assert c.Num == a * b.Num + +c = a / b +assert c.Num == a // b.Num + +c = a % b +assert c.Num == a % b.Num + +c = a & b +assert c.Num == a & b.Num + +c = a | b +assert c.Num == a | b.Num + +c = a ^ b +assert c.Num == a ^ b.Num + +c = a == b +assert c == (a == b.Num) + +c = a != b +assert c == (a != b.Num) + +c = a <= b +assert c == (a <= b.Num) + +c = a >= b +assert c == (a >= b.Num) + +c = a < b +assert c == (a < b.Num) + +c = a > b +assert c == (a > b.Num) +"); + } + + [Test] + public void ForwardOperatorWithCodec() + { + string name = string.Format("{0}.{1}", + typeof(OwnInt).DeclaringType.Name, + typeof(OwnInt).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +a = cls(2) +b = 10 +c = a + b +assert c.Num == a.Num + b + +c = a - b +assert c.Num == a.Num - b + +c = a * b +assert c.Num == a.Num * b + +c = a / b +assert c.Num == a.Num // b + +c = a % b +assert c.Num == a.Num % b + +c = a & b +assert c.Num == a.Num & b + +c = a | b +assert c.Num == a.Num | b + +c = a ^ b +assert c.Num == a.Num ^ b + +c = a == b +assert c == (a.Num == b) + +c = a != b +assert c == (a.Num != b) + +c = a <= b +assert c == (a.Num <= b) + +c = a >= b +assert c == (a.Num >= b) + +c = a < b +assert c == (a.Num < b) + +c = a > b +assert c == (a.Num > b) "); } } From 01d6772b05e88cf7b1472aad751fcde5aa8008c5 Mon Sep 17 00:00:00 2001 From: Gert Dreyer Date: Wed, 21 Feb 2024 22:07:18 +0200 Subject: [PATCH 05/77] Bugfix: RecursionError when reverse/righthand operations invoked. e.g. __rsub__, __rmul__ --- src/runtime/ClassManager.cs | 2 +- src/runtime/MethodBinder.cs | 40 +++++++++++++++++------------ src/runtime/Types/MethodBinding.cs | 3 ++- src/runtime/Types/MethodObject.cs | 18 ++++++------- src/runtime/Types/OperatorMethod.cs | 18 +++++-------- 5 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 79ab20e82..25f8639ab 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -546,7 +546,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ci.members[pyName] = new MethodObject(type, name, forwardMethods).AllocObject(); // Only methods where only the right operand is the declaring type. if (reverseMethods.Length > 0) - ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods).AllocObject(); + ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods, reverse_args: true).AllocObject(); } } diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 07ed4fe22..18ef573d0 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -28,17 +28,22 @@ internal class MethodBinder [NonSerialized] public bool init = false; + public const bool DefaultAllowThreads = true; public bool allow_threads = DefaultAllowThreads; - internal MethodBinder() + public bool args_reversed = false; + + internal MethodBinder(bool reverse_args = false) { list = new List(); + args_reversed = reverse_args; } - internal MethodBinder(MethodInfo mi) + internal MethodBinder(MethodInfo mi, bool reverse_args = false) { list = new List { new MaybeMethodBase(mi) }; + args_reversed = reverse_args; } public int Count @@ -271,10 +276,11 @@ internal static int ArgPrecedence(Type t) /// The Python target of the method invocation. /// The Python arguments. /// The Python keyword arguments. + /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool reverse_args = false) { - return Bind(inst, args, kw, null, null); + return Bind(inst, args, kw, null, null, reverse_args); } /// @@ -287,10 +293,11 @@ internal static int ArgPrecedence(Type t) /// The Python arguments. /// The Python keyword arguments. /// If not null, only bind to that method. + /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool reverse_args = false) { - return Bind(inst, args, kw, info, null); + return Bind(inst, args, kw, info, null, reverse_args); } private readonly struct MatchedMethod @@ -334,8 +341,9 @@ public MismatchedMethod(Exception exception, MethodBase mb) /// The Python keyword arguments. /// If not null, only bind to that method. /// If not null, additionally attempt to bind to the generic methods in this array by inferring generic type parameters. + /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool reverse_args = false) { // loop to find match, return invoker w/ or w/o error var kwargDict = new Dictionary(); @@ -363,10 +371,10 @@ public MismatchedMethod(Exception exception, MethodBase mb) _methods = GetMethods(); } - return Bind(inst, args, kwargDict, _methods, matchGenerics: true); + return Bind(inst, args, kwargDict, _methods, matchGenerics: true, reverse_args); } - static Binding? Bind(BorrowedReference inst, BorrowedReference args, Dictionary kwargDict, MethodBase[] methods, bool matchGenerics) + private static Binding? Bind(BorrowedReference inst, BorrowedReference args, Dictionary kwargDict, MethodBase[] methods, bool matchGenerics, bool reversed = false) { var pynargs = (int)Runtime.PyTuple_Size(args); var isGeneric = false; @@ -386,7 +394,7 @@ public MismatchedMethod(Exception exception, MethodBase mb) // Binary operator methods will have 2 CLR args but only one Python arg // (unary operators will have 1 less each), since Python operator methods are bound. isOperator = isOperator && pynargs == pi.Length - 1; - bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. + bool isReverse = isOperator && reversed; // Only cast if isOperator. if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi)) continue; // Comparison operators in Python have no reverse mode. if (!MatchesArgumentCount(pynargs, pi, kwargDict, out bool paramsArray, out ArrayList? defaultArgList, out int kwargsMatched, out int defaultsNeeded) && !isOperator) @@ -809,14 +817,14 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa return match; } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool reverse_args = false) { - return Invoke(inst, args, kw, null, null); + return Invoke(inst, args, kw, null, null, reverse_args); } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool reverse_args = false) { - return Invoke(inst, args, kw, info, null); + return Invoke(inst, args, kw, info, null, reverse_args = false); } protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) @@ -852,7 +860,7 @@ protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference ar to.Append(')'); } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool reverse_args = false) { // No valid methods, nothing to bind. if (GetMethods().Length == 0) @@ -865,7 +873,7 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a return Exceptions.RaiseTypeError(msg.ToString()); } - Binding? binding = Bind(inst, args, kw, info, methodinfo); + Binding? binding = Bind(inst, args, kw, info, methodinfo, reverse_args); object result; IntPtr ts = IntPtr.Zero; diff --git a/src/runtime/Types/MethodBinding.cs b/src/runtime/Types/MethodBinding.cs index 334d705a6..2f943f3fb 100644 --- a/src/runtime/Types/MethodBinding.cs +++ b/src/runtime/Types/MethodBinding.cs @@ -237,7 +237,8 @@ public static NewReference tp_call(BorrowedReference ob, BorrowedReference args, } } } - return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue); + + return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue, self.m.binder.args_reversed); } finally { diff --git a/src/runtime/Types/MethodObject.cs b/src/runtime/Types/MethodObject.cs index 05198da76..1943ed884 100644 --- a/src/runtime/Types/MethodObject.cs +++ b/src/runtime/Types/MethodObject.cs @@ -19,20 +19,20 @@ internal class MethodObject : ExtensionType { [NonSerialized] private MethodBase[]? _info = null; + private readonly List infoList; internal string name; internal readonly MethodBinder binder; internal bool is_static = false; - internal PyString? doc; internal MaybeType type; - public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads) + public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads, bool reverse_args = false) { this.type = type; this.name = name; this.infoList = new List(); - binder = new MethodBinder(); + binder = new MethodBinder(reverse_args); foreach (MethodBase item in info) { this.infoList.Add(item); @@ -45,8 +45,8 @@ public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_t binder.allow_threads = allow_threads; } - public MethodObject(MaybeType type, string name, MethodBase[] info) - : this(type, name, info, allow_threads: AllowThreads(info)) + public MethodObject(MaybeType type, string name, MethodBase[] info, bool reverse_args = false) + : this(type, name, info, allow_threads: AllowThreads(info), reverse_args) { } @@ -67,14 +67,14 @@ internal MethodBase[] info } } - public virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + public virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool reverse_args = false) { - return Invoke(inst, args, kw, null); + return Invoke(inst, args, kw, null, reverse_args); } - public virtual NewReference Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase? info) + public virtual NewReference Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool reverse_args = false) { - return binder.Invoke(target, args, kw, info, this.info); + return binder.Invoke(target, args, kw, info, this.info, reverse_args); } /// diff --git a/src/runtime/Types/OperatorMethod.cs b/src/runtime/Types/OperatorMethod.cs index e3cc23370..a2ca73982 100644 --- a/src/runtime/Types/OperatorMethod.cs +++ b/src/runtime/Types/OperatorMethod.cs @@ -177,17 +177,14 @@ public static string ReversePyMethodName(string pyName) } /// - /// Check if the method is performing a reverse operation. + /// Check if the method should have a reversed operation. /// /// The operator method. /// - public static bool IsReverse(MethodBase method) + public static bool HaveReverse(MethodBase method) { - Type primaryType = method.IsOpsHelper() - ? method.DeclaringType.GetGenericArguments()[0] - : method.DeclaringType; - Type leftOperandType = method.GetParameters()[0].ParameterType; - return leftOperandType != primaryType; + var pi = method.GetParameters(); + return OpMethodMap.ContainsKey(method.Name) && pi.Length == 2; } public static void FilterMethods(MethodBase[] methods, out MethodBase[] forwardMethods, out MethodBase[] reverseMethods) @@ -196,14 +193,11 @@ public static void FilterMethods(MethodBase[] methods, out MethodBase[] forwardM var reverseMethodsList = new List(); foreach (var method in methods) { - if (IsReverse(method)) + forwardMethodsList.Add(method); + if (HaveReverse(method)) { reverseMethodsList.Add(method); - } else - { - forwardMethodsList.Add(method); } - } forwardMethods = forwardMethodsList.ToArray(); reverseMethods = reverseMethodsList.ToArray(); From d3bde9d6d39e5bcf5990d7dba43cda42321287d2 Mon Sep 17 00:00:00 2001 From: Gert Dreyer Date: Fri, 23 Feb 2024 15:33:44 +0200 Subject: [PATCH 06/77] Update Authors and Changelog --- AUTHORS.md | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 577e898aa..18435671c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -86,3 +86,4 @@ - ([@gpetrou](https://github.com/gpetrou)) - Ehsan Iran-Nejad ([@eirannejad](https://github.com/eirannejad)) - ([@legomanww](https://github.com/legomanww)) +- ([@gertdreyer](https://github.com/gertdreyer)) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdab9bf64..5b545045f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Fixed +- Fixed RecursionError for reverse operators on C# operable types from python. See #2240 ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 From 1542cc96ae1e69f672da85f14ff51ed6a430b6f8 Mon Sep 17 00:00:00 2001 From: Gert Dreyer Date: Mon, 26 Feb 2024 13:11:41 +0200 Subject: [PATCH 07/77] Normalized names. Added HashCode and Equals to testing objects --- src/embed_tests/TestOperator.cs | 19 +++++++----- src/runtime/ClassManager.cs | 2 +- src/runtime/MethodBinder.cs | 50 ++++++++++++++++-------------- src/runtime/Types/MethodBinding.cs | 2 +- src/runtime/Types/MethodObject.cs | 16 +++++----- 5 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 1d66903ca..1ec3268ac 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -41,6 +41,17 @@ public OwnInt(int value) _value = value; } + public override int GetHashCode() + { + return unchecked(65535 + _value.GetHashCode()); + } + + public override bool Equals(object obj) + { + return obj is OwnInt @object && + Num == @object.Num; + } + public static OwnInt operator -(OwnInt p1, OwnInt p2) { return new OwnInt(p1._value - p2._value); @@ -125,14 +136,8 @@ public bool CanDecode(PyType objectType, Type targetType) return objectType.Name == "int" && targetType == typeof(OwnInt); } - public bool TryDecode(PyObject pyObj, out T? value) + public bool TryDecode(PyObject pyObj, out T value) { - if (pyObj.PyType.Name != "int" || typeof(T) != typeof(OwnInt)) - { - value = default(T); - return false; - } - value = (T)(object)new OwnInt(pyObj.As()); return true; } diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 25f8639ab..ecb6055a8 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -546,7 +546,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ci.members[pyName] = new MethodObject(type, name, forwardMethods).AllocObject(); // Only methods where only the right operand is the declaring type. if (reverseMethods.Length > 0) - ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods, reverse_args: true).AllocObject(); + ci.members[pyNameReverse] = new MethodObject(type, name, reverseMethods, argsReversed: true).AllocObject(); } } diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 18ef573d0..836e1da3e 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -32,18 +32,18 @@ internal class MethodBinder public const bool DefaultAllowThreads = true; public bool allow_threads = DefaultAllowThreads; - public bool args_reversed = false; + public bool argsReversed = false; - internal MethodBinder(bool reverse_args = false) + internal MethodBinder(bool argsReversed = false) { list = new List(); - args_reversed = reverse_args; + this.argsReversed = argsReversed; } - internal MethodBinder(MethodInfo mi, bool reverse_args = false) + internal MethodBinder(MethodInfo mi, bool argsReversed = false) { list = new List { new MaybeMethodBase(mi) }; - args_reversed = reverse_args; + this.argsReversed = argsReversed; } public int Count @@ -276,11 +276,11 @@ internal static int ArgPrecedence(Type t) /// The Python target of the method invocation. /// The Python arguments. /// The Python keyword arguments. - /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc + /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool reverse_args = false) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool argsReversed = false) { - return Bind(inst, args, kw, null, null, reverse_args); + return Bind(inst, args, kw, null, null, argsReversed); } /// @@ -293,11 +293,11 @@ internal static int ArgPrecedence(Type t) /// The Python arguments. /// The Python keyword arguments. /// If not null, only bind to that method. - /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc + /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool reverse_args = false) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool argsReversed = false) { - return Bind(inst, args, kw, info, null, reverse_args); + return Bind(inst, args, kw, info, null, argsReversed); } private readonly struct MatchedMethod @@ -341,9 +341,9 @@ public MismatchedMethod(Exception exception, MethodBase mb) /// The Python keyword arguments. /// If not null, only bind to that method. /// If not null, additionally attempt to bind to the generic methods in this array by inferring generic type parameters. - /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc + /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool reverse_args = false) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool argsReversed = false) { // loop to find match, return invoker w/ or w/o error var kwargDict = new Dictionary(); @@ -371,10 +371,10 @@ public MismatchedMethod(Exception exception, MethodBase mb) _methods = GetMethods(); } - return Bind(inst, args, kwargDict, _methods, matchGenerics: true, reverse_args); + return Bind(inst, args, kwargDict, _methods, matchGenerics: true, argsReversed); } - private static Binding? Bind(BorrowedReference inst, BorrowedReference args, Dictionary kwargDict, MethodBase[] methods, bool matchGenerics, bool reversed = false) + private static Binding? Bind(BorrowedReference inst, BorrowedReference args, Dictionary kwargDict, MethodBase[] methods, bool matchGenerics, bool argsReversed = false) { var pynargs = (int)Runtime.PyTuple_Size(args); var isGeneric = false; @@ -394,7 +394,7 @@ public MismatchedMethod(Exception exception, MethodBase mb) // Binary operator methods will have 2 CLR args but only one Python arg // (unary operators will have 1 less each), since Python operator methods are bound. isOperator = isOperator && pynargs == pi.Length - 1; - bool isReverse = isOperator && reversed; // Only cast if isOperator. + bool isReverse = isOperator && argsReversed; // Only cast if isOperator. if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi)) continue; // Comparison operators in Python have no reverse mode. if (!MatchesArgumentCount(pynargs, pi, kwargDict, out bool paramsArray, out ArrayList? defaultArgList, out int kwargsMatched, out int defaultsNeeded) && !isOperator) @@ -402,12 +402,14 @@ public MismatchedMethod(Exception exception, MethodBase mb) continue; } // Preprocessing pi to remove either the first or second argument. - if (isOperator && !isReverse) { + if (isOperator && !isReverse) + { // The first Python arg is the right operand, while the bound instance is the left. // We need to skip the first (left operand) CLR argument. pi = pi.Skip(1).ToArray(); } - else if (isOperator && isReverse) { + else if (isOperator && isReverse) + { // The first Python arg is the left operand. // We need to take the first CLR argument. pi = pi.Take(1).ToArray(); @@ -817,14 +819,14 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa return match; } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool reverse_args = false) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool argsReversed = false) { - return Invoke(inst, args, kw, null, null, reverse_args); + return Invoke(inst, args, kw, null, null, argsReversed); } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool reverse_args = false) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool argsReversed = false) { - return Invoke(inst, args, kw, info, null, reverse_args = false); + return Invoke(inst, args, kw, info, null, argsReversed = false); } protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) @@ -860,7 +862,7 @@ protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference ar to.Append(')'); } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool reverse_args = false) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool argsReversed = false) { // No valid methods, nothing to bind. if (GetMethods().Length == 0) @@ -873,7 +875,7 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a return Exceptions.RaiseTypeError(msg.ToString()); } - Binding? binding = Bind(inst, args, kw, info, methodinfo, reverse_args); + Binding? binding = Bind(inst, args, kw, info, methodinfo, argsReversed); object result; IntPtr ts = IntPtr.Zero; diff --git a/src/runtime/Types/MethodBinding.cs b/src/runtime/Types/MethodBinding.cs index 2f943f3fb..79607d1ae 100644 --- a/src/runtime/Types/MethodBinding.cs +++ b/src/runtime/Types/MethodBinding.cs @@ -238,7 +238,7 @@ public static NewReference tp_call(BorrowedReference ob, BorrowedReference args, } } - return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue, self.m.binder.args_reversed); + return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue, self.m.binder.argsReversed); } finally { diff --git a/src/runtime/Types/MethodObject.cs b/src/runtime/Types/MethodObject.cs index 1943ed884..4bc21458b 100644 --- a/src/runtime/Types/MethodObject.cs +++ b/src/runtime/Types/MethodObject.cs @@ -27,12 +27,12 @@ internal class MethodObject : ExtensionType internal PyString? doc; internal MaybeType type; - public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads, bool reverse_args = false) + public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads, bool argsReversed = false) { this.type = type; this.name = name; this.infoList = new List(); - binder = new MethodBinder(reverse_args); + binder = new MethodBinder(argsReversed); foreach (MethodBase item in info) { this.infoList.Add(item); @@ -45,8 +45,8 @@ public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_t binder.allow_threads = allow_threads; } - public MethodObject(MaybeType type, string name, MethodBase[] info, bool reverse_args = false) - : this(type, name, info, allow_threads: AllowThreads(info), reverse_args) + public MethodObject(MaybeType type, string name, MethodBase[] info, bool argsReversed = false) + : this(type, name, info, allow_threads: AllowThreads(info), argsReversed) { } @@ -67,14 +67,14 @@ internal MethodBase[] info } } - public virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool reverse_args = false) + public virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool argsReversed = false) { - return Invoke(inst, args, kw, null, reverse_args); + return Invoke(inst, args, kw, null, argsReversed); } - public virtual NewReference Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool reverse_args = false) + public virtual NewReference Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool argsReversed = false) { - return binder.Invoke(target, args, kw, info, this.info, reverse_args); + return binder.Invoke(target, args, kw, info, this.info, argsReversed); } /// From 71ca0634696be47ac52066097802d5a53a716ede Mon Sep 17 00:00:00 2001 From: Gert Dreyer Date: Mon, 26 Feb 2024 20:49:55 +0200 Subject: [PATCH 08/77] Cleanup Codec/Argument Reversing Passing. --- src/embed_tests/TestOperator.cs | 4 ++-- src/runtime/MethodBinder.cs | 31 +++++++++++++----------------- src/runtime/Types/MethodBinding.cs | 2 +- src/runtime/Types/MethodObject.cs | 10 +++++----- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 1ec3268ac..6bfb81bdb 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -25,9 +25,9 @@ public void Dispose() } // Mock Integer class to test math ops on non-native dotnet types - public struct OwnInt + public readonly struct OwnInt { - private int _value; + private readonly int _value; public int Num => _value; diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 836e1da3e..9a5515c8e 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -34,16 +34,14 @@ internal class MethodBinder public bool argsReversed = false; - internal MethodBinder(bool argsReversed = false) + internal MethodBinder() { list = new List(); - this.argsReversed = argsReversed; } - internal MethodBinder(MethodInfo mi, bool argsReversed = false) + internal MethodBinder(MethodInfo mi) { list = new List { new MaybeMethodBase(mi) }; - this.argsReversed = argsReversed; } public int Count @@ -276,11 +274,10 @@ internal static int ArgPrecedence(Type t) /// The Python target of the method invocation. /// The Python arguments. /// The Python keyword arguments. - /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool argsReversed = false) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) { - return Bind(inst, args, kw, null, null, argsReversed); + return Bind(inst, args, kw, null, null); } /// @@ -293,11 +290,10 @@ internal static int ArgPrecedence(Type t) /// The Python arguments. /// The Python keyword arguments. /// If not null, only bind to that method. - /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool argsReversed = false) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info) { - return Bind(inst, args, kw, info, null, argsReversed); + return Bind(inst, args, kw, info, null); } private readonly struct MatchedMethod @@ -341,9 +337,8 @@ public MismatchedMethod(Exception exception, MethodBase mb) /// The Python keyword arguments. /// If not null, only bind to that method. /// If not null, additionally attempt to bind to the generic methods in this array by inferring generic type parameters. - /// Reverse arguments of methods. Used for methods such as __radd__, __rsub__, __rmod__ etc /// A Binding if successful. Otherwise null. - internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool argsReversed = false) + internal Binding? Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo) { // loop to find match, return invoker w/ or w/o error var kwargDict = new Dictionary(); @@ -819,14 +814,14 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa return match; } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool argsReversed = false) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) { - return Invoke(inst, args, kw, null, null, argsReversed); + return Invoke(inst, args, kw, null, null); } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool argsReversed = false) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info) { - return Invoke(inst, args, kw, info, null, argsReversed = false); + return Invoke(inst, args, kw, info, null); } protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) @@ -862,7 +857,7 @@ protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference ar to.Append(')'); } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo, bool argsReversed = false) + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase? info, MethodBase[]? methodinfo) { // No valid methods, nothing to bind. if (GetMethods().Length == 0) @@ -875,7 +870,7 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a return Exceptions.RaiseTypeError(msg.ToString()); } - Binding? binding = Bind(inst, args, kw, info, methodinfo, argsReversed); + Binding? binding = Bind(inst, args, kw, info, methodinfo); object result; IntPtr ts = IntPtr.Zero; diff --git a/src/runtime/Types/MethodBinding.cs b/src/runtime/Types/MethodBinding.cs index 79607d1ae..bfe22b0f3 100644 --- a/src/runtime/Types/MethodBinding.cs +++ b/src/runtime/Types/MethodBinding.cs @@ -238,7 +238,7 @@ public static NewReference tp_call(BorrowedReference ob, BorrowedReference args, } } - return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue, self.m.binder.argsReversed); + return self.m.Invoke(target is null ? BorrowedReference.Null : target, args, kw, self.info.UnsafeValue); } finally { diff --git a/src/runtime/Types/MethodObject.cs b/src/runtime/Types/MethodObject.cs index 4bc21458b..12484d301 100644 --- a/src/runtime/Types/MethodObject.cs +++ b/src/runtime/Types/MethodObject.cs @@ -32,7 +32,7 @@ public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_t this.type = type; this.name = name; this.infoList = new List(); - binder = new MethodBinder(argsReversed); + binder = new MethodBinder() { argsReversed = argsReversed }; foreach (MethodBase item in info) { this.infoList.Add(item); @@ -67,14 +67,14 @@ internal MethodBase[] info } } - public virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, bool argsReversed = false) + public virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) { - return Invoke(inst, args, kw, null, argsReversed); + return Invoke(inst, args, kw, null); } - public virtual NewReference Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase? info, bool argsReversed = false) + public virtual NewReference Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase? info) { - return binder.Invoke(target, args, kw, info, this.info, argsReversed); + return binder.Invoke(target, args, kw, info, this.info); } /// From 9d18a243f507e18143ec97c743a0dfe76fdce67d Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 27 Feb 2024 23:36:05 -0800 Subject: [PATCH 09/77] Added `ToPythonAs()` extension method to allow for explicit conversion using a specific type (#2330) fixes https://github.com/pythonnet/pythonnet/issues/2311 --- CHANGELOG.md | 3 +++ src/embed_tests/TestConverter.cs | 9 +++++++++ src/runtime/Converter.cs | 11 +++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b545045f..83f9d4bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Added +- Added `ToPythonAs()` extension method to allow for explicit conversion using a specific type. ([#2311][i2311]) + ### Changed ### Fixed @@ -960,3 +962,4 @@ This version improves performance on benchmarks significantly compared to 2.3. [i238]: https://github.com/pythonnet/pythonnet/issues/238 [i1481]: https://github.com/pythonnet/pythonnet/issues/1481 [i1672]: https://github.com/pythonnet/pythonnet/pull/1672 +[i2311]: https://github.com/pythonnet/pythonnet/issues/2311 diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index 0686d528b..a59b9c97b 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -185,6 +185,15 @@ public void RawPyObjectProxy() Assert.AreEqual(pyObject.DangerousGetAddressOrNull(), proxiedHandle); } + [Test] + public void GenericToPython() + { + int i = 42; + var pyObject = i.ToPythonAs(); + var type = pyObject.GetPythonType(); + Assert.AreEqual(nameof(IConvertible), type.Name); + } + // regression for https://github.com/pythonnet/pythonnet/issues/451 [Test] public void CanGetListFromDerivedClass() diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index 412f3b711..50b33e60e 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -133,7 +133,8 @@ internal static NewReference ToPython(object? value, Type type) if (EncodableByUser(type, value)) { var encoded = PyObjectConversions.TryEncode(value, type); - if (encoded != null) { + if (encoded != null) + { return new NewReference(encoded); } } @@ -334,7 +335,7 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, if (obType.IsGenericType && obType.GetGenericTypeDefinition() == typeof(Nullable<>)) { - if( value == Runtime.PyNone ) + if (value == Runtime.PyNone) { result = null; return true; @@ -980,5 +981,11 @@ public static PyObject ToPython(this object? o) if (o is null) return Runtime.None; return Converter.ToPython(o, o.GetType()).MoveToPyObject(); } + + public static PyObject ToPythonAs(this T? o) + { + if (o is null) return Runtime.None; + return Converter.ToPython(o, typeof(T)).MoveToPyObject(); + } } } From 563e3695f284b0269c73048875b7d2031832993a Mon Sep 17 00:00:00 2001 From: Victor Nova Date: Thu, 15 Feb 2024 13:07:51 -0800 Subject: [PATCH 10/77] IComparable and IEquatable implementations for PyInt, PyFloat, and PyString for primitive .NET types --- CHANGELOG.md | 3 + src/embed_tests/TestPyFloat.cs | 27 ++++ src/embed_tests/TestPyInt.cs | 70 +++++++++ src/embed_tests/TestPyString.cs | 19 +++ .../PythonTypes/PyFloat.IComparable.cs | 34 +++++ src/runtime/PythonTypes/PyFloat.cs | 4 +- src/runtime/PythonTypes/PyInt.IComparable.cs | 136 ++++++++++++++++++ src/runtime/PythonTypes/PyInt.cs | 2 +- src/runtime/PythonTypes/PyObject.cs | 19 ++- src/runtime/PythonTypes/PyString.cs | 21 ++- 10 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 src/runtime/PythonTypes/PyFloat.IComparable.cs create mode 100644 src/runtime/PythonTypes/PyInt.IComparable.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f9d4bd1..e6cc52d72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. - Added `ToPythonAs()` extension method to allow for explicit conversion using a specific type. ([#2311][i2311]) +- Added `IComparable` and `IEquatable` implementations to `PyInt`, `PyFloat`, and `PyString` + to compare with primitive .NET types like `long`. + ### Changed ### Fixed diff --git a/src/embed_tests/TestPyFloat.cs b/src/embed_tests/TestPyFloat.cs index 36531cb6a..89e29e5fd 100644 --- a/src/embed_tests/TestPyFloat.cs +++ b/src/embed_tests/TestPyFloat.cs @@ -126,5 +126,32 @@ public void AsFloatBad() StringAssert.StartsWith("could not convert string to float", ex.Message); Assert.IsNull(a); } + + [Test] + public void CompareTo() + { + var v = new PyFloat(42); + + Assert.AreEqual(0, v.CompareTo(42f)); + Assert.AreEqual(0, v.CompareTo(42d)); + + Assert.AreEqual(1, v.CompareTo(41f)); + Assert.AreEqual(1, v.CompareTo(41d)); + + Assert.AreEqual(-1, v.CompareTo(43f)); + Assert.AreEqual(-1, v.CompareTo(43d)); + } + + [Test] + public void Equals() + { + var v = new PyFloat(42); + + Assert.IsTrue(v.Equals(42f)); + Assert.IsTrue(v.Equals(42d)); + + Assert.IsFalse(v.Equals(41f)); + Assert.IsFalse(v.Equals(41d)); + } } } diff --git a/src/embed_tests/TestPyInt.cs b/src/embed_tests/TestPyInt.cs index c147e074b..d2767e664 100644 --- a/src/embed_tests/TestPyInt.cs +++ b/src/embed_tests/TestPyInt.cs @@ -210,6 +210,76 @@ public void ToBigInteger() CollectionAssert.AreEqual(expected, actual); } + [Test] + public void CompareTo() + { + var v = new PyInt(42); + + #region Signed + Assert.AreEqual(0, v.CompareTo(42L)); + Assert.AreEqual(0, v.CompareTo(42)); + Assert.AreEqual(0, v.CompareTo((short)42)); + Assert.AreEqual(0, v.CompareTo((sbyte)42)); + + Assert.AreEqual(1, v.CompareTo(41L)); + Assert.AreEqual(1, v.CompareTo(41)); + Assert.AreEqual(1, v.CompareTo((short)41)); + Assert.AreEqual(1, v.CompareTo((sbyte)41)); + + Assert.AreEqual(-1, v.CompareTo(43L)); + Assert.AreEqual(-1, v.CompareTo(43)); + Assert.AreEqual(-1, v.CompareTo((short)43)); + Assert.AreEqual(-1, v.CompareTo((sbyte)43)); + #endregion Signed + + #region Unsigned + Assert.AreEqual(0, v.CompareTo(42UL)); + Assert.AreEqual(0, v.CompareTo(42U)); + Assert.AreEqual(0, v.CompareTo((ushort)42)); + Assert.AreEqual(0, v.CompareTo((byte)42)); + + Assert.AreEqual(1, v.CompareTo(41UL)); + Assert.AreEqual(1, v.CompareTo(41U)); + Assert.AreEqual(1, v.CompareTo((ushort)41)); + Assert.AreEqual(1, v.CompareTo((byte)41)); + + Assert.AreEqual(-1, v.CompareTo(43UL)); + Assert.AreEqual(-1, v.CompareTo(43U)); + Assert.AreEqual(-1, v.CompareTo((ushort)43)); + Assert.AreEqual(-1, v.CompareTo((byte)43)); + #endregion Unsigned + } + + [Test] + public void Equals() + { + var v = new PyInt(42); + + #region Signed + Assert.True(v.Equals(42L)); + Assert.True(v.Equals(42)); + Assert.True(v.Equals((short)42)); + Assert.True(v.Equals((sbyte)42)); + + Assert.False(v.Equals(41L)); + Assert.False(v.Equals(41)); + Assert.False(v.Equals((short)41)); + Assert.False(v.Equals((sbyte)41)); + #endregion Signed + + #region Unsigned + Assert.True(v.Equals(42UL)); + Assert.True(v.Equals(42U)); + Assert.True(v.Equals((ushort)42)); + Assert.True(v.Equals((byte)42)); + + Assert.False(v.Equals(41UL)); + Assert.False(v.Equals(41U)); + Assert.False(v.Equals((ushort)41)); + Assert.False(v.Equals((byte)41)); + #endregion Unsigned + } + [Test] public void ToBigIntegerLarge() { diff --git a/src/embed_tests/TestPyString.cs b/src/embed_tests/TestPyString.cs index b12e08c23..35c6339ee 100644 --- a/src/embed_tests/TestPyString.cs +++ b/src/embed_tests/TestPyString.cs @@ -112,5 +112,24 @@ public void TestUnicodeSurrogate() Assert.AreEqual(4, actual.Length()); Assert.AreEqual(expected, actual.ToString()); } + + [Test] + public void CompareTo() + { + var a = new PyString("foo"); + + Assert.AreEqual(0, a.CompareTo("foo")); + Assert.AreEqual("foo".CompareTo("bar"), a.CompareTo("bar")); + Assert.AreEqual("foo".CompareTo("foz"), a.CompareTo("foz")); + } + + [Test] + public void Equals() + { + var a = new PyString("foo"); + + Assert.True(a.Equals("foo")); + Assert.False(a.Equals("bar")); + } } } diff --git a/src/runtime/PythonTypes/PyFloat.IComparable.cs b/src/runtime/PythonTypes/PyFloat.IComparable.cs new file mode 100644 index 000000000..c12fc283a --- /dev/null +++ b/src/runtime/PythonTypes/PyFloat.IComparable.cs @@ -0,0 +1,34 @@ +using System; + +namespace Python.Runtime; + +partial class PyFloat : IComparable, IComparable + , IEquatable, IEquatable + , IComparable, IEquatable +{ + public override bool Equals(object o) + { + using var _ = Py.GIL(); + return o switch + { + double f64 => this.Equals(f64), + float f32 => this.Equals(f32), + _ => base.Equals(o), + }; + } + + public int CompareTo(double other) => this.ToDouble().CompareTo(other); + + public int CompareTo(float other) => this.ToDouble().CompareTo(other); + + public bool Equals(double other) => this.ToDouble().Equals(other); + + public bool Equals(float other) => this.ToDouble().Equals(other); + + public int CompareTo(PyFloat? other) + { + return other is null ? 1 : this.CompareTo(other.BorrowNullable()); + } + + public bool Equals(PyFloat? other) => base.Equals(other); +} diff --git a/src/runtime/PythonTypes/PyFloat.cs b/src/runtime/PythonTypes/PyFloat.cs index c09ec93ba..50621d5c2 100644 --- a/src/runtime/PythonTypes/PyFloat.cs +++ b/src/runtime/PythonTypes/PyFloat.cs @@ -8,7 +8,7 @@ namespace Python.Runtime /// PY3: https://docs.python.org/3/c-api/float.html /// for details. /// - public class PyFloat : PyNumber + public partial class PyFloat : PyNumber { internal PyFloat(in StolenReference ptr) : base(ptr) { @@ -100,6 +100,8 @@ public static PyFloat AsFloat(PyObject value) return new PyFloat(op.Steal()); } + public double ToDouble() => Runtime.PyFloat_AsDouble(obj); + public override TypeCode GetTypeCode() => TypeCode.Double; } } diff --git a/src/runtime/PythonTypes/PyInt.IComparable.cs b/src/runtime/PythonTypes/PyInt.IComparable.cs new file mode 100644 index 000000000..a96f02e10 --- /dev/null +++ b/src/runtime/PythonTypes/PyInt.IComparable.cs @@ -0,0 +1,136 @@ +using System; + +namespace Python.Runtime; + +partial class PyInt : IComparable, IComparable, IComparable, IComparable + , IComparable, IComparable, IComparable, IComparable + , IEquatable, IEquatable, IEquatable, IEquatable + , IEquatable, IEquatable, IEquatable, IEquatable + , IComparable, IEquatable +{ + public override bool Equals(object o) + { + using var _ = Py.GIL(); + return o switch + { + long i64 => this.Equals(i64), + int i32 => this.Equals(i32), + short i16 => this.Equals(i16), + sbyte i8 => this.Equals(i8), + + ulong u64 => this.Equals(u64), + uint u32 => this.Equals(u32), + ushort u16 => this.Equals(u16), + byte u8 => this.Equals(u8), + + _ => base.Equals(o), + }; + } + + #region Signed + public int CompareTo(long other) + { + using var pyOther = Runtime.PyInt_FromInt64(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public int CompareTo(int other) + { + using var pyOther = Runtime.PyInt_FromInt32(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public int CompareTo(short other) + { + using var pyOther = Runtime.PyInt_FromInt32(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public int CompareTo(sbyte other) + { + using var pyOther = Runtime.PyInt_FromInt32(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public bool Equals(long other) + { + using var pyOther = Runtime.PyInt_FromInt64(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + + public bool Equals(int other) + { + using var pyOther = Runtime.PyInt_FromInt32(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + + public bool Equals(short other) + { + using var pyOther = Runtime.PyInt_FromInt32(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + + public bool Equals(sbyte other) + { + using var pyOther = Runtime.PyInt_FromInt32(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + #endregion Signed + + #region Unsigned + public int CompareTo(ulong other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public int CompareTo(uint other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public int CompareTo(ushort other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public int CompareTo(byte other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.CompareTo(pyOther.BorrowOrThrow()); + } + + public bool Equals(ulong other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + + public bool Equals(uint other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + + public bool Equals(ushort other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + + public bool Equals(byte other) + { + using var pyOther = Runtime.PyLong_FromUnsignedLongLong(other); + return this.Equals(pyOther.BorrowOrThrow()); + } + #endregion Unsigned + + public int CompareTo(PyInt? other) + { + return other is null ? 1 : this.CompareTo(other.BorrowNullable()); + } + + public bool Equals(PyInt? other) => base.Equals(other); +} diff --git a/src/runtime/PythonTypes/PyInt.cs b/src/runtime/PythonTypes/PyInt.cs index e71462b74..0d00f5a13 100644 --- a/src/runtime/PythonTypes/PyInt.cs +++ b/src/runtime/PythonTypes/PyInt.cs @@ -9,7 +9,7 @@ namespace Python.Runtime /// Represents a Python integer object. /// See the documentation at https://docs.python.org/3/c-api/long.html /// - public class PyInt : PyNumber, IFormattable + public partial class PyInt : PyNumber, IFormattable { internal PyInt(in StolenReference ptr) : base(ptr) { diff --git a/src/runtime/PythonTypes/PyObject.cs b/src/runtime/PythonTypes/PyObject.cs index bda2d9c02..cf0c2a03f 100644 --- a/src/runtime/PythonTypes/PyObject.cs +++ b/src/runtime/PythonTypes/PyObject.cs @@ -1136,6 +1136,23 @@ public long Refcount } } + internal int CompareTo(BorrowedReference other) + { + int greater = Runtime.PyObject_RichCompareBool(this.Reference, other, Runtime.Py_GT); + Debug.Assert(greater != -1); + if (greater > 0) + return 1; + int less = Runtime.PyObject_RichCompareBool(this.Reference, other, Runtime.Py_LT); + Debug.Assert(less != -1); + return less > 0 ? -1 : 0; + } + + internal bool Equals(BorrowedReference other) + { + int equal = Runtime.PyObject_RichCompareBool(this.Reference, other, Runtime.Py_EQ); + Debug.Assert(equal != -1); + return equal > 0; + } public override bool TryGetMember(GetMemberBinder binder, out object? result) { @@ -1325,7 +1342,7 @@ private bool TryCompare(PyObject arg, int op, out object @out) } return true; } - + public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object? result) { using var _ = Py.GIL(); diff --git a/src/runtime/PythonTypes/PyString.cs b/src/runtime/PythonTypes/PyString.cs index d54397fcf..6fed25c3e 100644 --- a/src/runtime/PythonTypes/PyString.cs +++ b/src/runtime/PythonTypes/PyString.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.Serialization; namespace Python.Runtime @@ -13,7 +14,7 @@ namespace Python.Runtime /// 2011-01-29: ...Then why does the string constructor call PyUnicode_FromUnicode()??? /// [Serializable] - public class PyString : PySequence + public class PyString : PySequence, IComparable, IEquatable { internal PyString(in StolenReference reference) : base(reference) { } internal PyString(BorrowedReference reference) : base(reference) { } @@ -61,5 +62,23 @@ public static bool IsStringType(PyObject value) } public override TypeCode GetTypeCode() => TypeCode.String; + + internal string ToStringUnderGIL() + { + string? result = Runtime.GetManagedString(this.Reference); + Debug.Assert(result is not null); + return result!; + } + + public bool Equals(string? other) + => this.ToStringUnderGIL().Equals(other, StringComparison.CurrentCulture); + public int CompareTo(string? other) + => string.Compare(this.ToStringUnderGIL(), other, StringComparison.CurrentCulture); + + public override string ToString() + { + using var _ = Py.GIL(); + return this.ToStringUnderGIL(); + } } } From 6a8a97d0fc78ec11c754f8d8746c672cacefc586 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 5 May 2024 20:42:02 +0200 Subject: [PATCH 11/77] Fix CI by using macos-13 explicitly, adjust OS config (#2373) --- .github/workflows/main.yml | 48 +++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4af10c68..3396b83cc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,25 +9,35 @@ on: jobs: build-test: name: Build and Test - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os.instance }} timeout-minutes: 15 strategy: fail-fast: false matrix: - os: [windows, ubuntu, macos] - python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] - platform: [x64, x86] - exclude: - - os: ubuntu - platform: x86 - - os: macos + os: + - category: windows platform: x86 + instance: windows-latest + + - category: windows + platform: x64 + instance: windows-latest + + - category: ubuntu + platform: x64 + instance: ubuntu-latest + + - category: macos + platform: x64 + instance: macos-13 + + python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Set Environment on macOS uses: maxim-lobanov/setup-xamarin@v1 - if: ${{ matrix.os == 'macos' }} + if: ${{ matrix.os.category == 'macos' }} with: mono-version: latest @@ -43,7 +53,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - architecture: ${{ matrix.platform }} + architecture: ${{ matrix.os.platform }} - name: Install dependencies run: | @@ -55,42 +65,42 @@ jobs: pip install -v . - name: Set Python DLL path and PYTHONHOME (non Windows) - if: ${{ matrix.os != 'windows' }} + if: ${{ matrix.os.category != 'windows' }} run: | echo PYTHONNET_PYDLL=$(python -m find_libpython) >> $GITHUB_ENV echo PYTHONHOME=$(python -c 'import sys; print(sys.prefix)') >> $GITHUB_ENV - name: Set Python DLL path and PYTHONHOME (Windows) - if: ${{ matrix.os == 'windows' }} + if: ${{ matrix.os.category == 'windows' }} run: | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONNET_PYDLL=$(python -m find_libpython)" Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')" - name: Embedding tests - run: dotnet test --runtime any-${{ matrix.platform }} --logger "console;verbosity=detailed" src/embed_tests/ + run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ env: MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 - name: Python Tests (Mono) - if: ${{ matrix.os != 'windows' }} + if: ${{ matrix.os.category != 'windows' }} run: pytest --runtime mono # TODO: Run these tests on Windows x86 - name: Python Tests (.NET Core) - if: ${{ matrix.platform == 'x64' }} + if: ${{ matrix.os.platform == 'x64' }} run: pytest --runtime coreclr - name: Python Tests (.NET Framework) - if: ${{ matrix.os == 'windows' }} + if: ${{ matrix.os.category == 'windows' }} run: pytest --runtime netfx - name: Python tests run from .NET - run: dotnet test --runtime any-${{ matrix.platform }} src/python_tests_runner/ + run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ - name: Perf tests - if: ${{ (matrix.python == '3.8') && (matrix.platform == 'x64') }} + if: ${{ (matrix.python == '3.8') && (matrix.os.platform == 'x64') }} run: | pip install --force --no-deps --target src/perf_tests/baseline/ pythonnet==2.5.2 - dotnet test --configuration Release --runtime any-${{ matrix.platform }} --logger "console;verbosity=detailed" src/perf_tests/ + dotnet test --configuration Release --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/perf_tests/ # TODO: Run mono tests on Windows? From 195cde67fffd06521f3bcb2294e60cad4ec506d6 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 10 May 2024 19:28:38 +0200 Subject: [PATCH 12/77] Use non-BOM encodings (#2370) * Use non-BOM encodings The documentation of the used `PyUnicode_DecodeUTF16` states that not passing `*byteorder` or passing a 0 results in the first two bytes, if they are the BOM (U+FEFF, zero-width no-break space), to be interpreted and skipped, which is incorrect when we convert a known "non BOM" string, which all strings from C# are. --- src/embed_tests/TestPyType.cs | 2 +- src/runtime/Loader.cs | 6 ++-- src/runtime/Native/CustomMarshaler.cs | 2 +- src/runtime/Native/NativeTypeSpec.cs | 2 +- src/runtime/PythonTypes/PyType.cs | 2 +- src/runtime/Runtime.cs | 46 ++++++++++++++------------- src/runtime/Util/Encodings.cs | 10 ++++++ tests/test_conversion.py | 3 ++ 8 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 src/runtime/Util/Encodings.cs diff --git a/src/embed_tests/TestPyType.cs b/src/embed_tests/TestPyType.cs index 34645747d..0470070c3 100644 --- a/src/embed_tests/TestPyType.cs +++ b/src/embed_tests/TestPyType.cs @@ -28,7 +28,7 @@ public void CanCreateHeapType() const string name = "nÁmæ"; const string docStr = "dÁcæ"; - using var doc = new StrPtr(docStr, Encoding.UTF8); + using var doc = new StrPtr(docStr, Encodings.UTF8); var spec = new TypeSpec( name: name, basicSize: Util.ReadInt32(Runtime.Runtime.PyBaseObjectType, TypeOffset.tp_basicsize), diff --git a/src/runtime/Loader.cs b/src/runtime/Loader.cs index 516b9ab9c..c0e964abc 100644 --- a/src/runtime/Loader.cs +++ b/src/runtime/Loader.cs @@ -12,7 +12,7 @@ public unsafe static int Initialize(IntPtr data, int size) { try { - var dllPath = Encoding.UTF8.GetString((byte*)data.ToPointer(), size); + var dllPath = Encodings.UTF8.GetString((byte*)data.ToPointer(), size); if (!string.IsNullOrEmpty(dllPath)) { @@ -33,7 +33,7 @@ public unsafe static int Initialize(IntPtr data, int size) ); return 1; } - + return 0; } @@ -41,7 +41,7 @@ public unsafe static int Shutdown(IntPtr data, int size) { try { - var command = Encoding.UTF8.GetString((byte*)data.ToPointer(), size); + var command = Encodings.UTF8.GetString((byte*)data.ToPointer(), size); if (command == "full_shutdown") { diff --git a/src/runtime/Native/CustomMarshaler.cs b/src/runtime/Native/CustomMarshaler.cs index 62c027150..299af3a33 100644 --- a/src/runtime/Native/CustomMarshaler.cs +++ b/src/runtime/Native/CustomMarshaler.cs @@ -42,7 +42,7 @@ public int GetNativeDataSize() internal class UcsMarshaler : MarshalerBase { internal static readonly int _UCS = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 2 : 4; - internal static readonly Encoding PyEncoding = _UCS == 2 ? Encoding.Unicode : Encoding.UTF32; + internal static readonly Encoding PyEncoding = _UCS == 2 ? Encodings.UTF16 : Encodings.UTF32; private static readonly MarshalerBase Instance = new UcsMarshaler(); public override IntPtr MarshalManagedToNative(object managedObj) diff --git a/src/runtime/Native/NativeTypeSpec.cs b/src/runtime/Native/NativeTypeSpec.cs index 8b84df536..50019a148 100644 --- a/src/runtime/Native/NativeTypeSpec.cs +++ b/src/runtime/Native/NativeTypeSpec.cs @@ -17,7 +17,7 @@ public NativeTypeSpec(TypeSpec spec) { if (spec is null) throw new ArgumentNullException(nameof(spec)); - this.Name = new StrPtr(spec.Name, Encoding.UTF8); + this.Name = new StrPtr(spec.Name, Encodings.UTF8); this.BasicSize = spec.BasicSize; this.ItemSize = spec.ItemSize; this.Flags = (int)spec.Flags; diff --git a/src/runtime/PythonTypes/PyType.cs b/src/runtime/PythonTypes/PyType.cs index af796a5c5..28bda5d3e 100644 --- a/src/runtime/PythonTypes/PyType.cs +++ b/src/runtime/PythonTypes/PyType.cs @@ -53,7 +53,7 @@ public string Name { RawPointer = Util.ReadIntPtr(this, TypeOffset.tp_name), }; - return namePtr.ToString(System.Text.Encoding.UTF8)!; + return namePtr.ToString(Encodings.UTF8)!; } } diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index 4e1c6156a..2f9e18f65 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -795,13 +795,13 @@ public static int Py_Main(int argc, string[] argv) internal static int PyRun_SimpleString(string code) { - using var codePtr = new StrPtr(code, Encoding.UTF8); + using var codePtr = new StrPtr(code, Encodings.UTF8); return Delegates.PyRun_SimpleStringFlags(codePtr, Utf8String); } internal static NewReference PyRun_String(string code, RunFlagType st, BorrowedReference globals, BorrowedReference locals) { - using var codePtr = new StrPtr(code, Encoding.UTF8); + using var codePtr = new StrPtr(code, Encodings.UTF8); return Delegates.PyRun_StringFlags(codePtr, st, globals, locals, Utf8String); } @@ -813,14 +813,14 @@ internal static NewReference PyRun_String(string code, RunFlagType st, BorrowedR /// internal static NewReference Py_CompileString(string str, string file, int start) { - using var strPtr = new StrPtr(str, Encoding.UTF8); + using var strPtr = new StrPtr(str, Encodings.UTF8); using var fileObj = new PyString(file); return Delegates.Py_CompileStringObject(strPtr, fileObj, start, Utf8String, -1); } internal static NewReference PyImport_ExecCodeModule(string name, BorrowedReference code) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyImport_ExecCodeModule(namePtr, code); } @@ -867,13 +867,13 @@ internal static bool PyObject_IsIterable(BorrowedReference ob) internal static int PyObject_HasAttrString(BorrowedReference pointer, string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyObject_HasAttrString(pointer, namePtr); } internal static NewReference PyObject_GetAttrString(BorrowedReference pointer, string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyObject_GetAttrString(pointer, namePtr); } @@ -884,12 +884,12 @@ internal static NewReference PyObject_GetAttrString(BorrowedReference pointer, S internal static int PyObject_DelAttr(BorrowedReference @object, BorrowedReference name) => Delegates.PyObject_SetAttr(@object, name, null); internal static int PyObject_DelAttrString(BorrowedReference @object, string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyObject_SetAttrString(@object, namePtr, null); } internal static int PyObject_SetAttrString(BorrowedReference @object, string name, BorrowedReference value) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyObject_SetAttrString(@object, namePtr, value); } @@ -1071,7 +1071,7 @@ internal static bool PyBool_CheckExact(BorrowedReference ob) internal static NewReference PyLong_FromString(string value, int radix) { - using var valPtr = new StrPtr(value, Encoding.UTF8); + using var valPtr = new StrPtr(value, Encodings.UTF8); return Delegates.PyLong_FromString(valPtr, IntPtr.Zero, radix); } @@ -1252,12 +1252,14 @@ internal static bool PyString_CheckExact(BorrowedReference ob) internal static NewReference PyString_FromString(string value) { + int byteorder = BitConverter.IsLittleEndian ? -1 : 1; + int* byteorderPtr = &byteorder; fixed(char* ptr = value) return Delegates.PyUnicode_DecodeUTF16( (IntPtr)ptr, value.Length * sizeof(Char), IntPtr.Zero, - IntPtr.Zero + (IntPtr)byteorderPtr ); } @@ -1272,7 +1274,7 @@ internal static NewReference EmptyPyBytes() internal static NewReference PyByteArray_FromStringAndSize(IntPtr strPtr, nint len) => Delegates.PyByteArray_FromStringAndSize(strPtr, len); internal static NewReference PyByteArray_FromStringAndSize(string s) { - using var ptr = new StrPtr(s, Encoding.UTF8); + using var ptr = new StrPtr(s, Encodings.UTF8); return PyByteArray_FromStringAndSize(ptr.RawPointer, checked((nint)ptr.ByteCount)); } @@ -1300,7 +1302,7 @@ internal static IntPtr PyBytes_AsString(BorrowedReference ob) internal static NewReference PyUnicode_InternFromString(string s) { - using var ptr = new StrPtr(s, Encoding.UTF8); + using var ptr = new StrPtr(s, Encodings.UTF8); return Delegates.PyUnicode_InternFromString(ptr); } @@ -1375,7 +1377,7 @@ internal static bool PyDict_Check(BorrowedReference ob) internal static BorrowedReference PyDict_GetItemString(BorrowedReference pointer, string key) { - using var keyStr = new StrPtr(key, Encoding.UTF8); + using var keyStr = new StrPtr(key, Encodings.UTF8); return Delegates.PyDict_GetItemString(pointer, keyStr); } @@ -1391,7 +1393,7 @@ internal static BorrowedReference PyDict_GetItemString(BorrowedReference pointer /// internal static int PyDict_SetItemString(BorrowedReference dict, string key, BorrowedReference value) { - using var keyPtr = new StrPtr(key, Encoding.UTF8); + using var keyPtr = new StrPtr(key, Encodings.UTF8); return Delegates.PyDict_SetItemString(dict, keyPtr, value); } @@ -1400,7 +1402,7 @@ internal static int PyDict_SetItemString(BorrowedReference dict, string key, Bor internal static int PyDict_DelItemString(BorrowedReference pointer, string key) { - using var keyPtr = new StrPtr(key, Encoding.UTF8); + using var keyPtr = new StrPtr(key, Encodings.UTF8); return Delegates.PyDict_DelItemString(pointer, keyPtr); } @@ -1515,7 +1517,7 @@ internal static bool PyIter_Check(BorrowedReference ob) internal static NewReference PyModule_New(string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyModule_New(namePtr); } @@ -1529,7 +1531,7 @@ internal static NewReference PyModule_New(string name) /// Return -1 on error, 0 on success. internal static int PyModule_AddObject(BorrowedReference module, string name, StolenReference value) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); IntPtr valueAddr = value.DangerousGetAddressOrNull(); int res = Delegates.PyModule_AddObject(module, namePtr, valueAddr); // We can't just exit here because the reference is stolen only on success. @@ -1547,7 +1549,7 @@ internal static int PyModule_AddObject(BorrowedReference module, string name, St internal static NewReference PyImport_ImportModule(string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyImport_ImportModule(namePtr); } @@ -1556,7 +1558,7 @@ internal static NewReference PyImport_ImportModule(string name) internal static BorrowedReference PyImport_AddModule(string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PyImport_AddModule(namePtr); } @@ -1584,13 +1586,13 @@ internal static void PySys_SetArgvEx(int argc, string[] argv, int updatepath) internal static BorrowedReference PySys_GetObject(string name) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PySys_GetObject(namePtr); } internal static int PySys_SetObject(string name, BorrowedReference ob) { - using var namePtr = new StrPtr(name, Encoding.UTF8); + using var namePtr = new StrPtr(name, Encodings.UTF8); return Delegates.PySys_SetObject(namePtr, ob); } @@ -1689,7 +1691,7 @@ internal static IntPtr PyMem_Malloc(long size) internal static void PyErr_SetString(BorrowedReference ob, string message) { - using var msgPtr = new StrPtr(message, Encoding.UTF8); + using var msgPtr = new StrPtr(message, Encodings.UTF8); Delegates.PyErr_SetString(ob, msgPtr); } diff --git a/src/runtime/Util/Encodings.cs b/src/runtime/Util/Encodings.cs new file mode 100644 index 000000000..d5a0c6ff8 --- /dev/null +++ b/src/runtime/Util/Encodings.cs @@ -0,0 +1,10 @@ +using System; +using System.Text; + +namespace Python.Runtime; + +static class Encodings { + public static System.Text.Encoding UTF8 = new UTF8Encoding(false, true); + public static System.Text.Encoding UTF16 = new UnicodeEncoding(!BitConverter.IsLittleEndian, false, true); + public static System.Text.Encoding UTF32 = new UTF32Encoding(!BitConverter.IsLittleEndian, false, true); +} diff --git a/tests/test_conversion.py b/tests/test_conversion.py index bb686dd52..dd70f900a 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -510,6 +510,9 @@ def test_string_conversion(): ob.StringField = System.String(u'\uffff\uffff') assert ob.StringField == u'\uffff\uffff' + ob.StringField = System.String("\ufeffbom") + assert ob.StringField == "\ufeffbom" + ob.StringField = None assert ob.StringField is None From 32051cb3c7c2edffa031569043eac5aecaa573a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Bourbonnais?= <6788684+BadSingleton@users.noreply.github.com> Date: Fri, 10 May 2024 13:35:55 -0400 Subject: [PATCH 13/77] Expose serialization api (#2336) * Expose an API for users to specify their own formatter Adds post-serialization and pre-deserialization hooks for additional customization. * Add API for capsuling data when serializing * Add NoopFormatter and fall back to it if BinaryFormatter is not available --------- Co-authored-by: Benedikt Reinartz --- CHANGELOG.md | 3 + .../StateSerialization/NoopFormatter.cs | 14 ++ src/runtime/StateSerialization/RuntimeData.cs | 138 +++++++++++++++++- tests/domain_tests/TestRunner.cs | 117 +++++++++++++++ tests/domain_tests/test_domain_reload.py | 3 + 5 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 src/runtime/StateSerialization/NoopFormatter.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cc52d72..adef224e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. to compare with primitive .NET types like `long`. ### Changed +- Added a `FormatterFactory` member in RuntimeData to create formatters with parameters. For compatibility, the `FormatterType` member is still present and has precedence when defining both `FormatterFactory` and `FormatterType` +- Added a post-serialization and a pre-deserialization step callbacks to extend (de)serialization process +- Added an API to stash serialized data on Python capsules ### Fixed diff --git a/src/runtime/StateSerialization/NoopFormatter.cs b/src/runtime/StateSerialization/NoopFormatter.cs new file mode 100644 index 000000000..f05b7ebb2 --- /dev/null +++ b/src/runtime/StateSerialization/NoopFormatter.cs @@ -0,0 +1,14 @@ +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace Python.Runtime; + +public class NoopFormatter : IFormatter { + public object Deserialize(Stream s) => throw new NotImplementedException(); + public void Serialize(Stream s, object o) {} + + public SerializationBinder? Binder { get; set; } + public StreamingContext Context { get; set; } + public ISurrogateSelector? SurrogateSelector { get; set; } +} diff --git a/src/runtime/StateSerialization/RuntimeData.cs b/src/runtime/StateSerialization/RuntimeData.cs index 204e15b5b..8eda9ce0b 100644 --- a/src/runtime/StateSerialization/RuntimeData.cs +++ b/src/runtime/StateSerialization/RuntimeData.cs @@ -1,7 +1,5 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; @@ -17,7 +15,34 @@ namespace Python.Runtime { public static class RuntimeData { - private static Type? _formatterType; + + public readonly static Func DefaultFormatterFactory = () => + { + try + { + return new BinaryFormatter(); + } + catch + { + return new NoopFormatter(); + } + }; + + private static Func _formatterFactory { get; set; } = DefaultFormatterFactory; + + public static Func FormatterFactory + { + get => _formatterFactory; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + _formatterFactory = value; + } + } + + private static Type? _formatterType = null; public static Type? FormatterType { get => _formatterType; @@ -31,6 +56,14 @@ public static Type? FormatterType } } + /// + /// Callback called as a last step in the serialization process + /// + public static Action? PostStashHook { get; set; } = null; + /// + /// Callback called as the first step in the deserialization process + /// + public static Action? PreRestoreHook { get; set; } = null; public static ICLRObjectStorer? WrappersStorer { get; set; } /// @@ -74,6 +107,7 @@ internal static void Stash() using NewReference capsule = PyCapsule_New(mem, IntPtr.Zero, IntPtr.Zero); int res = PySys_SetObject("clr_data", capsule.BorrowOrThrow()); PythonException.ThrowIfIsNotZero(res); + PostStashHook?.Invoke(); } internal static void RestoreRuntimeData() @@ -90,6 +124,7 @@ internal static void RestoreRuntimeData() private static void RestoreRuntimeDataImpl() { + PreRestoreHook?.Invoke(); BorrowedReference capsule = PySys_GetObject("clr_data"); if (capsule.IsNull) { @@ -250,11 +285,102 @@ private static void RestoreRuntimeDataObjects(SharedObjectsState storage) } } + static readonly string serialization_key_namepsace = "pythonnet_serialization_"; + /// + /// Removes the serialization capsule from the `sys` module object. + /// + /// + /// The serialization data must have been set with StashSerializationData + /// + /// The name given to the capsule on the `sys` module object + public static void FreeSerializationData(string key) + { + key = serialization_key_namepsace + key; + BorrowedReference oldCapsule = PySys_GetObject(key); + if (!oldCapsule.IsNull) + { + IntPtr oldData = PyCapsule_GetPointer(oldCapsule, IntPtr.Zero); + Marshal.FreeHGlobal(oldData); + PyCapsule_SetPointer(oldCapsule, IntPtr.Zero); + PySys_SetObject(key, null); + } + } + + /// + /// Stores the data in the argument in a Python capsule and stores + /// the capsule on the `sys` module object with the name . + /// + /// + /// No checks on pre-existing names on the `sys` module object are made. + /// + /// The name given to the capsule on the `sys` module object + /// A MemoryStream that contains the data to be placed in the capsule + public static void StashSerializationData(string key, MemoryStream stream) + { + if (stream.TryGetBuffer(out var data)) + { + IntPtr mem = Marshal.AllocHGlobal(IntPtr.Size + data.Count); + + // store the length of the buffer first + Marshal.WriteIntPtr(mem, (IntPtr)data.Count); + Marshal.Copy(data.Array, data.Offset, mem + IntPtr.Size, data.Count); + + try + { + using NewReference capsule = PyCapsule_New(mem, IntPtr.Zero, IntPtr.Zero); + int res = PySys_SetObject(key, capsule.BorrowOrThrow()); + PythonException.ThrowIfIsNotZero(res); + } + catch + { + Marshal.FreeHGlobal(mem); + } + } + else + { + throw new NotImplementedException($"{nameof(stream)} must be exposable"); + } + + } + + static byte[] emptyBuffer = new byte[0]; + /// + /// Retreives the previously stored data on a Python capsule. + /// Throws if the object corresponding to the parameter + /// on the `sys` module object is not a capsule. + /// + /// The name given to the capsule on the `sys` module object + /// A MemoryStream containing the previously saved serialization data. + /// The stream is empty if no name matches the key. + public static MemoryStream GetSerializationData(string key) + { + BorrowedReference capsule = PySys_GetObject(key); + if (capsule.IsNull) + { + // nothing to do. + return new MemoryStream(emptyBuffer, writable:false); + } + var ptr = PyCapsule_GetPointer(capsule, IntPtr.Zero); + if (ptr == IntPtr.Zero) + { + // The PyCapsule API returns NULL on error; NULL cannot be stored + // as a capsule's value + PythonException.ThrowIfIsNull(null); + } + var len = (int)Marshal.ReadIntPtr(ptr); + byte[] buffer = new byte[len]; + Marshal.Copy(ptr+IntPtr.Size, buffer, 0, len); + return new MemoryStream(buffer, writable:false); + } + internal static IFormatter CreateFormatter() { - return FormatterType != null ? - (IFormatter)Activator.CreateInstance(FormatterType) - : new BinaryFormatter(); + + if (FormatterType != null) + { + return (IFormatter)Activator.CreateInstance(FormatterType); + } + return FormatterFactory(); } } } diff --git a/tests/domain_tests/TestRunner.cs b/tests/domain_tests/TestRunner.cs index 4f6a3ea28..bbee81b3d 100644 --- a/tests/domain_tests/TestRunner.cs +++ b/tests/domain_tests/TestRunner.cs @@ -1132,6 +1132,66 @@ import System ", }, + new TestCase + { + Name = "test_serialize_unserializable_object", + DotNetBefore = @" + namespace TestNamespace + { + public class NotSerializableTextWriter : System.IO.TextWriter + { + override public System.Text.Encoding Encoding { get { return System.Text.Encoding.ASCII;} } + } + [System.Serializable] + public static class SerializableWriter + { + private static System.IO.TextWriter _writer = null; + public static System.IO.TextWriter Writer {get { return _writer; }} + public static void CreateInternalWriter() + { + _writer = System.IO.TextWriter.Synchronized(new NotSerializableTextWriter()); + } + } + } +", + DotNetAfter = @" + namespace TestNamespace + { + public class NotSerializableTextWriter : System.IO.TextWriter + { + override public System.Text.Encoding Encoding { get { return System.Text.Encoding.ASCII;} } + } + [System.Serializable] + public static class SerializableWriter + { + private static System.IO.TextWriter _writer = null; + public static System.IO.TextWriter Writer {get { return _writer; }} + public static void CreateInternalWriter() + { + _writer = System.IO.TextWriter.Synchronized(new NotSerializableTextWriter()); + } + } + } + ", + PythonCode = @" +import sys + +def before_reload(): + import clr + import System + clr.AddReference('DomainTests') + import TestNamespace + TestNamespace.SerializableWriter.CreateInternalWriter(); + sys.__obj = TestNamespace.SerializableWriter.Writer + sys.__obj.WriteLine('test') + +def after_reload(): + import clr + import System + sys.__obj.WriteLine('test') + + ", + } }; /// @@ -1142,7 +1202,59 @@ import System const string CaseRunnerTemplate = @" using System; using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; using Python.Runtime; + +namespace Serialization +{{ + // Classes in this namespace is mostly useful for test_serialize_unserializable_object + class NotSerializableSerializer : ISerializationSurrogate + {{ + public NotSerializableSerializer() + {{ + }} + public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) + {{ + info.AddValue(""notSerialized_tp"", obj.GetType()); + }} + public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) + {{ + if (info == null) + {{ + return null; + }} + Type typeObj = info.GetValue(""notSerialized_tp"", typeof(Type)) as Type; + if (typeObj == null) + {{ + return null; + }} + + obj = Activator.CreateInstance(typeObj); + return obj; + }} + }} + class NonSerializableSelector : SurrogateSelector + {{ + public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector) + {{ + if (type == null) + {{ + throw new ArgumentNullException(); + }} + selector = (ISurrogateSelector)this; + if (type.IsSerializable) + {{ + return null; // use whichever default + }} + else + {{ + return (ISerializationSurrogate)(new NotSerializableSerializer()); + }} + }} + }} +}} + namespace CaseRunner {{ class CaseRunner @@ -1151,6 +1263,11 @@ public static int Main() {{ try {{ + RuntimeData.FormatterFactory = () => + {{ + return new BinaryFormatter(){{SurrogateSelector = new Serialization.NonSerializableSelector()}}; + }}; + PythonEngine.Initialize(); using (Py.GIL()) {{ diff --git a/tests/domain_tests/test_domain_reload.py b/tests/domain_tests/test_domain_reload.py index 8999e481b..1e5e8e81b 100644 --- a/tests/domain_tests/test_domain_reload.py +++ b/tests/domain_tests/test_domain_reload.py @@ -88,3 +88,6 @@ def test_nested_type(): def test_import_after_reload(): _run_test("import_after_reload") + +def test_import_after_reload(): + _run_test("test_serialize_unserializable_object") \ No newline at end of file From b112885d19091f0e5fe1e7609236d6093fbd5a0b Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 11 May 2024 00:34:26 -0700 Subject: [PATCH 14/77] handle bad paths in sys.path (#2383) fixes #2376 --- CHANGELOG.md | 1 + src/runtime/AssemblyManager.cs | 7 +++++++ src/runtime/Exceptions.cs | 4 ++-- tests/test_module.py | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adef224e0..23184258d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Fixed - Fixed RecursionError for reverse operators on C# operable types from python. See #2240 +- Fixed probing for assemblies in `sys.path` failing when a path in `sys.path` has invalid characters. See #2376 ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 diff --git a/src/runtime/AssemblyManager.cs b/src/runtime/AssemblyManager.cs index a8bbd1f6c..82658bf50 100644 --- a/src/runtime/AssemblyManager.cs +++ b/src/runtime/AssemblyManager.cs @@ -200,6 +200,13 @@ static IEnumerable FindAssemblyCandidates(string name) } else { + int invalidCharIndex = head.IndexOfAny(Path.GetInvalidPathChars()); + if (invalidCharIndex >= 0) + { + using var importWarning = Runtime.PyObject_GetAttrString(Exceptions.exceptions_module, "ImportWarning"); + Exceptions.warn($"Path entry '{head}' has invalid char at position {invalidCharIndex}", importWarning.BorrowOrThrow()); + continue; + } path = Path.Combine(head, name); } diff --git a/src/runtime/Exceptions.cs b/src/runtime/Exceptions.cs index da095e030..85e56eace 100644 --- a/src/runtime/Exceptions.cs +++ b/src/runtime/Exceptions.cs @@ -270,7 +270,7 @@ public static void warn(string message, BorrowedReference exception, int stackle } using var warn = Runtime.PyObject_GetAttrString(warnings_module.obj, "warn"); - Exceptions.ErrorCheck(warn.Borrow()); + warn.BorrowOrThrow(); using var argsTemp = Runtime.PyTuple_New(3); BorrowedReference args = argsTemp.BorrowOrThrow(); @@ -283,7 +283,7 @@ public static void warn(string message, BorrowedReference exception, int stackle Runtime.PyTuple_SetItem(args, 2, level.StealOrThrow()); using var result = Runtime.PyObject_CallObject(warn.Borrow(), args); - Exceptions.ErrorCheck(result.Borrow()); + result.BorrowOrThrow(); } public static void warn(string message, BorrowedReference exception) diff --git a/tests/test_module.py b/tests/test_module.py index ddfa7bb36..0c20dcfc0 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -344,6 +344,20 @@ def test_clr_add_reference(): with pytest.raises(FileNotFoundException): AddReference("somethingtotallysilly") + +def test_clr_add_reference_bad_path(): + import sys + from clr import AddReference + from System.IO import FileNotFoundException + bad_path = "hello\0world" + sys.path.append(bad_path) + try: + with pytest.raises(FileNotFoundException): + AddReference("test_clr_add_reference_bad_path") + finally: + sys.path.remove(bad_path) + + def test_clr_get_clr_type(): """Test clr.GetClrType().""" from clr import GetClrType From 4e5afdf973e29f1ae50413aa0cc092f2a03df68f Mon Sep 17 00:00:00 2001 From: Frank Witscher Date: Mon, 13 May 2024 16:25:11 +0200 Subject: [PATCH 15/77] Fix access violation exception on shutdown (#1977) When nulling the GC handles on shutdown the reference count of all objects pointed to by the IntPtr in the `CLRObject.reflectedObjects` are zero. This caused an exception in some scenarios because `Runtime.PyObject_TYPE(reflectedClrObject)` is called while the reference counter is at zero. After `TypeManager.RemoveTypes();` is called in the `Runtime.Shutdown()` method, reference count decrements to zero do not invoke `ClassBase.tp_clear` for managed objects anymore which normally is responsible for removing references from `CLRObject.reflectedObjects`. Collecting objects referenced in `CLRObject.reflectedObjects` only after leads to an unstable state in which the reference count for these object addresses is zero while still maintaining them to be used for further pseudo-cleanup. In that time, the memory could have been reclaimed already which leads to the exception. --- AUTHORS.md | 1 + CHANGELOG.md | 2 ++ src/runtime/Runtime.cs | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 18435671c..6aa4a6010 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -38,6 +38,7 @@ - Dmitriy Se ([@dmitriyse](https://github.com/dmitriyse)) - Félix Bourbonnais ([@BadSingleton](https://github.com/BadSingleton)) - Florian Treurniet ([@ftreurni](https://github.com/ftreurni)) +- Frank Witscher ([@Frawak](https://github.com/Frawak)) - He-chien Tsai ([@t3476](https://github.com/t3476)) - Inna Wiesel ([@inna-w](https://github.com/inna-w)) - Ivan Cronyn ([@cronan](https://github.com/cronan)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23184258d..7d2faa1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. - Fixed RecursionError for reverse operators on C# operable types from python. See #2240 - Fixed probing for assemblies in `sys.path` failing when a path in `sys.path` has invalid characters. See #2376 +- Fixed possible access violation exception on shutdown. See ([#1977][i1977]) ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 @@ -970,3 +971,4 @@ This version improves performance on benchmarks significantly compared to 2.3. [i1481]: https://github.com/pythonnet/pythonnet/issues/1481 [i1672]: https://github.com/pythonnet/pythonnet/pull/1672 [i2311]: https://github.com/pythonnet/pythonnet/issues/2311 +[i1977]: https://github.com/pythonnet/pythonnet/issues/1977 diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index 2f9e18f65..a65fea66f 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -278,6 +278,8 @@ internal static void Shutdown() ClearClrModules(); RemoveClrRootModule(); + TryCollectingGarbage(MaxCollectRetriesOnShutdown, forceBreakLoops: true); + NullGCHandles(ExtensionType.loadedExtensions); ClassManager.RemoveClasses(); TypeManager.RemoveTypes(); @@ -295,8 +297,7 @@ internal static void Shutdown() PyObjectConversions.Reset(); PyGC_Collect(); - bool everythingSeemsCollected = TryCollectingGarbage(MaxCollectRetriesOnShutdown, - forceBreakLoops: true); + bool everythingSeemsCollected = TryCollectingGarbage(MaxCollectRetriesOnShutdown); Debug.Assert(everythingSeemsCollected); Finalizer.Shutdown(); From 6f0f6713e8f55a24ea7803584c5490eca0518739 Mon Sep 17 00:00:00 2001 From: Frank Witscher Date: Mon, 13 May 2024 22:38:59 +0200 Subject: [PATCH 16/77] Restrict first garbage collection Otherwise, collecting all at this earlier point results in corrupt memory for derived types. --- src/runtime/Finalizer.cs | 8 ++++---- src/runtime/Runtime.cs | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/runtime/Finalizer.cs b/src/runtime/Finalizer.cs index 713564f08..5b5ecfcfc 100644 --- a/src/runtime/Finalizer.cs +++ b/src/runtime/Finalizer.cs @@ -191,7 +191,7 @@ internal static void Shutdown() Instance.started = false; } - internal nint DisposeAll() + internal nint DisposeAll(bool disposeObj = true, bool disposeDerived = true, bool disposeBuffer = true) { if (_objQueue.IsEmpty && _derivedQueue.IsEmpty && _bufferQueue.IsEmpty) return 0; @@ -216,7 +216,7 @@ internal nint DisposeAll() try { - while (!_objQueue.IsEmpty) + if (disposeObj) while (!_objQueue.IsEmpty) { if (!_objQueue.TryDequeue(out var obj)) continue; @@ -240,7 +240,7 @@ internal nint DisposeAll() } } - while (!_derivedQueue.IsEmpty) + if (disposeDerived) while (!_derivedQueue.IsEmpty) { if (!_derivedQueue.TryDequeue(out var derived)) continue; @@ -258,7 +258,7 @@ internal nint DisposeAll() collected++; } - while (!_bufferQueue.IsEmpty) + if (disposeBuffer) while (!_bufferQueue.IsEmpty) { if (!_bufferQueue.TryDequeue(out var buffer)) continue; diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index a65fea66f..b3820270c 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -278,7 +278,8 @@ internal static void Shutdown() ClearClrModules(); RemoveClrRootModule(); - TryCollectingGarbage(MaxCollectRetriesOnShutdown, forceBreakLoops: true); + TryCollectingGarbage(MaxCollectRetriesOnShutdown, forceBreakLoops: true, + obj: true, derived: false, buffer: false); NullGCHandles(ExtensionType.loadedExtensions); ClassManager.RemoveClasses(); @@ -329,7 +330,8 @@ internal static void Shutdown() const int MaxCollectRetriesOnShutdown = 20; internal static int _collected; - static bool TryCollectingGarbage(int runs, bool forceBreakLoops) + static bool TryCollectingGarbage(int runs, bool forceBreakLoops, + bool obj = true, bool derived = true, bool buffer = true) { if (runs <= 0) throw new ArgumentOutOfRangeException(nameof(runs)); @@ -342,7 +344,9 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops) GC.Collect(); GC.WaitForPendingFinalizers(); pyCollected += PyGC_Collect(); - pyCollected += Finalizer.Instance.DisposeAll(); + pyCollected += Finalizer.Instance.DisposeAll(disposeObj: obj, + disposeDerived: derived, + disposeBuffer: buffer); } if (Volatile.Read(ref _collected) == 0 && pyCollected == 0) { From f82aeea80cd463996d3ab376c94a57a8d2d7e774 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 9 Jun 2024 01:32:00 +0200 Subject: [PATCH 17/77] Simplify UTF8 StrPtr usage (#2374) * Use non-BOM encodings * Copy potential BOM to the output of PyString_FromString The documentation of the used `PyUnicode_DecodeUTF16` states that not passing `*byteorder` or passing a 0 results in the first two bytes, if they are the BOM (U+FEFF, zero-width no-break space), to be interpreted and skipped, which is incorrect when we convert a known "non BOM" string, which all strings from C# are. * Default to UTF8 for StrPtr --- src/embed_tests/TestPyType.cs | 3 +- src/runtime/Native/NativeTypeSpec.cs | 2 +- src/runtime/Native/StrPtr.cs | 2 ++ src/runtime/Runtime.cs | 43 ++++++++++++++-------------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/embed_tests/TestPyType.cs b/src/embed_tests/TestPyType.cs index 0470070c3..d98dfda2e 100644 --- a/src/embed_tests/TestPyType.cs +++ b/src/embed_tests/TestPyType.cs @@ -28,7 +28,8 @@ public void CanCreateHeapType() const string name = "nÁmæ"; const string docStr = "dÁcæ"; - using var doc = new StrPtr(docStr, Encodings.UTF8); + using var doc = new StrPtr(docStr); + var spec = new TypeSpec( name: name, basicSize: Util.ReadInt32(Runtime.Runtime.PyBaseObjectType, TypeOffset.tp_basicsize), diff --git a/src/runtime/Native/NativeTypeSpec.cs b/src/runtime/Native/NativeTypeSpec.cs index 50019a148..90e07afd7 100644 --- a/src/runtime/Native/NativeTypeSpec.cs +++ b/src/runtime/Native/NativeTypeSpec.cs @@ -17,7 +17,7 @@ public NativeTypeSpec(TypeSpec spec) { if (spec is null) throw new ArgumentNullException(nameof(spec)); - this.Name = new StrPtr(spec.Name, Encodings.UTF8); + this.Name = new StrPtr(spec.Name); this.BasicSize = spec.BasicSize; this.ItemSize = spec.ItemSize; this.Flags = (int)spec.Flags; diff --git a/src/runtime/Native/StrPtr.cs b/src/runtime/Native/StrPtr.cs index 4f73be9b5..c9f4db660 100644 --- a/src/runtime/Native/StrPtr.cs +++ b/src/runtime/Native/StrPtr.cs @@ -10,6 +10,8 @@ struct StrPtr : IDisposable public IntPtr RawPointer { get; set; } unsafe byte* Bytes => (byte*)this.RawPointer; + public unsafe StrPtr(string value) : this(value, Encodings.UTF8) {} + public unsafe StrPtr(string value, Encoding encoding) { if (value is null) throw new ArgumentNullException(nameof(value)); diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index 2f9e18f65..a26ad67a9 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -795,13 +795,13 @@ public static int Py_Main(int argc, string[] argv) internal static int PyRun_SimpleString(string code) { - using var codePtr = new StrPtr(code, Encodings.UTF8); + using var codePtr = new StrPtr(code); return Delegates.PyRun_SimpleStringFlags(codePtr, Utf8String); } internal static NewReference PyRun_String(string code, RunFlagType st, BorrowedReference globals, BorrowedReference locals) { - using var codePtr = new StrPtr(code, Encodings.UTF8); + using var codePtr = new StrPtr(code); return Delegates.PyRun_StringFlags(codePtr, st, globals, locals, Utf8String); } @@ -813,14 +813,15 @@ internal static NewReference PyRun_String(string code, RunFlagType st, BorrowedR /// internal static NewReference Py_CompileString(string str, string file, int start) { - using var strPtr = new StrPtr(str, Encodings.UTF8); + using var strPtr = new StrPtr(str); + using var fileObj = new PyString(file); return Delegates.Py_CompileStringObject(strPtr, fileObj, start, Utf8String, -1); } internal static NewReference PyImport_ExecCodeModule(string name, BorrowedReference code) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyImport_ExecCodeModule(namePtr, code); } @@ -867,13 +868,13 @@ internal static bool PyObject_IsIterable(BorrowedReference ob) internal static int PyObject_HasAttrString(BorrowedReference pointer, string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyObject_HasAttrString(pointer, namePtr); } internal static NewReference PyObject_GetAttrString(BorrowedReference pointer, string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyObject_GetAttrString(pointer, namePtr); } @@ -884,12 +885,12 @@ internal static NewReference PyObject_GetAttrString(BorrowedReference pointer, S internal static int PyObject_DelAttr(BorrowedReference @object, BorrowedReference name) => Delegates.PyObject_SetAttr(@object, name, null); internal static int PyObject_DelAttrString(BorrowedReference @object, string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyObject_SetAttrString(@object, namePtr, null); } internal static int PyObject_SetAttrString(BorrowedReference @object, string name, BorrowedReference value) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyObject_SetAttrString(@object, namePtr, value); } @@ -1071,7 +1072,7 @@ internal static bool PyBool_CheckExact(BorrowedReference ob) internal static NewReference PyLong_FromString(string value, int radix) { - using var valPtr = new StrPtr(value, Encodings.UTF8); + using var valPtr = new StrPtr(value); return Delegates.PyLong_FromString(valPtr, IntPtr.Zero, radix); } @@ -1274,7 +1275,7 @@ internal static NewReference EmptyPyBytes() internal static NewReference PyByteArray_FromStringAndSize(IntPtr strPtr, nint len) => Delegates.PyByteArray_FromStringAndSize(strPtr, len); internal static NewReference PyByteArray_FromStringAndSize(string s) { - using var ptr = new StrPtr(s, Encodings.UTF8); + using var ptr = new StrPtr(s); return PyByteArray_FromStringAndSize(ptr.RawPointer, checked((nint)ptr.ByteCount)); } @@ -1302,7 +1303,7 @@ internal static IntPtr PyBytes_AsString(BorrowedReference ob) internal static NewReference PyUnicode_InternFromString(string s) { - using var ptr = new StrPtr(s, Encodings.UTF8); + using var ptr = new StrPtr(s); return Delegates.PyUnicode_InternFromString(ptr); } @@ -1377,7 +1378,7 @@ internal static bool PyDict_Check(BorrowedReference ob) internal static BorrowedReference PyDict_GetItemString(BorrowedReference pointer, string key) { - using var keyStr = new StrPtr(key, Encodings.UTF8); + using var keyStr = new StrPtr(key); return Delegates.PyDict_GetItemString(pointer, keyStr); } @@ -1393,7 +1394,7 @@ internal static BorrowedReference PyDict_GetItemString(BorrowedReference pointer /// internal static int PyDict_SetItemString(BorrowedReference dict, string key, BorrowedReference value) { - using var keyPtr = new StrPtr(key, Encodings.UTF8); + using var keyPtr = new StrPtr(key); return Delegates.PyDict_SetItemString(dict, keyPtr, value); } @@ -1402,7 +1403,7 @@ internal static int PyDict_SetItemString(BorrowedReference dict, string key, Bor internal static int PyDict_DelItemString(BorrowedReference pointer, string key) { - using var keyPtr = new StrPtr(key, Encodings.UTF8); + using var keyPtr = new StrPtr(key); return Delegates.PyDict_DelItemString(pointer, keyPtr); } @@ -1517,7 +1518,7 @@ internal static bool PyIter_Check(BorrowedReference ob) internal static NewReference PyModule_New(string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyModule_New(namePtr); } @@ -1531,7 +1532,7 @@ internal static NewReference PyModule_New(string name) /// Return -1 on error, 0 on success. internal static int PyModule_AddObject(BorrowedReference module, string name, StolenReference value) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); IntPtr valueAddr = value.DangerousGetAddressOrNull(); int res = Delegates.PyModule_AddObject(module, namePtr, valueAddr); // We can't just exit here because the reference is stolen only on success. @@ -1549,7 +1550,7 @@ internal static int PyModule_AddObject(BorrowedReference module, string name, St internal static NewReference PyImport_ImportModule(string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyImport_ImportModule(namePtr); } @@ -1558,7 +1559,7 @@ internal static NewReference PyImport_ImportModule(string name) internal static BorrowedReference PyImport_AddModule(string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PyImport_AddModule(namePtr); } @@ -1586,13 +1587,13 @@ internal static void PySys_SetArgvEx(int argc, string[] argv, int updatepath) internal static BorrowedReference PySys_GetObject(string name) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PySys_GetObject(namePtr); } internal static int PySys_SetObject(string name, BorrowedReference ob) { - using var namePtr = new StrPtr(name, Encodings.UTF8); + using var namePtr = new StrPtr(name); return Delegates.PySys_SetObject(namePtr, ob); } @@ -1691,7 +1692,7 @@ internal static IntPtr PyMem_Malloc(long size) internal static void PyErr_SetString(BorrowedReference ob, string message) { - using var msgPtr = new StrPtr(message, Encodings.UTF8); + using var msgPtr = new StrPtr(message); Delegates.PyErr_SetString(ob, msgPtr); } From 9ebfbde35eef742e39b80d4fe2464bdece4e34be Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 1 Jul 2024 23:13:03 -0700 Subject: [PATCH 18/77] Fix crash when event does not have `Add` method and improve message for some other internal errors (#2409) * not all events have Add method fixes https://github.com/pythonnet/pythonnet/discussions/2405 * give users some idea of why we might be unable to reflect .NET types to Python for them * mentioned event Add method crash fix in changelog --- CHANGELOG.md | 1 + src/runtime/ClassManager.cs | 4 ++- src/runtime/InternalPythonnetException.cs | 9 +++++++ src/runtime/Types/ReflectedClrType.cs | 31 ++++++++++++++--------- 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 src/runtime/InternalPythonnetException.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 23184258d..829180f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Fixed - Fixed RecursionError for reverse operators on C# operable types from python. See #2240 +- Fixed crash when .NET event has no `AddMethod` - Fixed probing for assemblies in `sys.path` failing when a path in `sys.path` has invalid characters. See #2376 ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index ecb6055a8..d743bc006 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -290,11 +290,13 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p internal static bool ShouldBindMethod(MethodBase mb) { + if (mb is null) throw new ArgumentNullException(nameof(mb)); return (mb.IsPublic || mb.IsFamily || mb.IsFamilyOrAssembly); } internal static bool ShouldBindField(FieldInfo fi) { + if (fi is null) throw new ArgumentNullException(nameof(fi)); return (fi.IsPublic || fi.IsFamily || fi.IsFamilyOrAssembly); } @@ -326,7 +328,7 @@ internal static bool ShouldBindProperty(PropertyInfo pi) internal static bool ShouldBindEvent(EventInfo ei) { - return ShouldBindMethod(ei.GetAddMethod(true)); + return ei.GetAddMethod(true) is { } add && ShouldBindMethod(add); } private static ClassInfo GetClassInfo(Type type, ClassBase impl) diff --git a/src/runtime/InternalPythonnetException.cs b/src/runtime/InternalPythonnetException.cs new file mode 100644 index 000000000..d0ea1bece --- /dev/null +++ b/src/runtime/InternalPythonnetException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Python.Runtime; + +public class InternalPythonnetException : Exception +{ + public InternalPythonnetException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/src/runtime/Types/ReflectedClrType.cs b/src/runtime/Types/ReflectedClrType.cs index 3d0aa7e99..df9b26c29 100644 --- a/src/runtime/Types/ReflectedClrType.cs +++ b/src/runtime/Types/ReflectedClrType.cs @@ -30,22 +30,29 @@ public static ReflectedClrType GetOrCreate(Type type) return pyType; } - // Ensure, that matching Python type exists first. - // It is required for self-referential classes - // (e.g. with members, that refer to the same class) - pyType = AllocateClass(type); - ClassManager.cache.Add(type, pyType); + try + { + // Ensure, that matching Python type exists first. + // It is required for self-referential classes + // (e.g. with members, that refer to the same class) + pyType = AllocateClass(type); + ClassManager.cache.Add(type, pyType); - var impl = ClassManager.CreateClass(type); + var impl = ClassManager.CreateClass(type); - TypeManager.InitializeClassCore(type, pyType, impl); + TypeManager.InitializeClassCore(type, pyType, impl); - ClassManager.InitClassBase(type, impl, pyType); + ClassManager.InitClassBase(type, impl, pyType); - // Now we force initialize the Python type object to reflect the given - // managed type, filling the Python type slots with thunks that - // point to the managed methods providing the implementation. - TypeManager.InitializeClass(pyType, impl, type); + // Now we force initialize the Python type object to reflect the given + // managed type, filling the Python type slots with thunks that + // point to the managed methods providing the implementation. + TypeManager.InitializeClass(pyType, impl, type); + } + catch (Exception e) + { + throw new InternalPythonnetException($"Failed to create Python type for {type.FullName}", e); + } return pyType; } From c99cdf3efef20451b96417d9422e21b8bcbf2cf4 Mon Sep 17 00:00:00 2001 From: Frank Witscher Date: Mon, 8 Jul 2024 09:49:16 +0200 Subject: [PATCH 19/77] Throw exception trying to add a reflected object after the hashset is cleared --- src/runtime/Runtime.cs | 2 ++ src/runtime/Types/ClrObject.cs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index b3820270c..3b9b0ce48 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -158,6 +158,7 @@ internal static void Initialize(bool initSigs = false) ClassManager.Reset(); ClassDerivedObject.Reset(); TypeManager.Initialize(); + CLRObject.creationBlocked = false; _typesInitialized = true; // Initialize modules that depend on the runtime class. @@ -356,6 +357,7 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops, { NullGCHandles(CLRObject.reflectedObjects); CLRObject.reflectedObjects.Clear(); + CLRObject.creationBlocked = true; } } return false; diff --git a/src/runtime/Types/ClrObject.cs b/src/runtime/Types/ClrObject.cs index 4cf9062cb..afa136414 100644 --- a/src/runtime/Types/ClrObject.cs +++ b/src/runtime/Types/ClrObject.cs @@ -11,10 +11,15 @@ internal sealed class CLRObject : ManagedType { internal readonly object inst; + internal static bool creationBlocked = false; + // "borrowed" references internal static readonly HashSet reflectedObjects = new(); static NewReference Create(object ob, BorrowedReference tp) { + if (creationBlocked) + throw new InvalidOperationException("Reflected objects should not be created anymore."); + Debug.Assert(tp != null); var py = Runtime.PyType_GenericAlloc(tp, 0); @@ -61,6 +66,9 @@ internal static void Restore(object ob, BorrowedReference pyHandle, Dictionary? context) { + if (creationBlocked) + throw new InvalidOperationException("Reflected objects should not be loaded anymore."); + base.OnLoad(ob, context); GCHandle gc = GCHandle.Alloc(this); SetGCHandle(ob, gc); From 6cdd6d7d7b7c50781390c5e59978cdc90967ef97 Mon Sep 17 00:00:00 2001 From: Frank Witscher Date: Mon, 5 Aug 2024 09:46:07 +0200 Subject: [PATCH 20/77] Move reflected object creation block Otherwise if you have a Python object that needs to temporarily create a .NET object in its destructor (for instance write log summary to a file), its destructor will fail if it happens to be freed on the second iteration of loop breaking. --- src/runtime/Runtime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index 3b9b0ce48..4fa4f5957 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -281,6 +281,7 @@ internal static void Shutdown() TryCollectingGarbage(MaxCollectRetriesOnShutdown, forceBreakLoops: true, obj: true, derived: false, buffer: false); + CLRObject.creationBlocked = true; NullGCHandles(ExtensionType.loadedExtensions); ClassManager.RemoveClasses(); @@ -357,7 +358,6 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops, { NullGCHandles(CLRObject.reflectedObjects); CLRObject.reflectedObjects.Clear(); - CLRObject.creationBlocked = true; } } return false; From 6690310376b0b499be39bab59f15a54f31be196a Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 19 Sep 2024 19:21:33 +0200 Subject: [PATCH 21/77] Bump to 3.0.4 --- CHANGELOG.md | 20 ++++++++++++-------- version.txt | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1355bd692..66f66670e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,29 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. -## [Unreleased][] +## [3.0.4](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.4) - 2024-09-19 ### Added -- Added `ToPythonAs()` extension method to allow for explicit conversion using a specific type. ([#2311][i2311]) - -- Added `IComparable` and `IEquatable` implementations to `PyInt`, `PyFloat`, and `PyString` - to compare with primitive .NET types like `long`. +- Added `ToPythonAs()` extension method to allow for explicit conversion + using a specific type. ([#2311][i2311]) +- Added `IComparable` and `IEquatable` implementations to `PyInt`, `PyFloat`, + and `PyString` to compare with primitive .NET types like `long`. ### Changed -- Added a `FormatterFactory` member in RuntimeData to create formatters with parameters. For compatibility, the `FormatterType` member is still present and has precedence when defining both `FormatterFactory` and `FormatterType` -- Added a post-serialization and a pre-deserialization step callbacks to extend (de)serialization process +- Added a `FormatterFactory` member in RuntimeData to create formatters with + parameters. For compatibility, the `FormatterType` member is still present + and has precedence when defining both `FormatterFactory` and `FormatterType` +- Added a post-serialization and a pre-deserialization step callbacks to + extend (de)serialization process - Added an API to stash serialized data on Python capsules ### Fixed - Fixed RecursionError for reverse operators on C# operable types from python. See #2240 - Fixed crash when .NET event has no `AddMethod` -- Fixed probing for assemblies in `sys.path` failing when a path in `sys.path` has invalid characters. See #2376 +- Fixed probing for assemblies in `sys.path` failing when a path in `sys.path` + has invalid characters. See #2376 - Fixed possible access violation exception on shutdown. See ([#1977][i1977]) ## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 diff --git a/version.txt b/version.txt index 0f9d6b15d..b0f2dcb32 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.1.0-dev +3.0.4 From 5c50b206617aebb8960d40fff7395a657eed6857 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 19 Sep 2024 19:25:21 +0200 Subject: [PATCH 22/77] Back to dev --- CHANGELOG.md | 8 ++++++++ version.txt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f66670e..29b4c4a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. +## Unreleased + +### Added +### Changed +### Fixed + + ## [3.0.4](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.4) - 2024-09-19 ### Added @@ -15,6 +22,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. and `PyString` to compare with primitive .NET types like `long`. ### Changed + - Added a `FormatterFactory` member in RuntimeData to create formatters with parameters. For compatibility, the `FormatterType` member is still present and has precedence when defining both `FormatterFactory` and `FormatterType` diff --git a/version.txt b/version.txt index b0f2dcb32..0f9d6b15d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.0.4 +3.1.0-dev From de7a1d2d2aa505a4be5e4dee9368e514b6963bef Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 1 Oct 2024 12:10:08 +0200 Subject: [PATCH 23/77] Drop console application (#2456) --- pythonnet.sln | 2 - src/console/Console.csproj | 27 ------------ src/console/python-clear.ico | Bin 270398 -> 0 bytes src/console/python-clear.png | Bin 21008 -> 0 bytes src/console/pythonconsole.cs | 82 ----------------------------------- 5 files changed, 111 deletions(-) delete mode 100644 src/console/Console.csproj delete mode 100644 src/console/python-clear.ico delete mode 100644 src/console/python-clear.png delete mode 100644 src/console/pythonconsole.cs diff --git a/pythonnet.sln b/pythonnet.sln index d1a47892e..5bf4a2dbf 100644 --- a/pythonnet.sln +++ b/pythonnet.sln @@ -4,8 +4,6 @@ VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.Runtime", "src\runtime\Python.Runtime.csproj", "{4E8C8FE2-0FB8-4517-B2D9-5FB2D5FC849B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "src\console\Console.csproj", "{E6B01706-00BA-4144-9029-186AC42FBE9A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.EmbeddingTest", "src\embed_tests\Python.EmbeddingTest.csproj", "{819E089B-4770-400E-93C6-4F7A35F0EA12}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.Test", "src\testing\Python.Test.csproj", "{14EF9518-5BB7-4F83-8686-015BD2CC788E}" diff --git a/src/console/Console.csproj b/src/console/Console.csproj deleted file mode 100644 index bcbc1292b..000000000 --- a/src/console/Console.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - net472;net6.0 - x64;x86 - Exe - nPython - Python.Runtime - nPython - python-clear.ico - - - - - - Python.Runtime.dll - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - diff --git a/src/console/python-clear.ico b/src/console/python-clear.ico deleted file mode 100644 index b37050cce6b7bf79e540b30ed0c27beef0365794..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270398 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 D`G5c$ diff --git a/src/console/pythonconsole.cs b/src/console/pythonconsole.cs deleted file mode 100644 index bf17848f7..000000000 --- a/src/console/pythonconsole.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Python.Runtime; - -namespace Python.Runtime -{ - ///

- /// Example of Embedding Python inside of a .NET program. - /// - /// - /// It has similar functionality to doing `import clr` from within Python, but this does it - /// the other way around; That is, it loads Python inside of .NET program. - /// See https://github.com/pythonnet/pythonnet/issues/358 for more info. - /// - public sealed class PythonConsole - { -#if NET40 - private static AssemblyLoader assemblyLoader = new AssemblyLoader(); -#endif - private PythonConsole() - { - } - - [STAThread] - public static int Main(string[] args) - { - // Only .NET Framework is capable to safely inject python.runtime.dll into resources. -#if NET40 - // reference the static assemblyLoader to stop it being optimized away - AssemblyLoader a = assemblyLoader; -#endif - string[] cmd = Environment.GetCommandLineArgs(); - PythonEngine.Initialize(); - - int i = Runtime.Py_Main(cmd.Length, cmd); - PythonEngine.Shutdown(); - - return i; - } - -#if NET40 - // Register a callback function to load embedded assemblies. - // (Python.Runtime.dll is included as a resource) - private sealed class AssemblyLoader - { - private Dictionary loadedAssemblies; - - public AssemblyLoader() - { - loadedAssemblies = new Dictionary(); - - AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => - { - string shortName = args.Name.Split(',')[0]; - string resourceName = $"{shortName}.dll"; - - if (loadedAssemblies.ContainsKey(resourceName)) - { - return loadedAssemblies[resourceName]; - } - - // looks for the assembly from the resources and load it - using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) - { - if (stream != null) - { - var assemblyData = new byte[stream.Length]; - stream.Read(assemblyData, 0, assemblyData.Length); - Assembly assembly = Assembly.Load(assemblyData); - loadedAssemblies[resourceName] = assembly; - return assembly; - } - } - return null; - }; - } - } -#endif - } -} From 0ea1d29f912e9e4e1b43c4179df842ac2f3a0af2 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 8 Nov 2024 13:50:39 +0100 Subject: [PATCH 24/77] Drop reference to Py_Main (#2504) Was only used in the console and might not be available in some embedding scenarios. --- src/runtime/Runtime.Delegates.cs | 2 -- src/runtime/Runtime.cs | 14 -------------- 2 files changed, 16 deletions(-) diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs index 6490c3fe5..639ba4812 100644 --- a/src/runtime/Runtime.Delegates.cs +++ b/src/runtime/Runtime.Delegates.cs @@ -35,7 +35,6 @@ static Delegates() PyGILState_Ensure = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyGILState_Ensure), GetUnmanagedDll(_PythonDll)); PyGILState_Release = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyGILState_Release), GetUnmanagedDll(_PythonDll)); PyGILState_GetThisThreadState = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyGILState_GetThisThreadState), GetUnmanagedDll(_PythonDll)); - Py_Main = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_Main), GetUnmanagedDll(_PythonDll)); PyEval_InitThreads = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyEval_InitThreads), GetUnmanagedDll(_PythonDll)); PyEval_ThreadsInitialized = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyEval_ThreadsInitialized), GetUnmanagedDll(_PythonDll)); PyEval_AcquireLock = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyEval_AcquireLock), GetUnmanagedDll(_PythonDll)); @@ -319,7 +318,6 @@ static Delegates() internal static delegate* unmanaged[Cdecl] PyGILState_Ensure { get; } internal static delegate* unmanaged[Cdecl] PyGILState_Release { get; } internal static delegate* unmanaged[Cdecl] PyGILState_GetThisThreadState { get; } - internal static delegate* unmanaged[Cdecl] Py_Main { get; } internal static delegate* unmanaged[Cdecl] PyEval_InitThreads { get; } internal static delegate* unmanaged[Cdecl] PyEval_ThreadsInitialized { get; } internal static delegate* unmanaged[Cdecl] PyEval_AcquireLock { get; } diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index a33e20b4a..d769ebdc0 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -719,20 +719,6 @@ internal static T TryUsingDll(Func op) internal static PyThreadState* PyGILState_GetThisThreadState() => Delegates.PyGILState_GetThisThreadState(); - public static int Py_Main(int argc, string[] argv) - { - var marshaler = StrArrayMarshaler.GetInstance(null); - var argvPtr = marshaler.MarshalManagedToNative(argv); - try - { - return Delegates.Py_Main(argc, argvPtr); - } - finally - { - marshaler.CleanUpNativeData(argvPtr); - } - } - internal static void PyEval_InitThreads() => Delegates.PyEval_InitThreads(); From 7a9e3bf5ffb6cffebed2832ce4dfc7cd3b688247 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 11 Dec 2024 18:12:04 +0100 Subject: [PATCH 25/77] Stick to ubuntu-22.04 for now as that image still contains Mono --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3396b83cc..f6970d85b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: - category: ubuntu platform: x64 - instance: ubuntu-latest + instance: ubuntu-22.04 - category: macos platform: x64 From 5cf2534a07ab8cc2041997574138bedee4a21ade Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 19 Sep 2024 19:10:46 +0200 Subject: [PATCH 26/77] First attempt at Python 3.13 --- src/runtime/Native/TypeOffset313.cs | 146 ++++++++++++++++++++++++++++ src/runtime/Runtime.Delegates.cs | 9 +- 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/runtime/Native/TypeOffset313.cs diff --git a/src/runtime/Native/TypeOffset313.cs b/src/runtime/Native/TypeOffset313.cs new file mode 100644 index 000000000..20a67fbff --- /dev/null +++ b/src/runtime/Native/TypeOffset313.cs @@ -0,0 +1,146 @@ + +// Auto-generated by geninterop.py. +// DO NOT MODIFY BY HAND. + +// Python 3.13: ABI flags: '' + +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +using Python.Runtime.Native; + +namespace Python.Runtime +{ + [SuppressMessage("Style", "IDE1006:Naming Styles", + Justification = "Following CPython", + Scope = "type")] + + [StructLayout(LayoutKind.Sequential)] + internal class TypeOffset313 : GeneratedTypeOffsets, ITypeOffsets + { + public TypeOffset313() { } + // Auto-generated from PyHeapTypeObject in Python.h + public int ob_refcnt { get; private set; } + public int ob_type { get; private set; } + public int ob_size { get; private set; } + public int tp_name { get; private set; } + public int tp_basicsize { get; private set; } + public int tp_itemsize { get; private set; } + public int tp_dealloc { get; private set; } + public int tp_vectorcall_offset { get; private set; } + public int tp_getattr { get; private set; } + public int tp_setattr { get; private set; } + public int tp_as_async { get; private set; } + public int tp_repr { get; private set; } + public int tp_as_number { get; private set; } + public int tp_as_sequence { get; private set; } + public int tp_as_mapping { get; private set; } + public int tp_hash { get; private set; } + public int tp_call { get; private set; } + public int tp_str { get; private set; } + public int tp_getattro { get; private set; } + public int tp_setattro { get; private set; } + public int tp_as_buffer { get; private set; } + public int tp_flags { get; private set; } + public int tp_doc { get; private set; } + public int tp_traverse { get; private set; } + public int tp_clear { get; private set; } + public int tp_richcompare { get; private set; } + public int tp_weaklistoffset { get; private set; } + public int tp_iter { get; private set; } + public int tp_iternext { get; private set; } + public int tp_methods { get; private set; } + public int tp_members { get; private set; } + public int tp_getset { get; private set; } + public int tp_base { get; private set; } + public int tp_dict { get; private set; } + public int tp_descr_get { get; private set; } + public int tp_descr_set { get; private set; } + public int tp_dictoffset { get; private set; } + public int tp_init { get; private set; } + public int tp_alloc { get; private set; } + public int tp_new { get; private set; } + public int tp_free { get; private set; } + public int tp_is_gc { get; private set; } + public int tp_bases { get; private set; } + public int tp_mro { get; private set; } + public int tp_cache { get; private set; } + public int tp_subclasses { get; private set; } + public int tp_weaklist { get; private set; } + public int tp_del { get; private set; } + public int tp_version_tag { get; private set; } + public int tp_finalize { get; private set; } + public int tp_vectorcall { get; private set; } + public int tp_watched { get; private set; } + public int tp_versions_used { get; private set; } + public int am_await { get; private set; } + public int am_aiter { get; private set; } + public int am_anext { get; private set; } + public int am_send { get; private set; } + public int nb_add { get; private set; } + public int nb_subtract { get; private set; } + public int nb_multiply { get; private set; } + public int nb_remainder { get; private set; } + public int nb_divmod { get; private set; } + public int nb_power { get; private set; } + public int nb_negative { get; private set; } + public int nb_positive { get; private set; } + public int nb_absolute { get; private set; } + public int nb_bool { get; private set; } + public int nb_invert { get; private set; } + public int nb_lshift { get; private set; } + public int nb_rshift { get; private set; } + public int nb_and { get; private set; } + public int nb_xor { get; private set; } + public int nb_or { get; private set; } + public int nb_int { get; private set; } + public int nb_reserved { get; private set; } + public int nb_float { get; private set; } + public int nb_inplace_add { get; private set; } + public int nb_inplace_subtract { get; private set; } + public int nb_inplace_multiply { get; private set; } + public int nb_inplace_remainder { get; private set; } + public int nb_inplace_power { get; private set; } + public int nb_inplace_lshift { get; private set; } + public int nb_inplace_rshift { get; private set; } + public int nb_inplace_and { get; private set; } + public int nb_inplace_xor { get; private set; } + public int nb_inplace_or { get; private set; } + public int nb_floor_divide { get; private set; } + public int nb_true_divide { get; private set; } + public int nb_inplace_floor_divide { get; private set; } + public int nb_inplace_true_divide { get; private set; } + public int nb_index { get; private set; } + public int nb_matrix_multiply { get; private set; } + public int nb_inplace_matrix_multiply { get; private set; } + public int mp_length { get; private set; } + public int mp_subscript { get; private set; } + public int mp_ass_subscript { get; private set; } + public int sq_length { get; private set; } + public int sq_concat { get; private set; } + public int sq_repeat { get; private set; } + public int sq_item { get; private set; } + public int was_sq_slice { get; private set; } + public int sq_ass_item { get; private set; } + public int was_sq_ass_slice { get; private set; } + public int sq_contains { get; private set; } + public int sq_inplace_concat { get; private set; } + public int sq_inplace_repeat { get; private set; } + public int bf_getbuffer { get; private set; } + public int bf_releasebuffer { get; private set; } + public int name { get; private set; } + public int ht_slots { get; private set; } + public int qualname { get; private set; } + public int ht_cached_keys { get; private set; } + public int ht_module { get; private set; } + public int _ht_tpname { get; private set; } + public int spec_cache_getitem { get; private set; } + public int getitem_version { get; private set; } + public int init { get; private set; } + } +} + diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs index 639ba4812..78bad2739 100644 --- a/src/runtime/Runtime.Delegates.cs +++ b/src/runtime/Runtime.Delegates.cs @@ -23,7 +23,14 @@ static Delegates() Py_EndInterpreter = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_EndInterpreter), GetUnmanagedDll(_PythonDll)); PyThreadState_New = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_New), GetUnmanagedDll(_PythonDll)); PyThreadState_Get = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_Get), GetUnmanagedDll(_PythonDll)); - _PyThreadState_UncheckedGet = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(_PyThreadState_UncheckedGet), GetUnmanagedDll(_PythonDll)); + try + { + _PyThreadState_UncheckedGet = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(_PyThreadState_UncheckedGet), GetUnmanagedDll(_PythonDll)); + } + catch (MissingMethodException) + { + // Not supported in Python 3.13 anymore + } try { PyGILState_Check = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyGILState_Check), GetUnmanagedDll(_PythonDll)); From 2f3a0e79b01a30e729ec22bc50dcf0139b9424ae Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 25 Oct 2024 12:11:51 +0200 Subject: [PATCH 27/77] Add Python 3.13 to CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6970d85b..ac603f32d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: platform: x64 instance: macos-13 - python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Set Environment on macOS From 5a7b5be4516bd08990e6f80211af1759d2d4d3b7 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 25 Oct 2024 12:14:29 +0200 Subject: [PATCH 28/77] Update project file --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ece5f3a4..ab2693a38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "clr_loader>=0.2.6,<0.3.0" ] -requires-python = ">=3.7, <3.13" +requires-python = ">=3.7, <3.14" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -27,6 +27,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", From f3face061ac4432762fe707081c3e437b1f42d7d Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 11 Dec 2024 17:54:54 +0100 Subject: [PATCH 29/77] Use PyThreadState_GetUnchecked on Python 3.13 --- src/runtime/Runtime.Delegates.cs | 9 ++++++--- src/runtime/Runtime.cs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs index 78bad2739..262dc1e19 100644 --- a/src/runtime/Runtime.Delegates.cs +++ b/src/runtime/Runtime.Delegates.cs @@ -25,11 +25,14 @@ static Delegates() PyThreadState_Get = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_Get), GetUnmanagedDll(_PythonDll)); try { - _PyThreadState_UncheckedGet = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(_PyThreadState_UncheckedGet), GetUnmanagedDll(_PythonDll)); + // Up until Python 3.13, this function was private and named + // slightly differently. + PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName("_PyThreadState_UncheckedGet", GetUnmanagedDll(_PythonDll)); } catch (MissingMethodException) { - // Not supported in Python 3.13 anymore + + PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_GetUnchecked), GetUnmanagedDll(_PythonDll)); } try { @@ -320,7 +323,7 @@ static Delegates() internal static delegate* unmanaged[Cdecl] Py_EndInterpreter { get; } internal static delegate* unmanaged[Cdecl] PyThreadState_New { get; } internal static delegate* unmanaged[Cdecl] PyThreadState_Get { get; } - internal static delegate* unmanaged[Cdecl] _PyThreadState_UncheckedGet { get; } + internal static delegate* unmanaged[Cdecl] PyThreadState_GetUnchecked { get; } internal static delegate* unmanaged[Cdecl] PyGILState_Check { get; } internal static delegate* unmanaged[Cdecl] PyGILState_Ensure { get; } internal static delegate* unmanaged[Cdecl] PyGILState_Release { get; } diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index d769ebdc0..c8f022860 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -316,7 +316,7 @@ internal static void Shutdown() // Then release the GIL for good, if there is somehting to release // Use the unchecked version as the checked version calls `abort()` // if the current state is NULL. - if (_PyThreadState_UncheckedGet() != (PyThreadState*)0) + if (PyThreadState_GetUnchecked() != (PyThreadState*)0) { PyEval_SaveThread(); } @@ -705,7 +705,7 @@ internal static T TryUsingDll(Func op) internal static PyThreadState* PyThreadState_Get() => Delegates.PyThreadState_Get(); - internal static PyThreadState* _PyThreadState_UncheckedGet() => Delegates._PyThreadState_UncheckedGet(); + internal static PyThreadState* PyThreadState_GetUnchecked() => Delegates.PyThreadState_GetUnchecked(); internal static int PyGILState_Check() => Delegates.PyGILState_Check(); From 88d98f2f1a69a4658a8a5053f86015701cde8227 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 20:28:02 +0100 Subject: [PATCH 30/77] Workaround for geninterop failure to handle non-pointer fields --- src/runtime/Native/TypeOffset313.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/runtime/Native/TypeOffset313.cs b/src/runtime/Native/TypeOffset313.cs index 20a67fbff..4c2e71295 100644 --- a/src/runtime/Native/TypeOffset313.cs +++ b/src/runtime/Native/TypeOffset313.cs @@ -75,8 +75,14 @@ public TypeOffset313() { } public int tp_version_tag { get; private set; } public int tp_finalize { get; private set; } public int tp_vectorcall { get; private set; } + // This is an error in our generator: + // + // The fields below are actually not pointers (like we incorrectly + // assume for all other fields) but instead a char (1 byte) and a short + // (2 bytes). By dropping one of the fields, we still get the correct + // overall size of the struct. public int tp_watched { get; private set; } - public int tp_versions_used { get; private set; } + // public int tp_versions_used { get; private set; } public int am_await { get; private set; } public int am_aiter { get; private set; } public int am_anext { get; private set; } From 4bb673f960ce1f9aa77157dda4f2a83cb3476eb8 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:30:20 +0100 Subject: [PATCH 31/77] Bump clr-loader and dev dependencies --- pyproject.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ab2693a38..31432f04c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ license = {text = "MIT"} readme = "README.rst" dependencies = [ - "clr_loader>=0.2.6,<0.3.0" + "clr_loader>=0.2.7,<0.3.0" ] requires-python = ">=3.7, <3.14" @@ -35,6 +35,15 @@ classifiers = [ dynamic = ["version"] +[dependency-groups] +dev = [ + "pytest >= 6", + "find_libpython >= 0.3.0", + "numpy >=2 ; python_version >= '3.10'", + "numpy <2 ; python_version < '3.10'", + "psutil" +] + [[project.authors]] name = "The Contributors of the Python.NET Project" email = "pythonnet@python.org" From 828362748ee097bddfe292e46ec6f748e7c3ecd6 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:30:32 +0100 Subject: [PATCH 32/77] Raise maximum supported version --- src/runtime/PythonEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/PythonEngine.cs b/src/runtime/PythonEngine.cs index 2c4c6c088..0b28c3a35 100644 --- a/src/runtime/PythonEngine.cs +++ b/src/runtime/PythonEngine.cs @@ -135,7 +135,7 @@ public static string PythonPath } public static Version MinSupportedVersion => new(3, 7); - public static Version MaxSupportedVersion => new(3, 12, int.MaxValue, int.MaxValue); + public static Version MaxSupportedVersion => new(3, 13, int.MaxValue, int.MaxValue); public static bool IsSupportedVersion(Version version) => version >= MinSupportedVersion && version <= MaxSupportedVersion; public static string Version From 1920b19749082c6b8c57fd77164f2fbe2d8d775c Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:30:53 +0100 Subject: [PATCH 33/77] Xfail a test that only fails locally on my machine right now --- tests/test_method.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_method.py b/tests/test_method.py index b86bbd6b4..c70200c7e 100644 --- a/tests/test_method.py +++ b/tests/test_method.py @@ -1023,6 +1023,7 @@ def test_getting_method_overloads_binding_does_not_leak_ref_count(): refCount = sys.getrefcount(PlainOldClass().OverloadedMethod.Overloads) assert refCount == 1 +@pytest.mark.xfail(reason="Fails locally, need to investigate later", strict=False) def test_getting_method_overloads_binding_does_not_leak_memory(): """Test that managed object is freed after calling overloaded method. Issue #691""" From 60a057f36402578a69403d0e195925468a50c87b Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:06:16 +0100 Subject: [PATCH 34/77] Skip embed tests on Python 3.13 for now Verified them locally, but there is an issue with the Github workflow image that can hopefully be resolved later by using a full venv instead of relying on the system environment. --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac603f32d..d21a761c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,6 +77,7 @@ jobs: Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')" - name: Embedding tests + if: ${{ matrix.python != '3.13' }} run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ env: MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 @@ -95,6 +96,7 @@ jobs: run: pytest --runtime netfx - name: Python tests run from .NET + if: ${{ matrix.python != '3.13' }} run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ - name: Perf tests From 8f0ccb75b8c5239d995e4f84f528c0dbdbd316f8 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:16:02 +0100 Subject: [PATCH 35/77] Add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b4c4a68..e4332cded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ## Unreleased ### Added + +- Support for Python 3.13 (#2454) + ### Changed ### Fixed From 7c87fec879573089e62831230878b7216320ebc0 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:16:12 +0100 Subject: [PATCH 36/77] Workaround for setuptools bug #4759 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 31432f04c..968998e8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ Sources = "https://github.com/pythonnet/pythonnet" [tool.setuptools] zip-safe = false py-modules = ["clr"] +license-files = [] [tool.setuptools.dynamic.version] file = "version.txt" From 0ea8e6f479426c5e6eb38e00843c34fa3153a6db Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:23:50 +0100 Subject: [PATCH 37/77] Remove win-x86-py3.13 entirely for now --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d21a761c6..f7e3eaa4e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,12 @@ jobs: python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + # This fails in pytest with: + # CSC : error CS4023: /platform:anycpu32bitpreferred can only be used with /t:exe, /t:winexe and /t:appcontainerexe [D:\a\pythonnet\pythonnet\src\runtime\Python.Runtime.csproj] + exclude: + - os: { category: windows, platform: x86 } + python: ["3.13"] + steps: - name: Set Environment on macOS uses: maxim-lobanov/setup-xamarin@v1 From 0826fc0bffb9aa6f91abae37f05e4b259c0ca94f Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:29:39 +0100 Subject: [PATCH 38/77] Release 3.0.5 --- CHANGELOG.md | 5 +---- version.txt | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4332cded..a983b4ea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,12 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. -## Unreleased +## [3.0.5](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.5) - 2024-12-13 ### Added - Support for Python 3.13 (#2454) -### Changed -### Fixed - ## [3.0.4](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.4) - 2024-09-19 diff --git a/version.txt b/version.txt index 0f9d6b15d..eca690e73 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.1.0-dev +3.0.5 From f361fa9a04ff578fecf0137145438c300cf98689 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:31:38 +0100 Subject: [PATCH 39/77] Back to dev --- CHANGELOG.md | 6 ++++++ version.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a983b4ea1..078a6ad6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. +## Unreleased + +### Added +### Changed +### Fixed + ## [3.0.5](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.5) - 2024-12-13 ### Added diff --git a/version.txt b/version.txt index eca690e73..0f9d6b15d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.0.5 +3.1.0-dev From dfd746b339dc24d530f7d64768e1c8acf9d84cb4 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 13 Dec 2024 09:33:25 +0100 Subject: [PATCH 40/77] Drop icon --- src/runtime/Python.Runtime.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 5072f23cd..0a7f58a3c 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -12,8 +12,6 @@ https://github.com/pythonnet/pythonnet git python interop dynamic dlr Mono pinvoke - python-clear.png - https://raw.githubusercontent.com/pythonnet/pythonnet/master/src/console/python-clear.ico https://pythonnet.github.io/ README.md true @@ -48,7 +46,6 @@ - From 030a9f9916563babc5055ffeb9dc85d44831ce74 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:29:11 +0100 Subject: [PATCH 41/77] ci: properly exclude job (#2542) --- .github/workflows/main.yml | 6 ++++-- AUTHORS.md | 1 + CHANGELOG.md | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7e3eaa4e..8485189e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,8 +37,10 @@ jobs: # This fails in pytest with: # CSC : error CS4023: /platform:anycpu32bitpreferred can only be used with /t:exe, /t:winexe and /t:appcontainerexe [D:\a\pythonnet\pythonnet\src\runtime\Python.Runtime.csproj] exclude: - - os: { category: windows, platform: x86 } - python: ["3.13"] + - os: + category: windows + platform: x86 + python: "3.13" steps: - name: Set Environment on macOS diff --git a/AUTHORS.md b/AUTHORS.md index 6aa4a6010..7ea639059 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -59,6 +59,7 @@ - Peter Kese ([@pkese](https://github.com/pkese)) - Raphael Nestler ([@rnestler](https://github.com/rnestler)) - Rickard Holmberg ([@rickardraysearch](https://github.com/rickardraysearch)) +- Roberto Pastor Muela ([@RobPasMue](https://github.com/RobPasMue)) - Sam Winstanley ([@swinstanley](https://github.com/swinstanley)) - Sean Freitag ([@cowboygneox](https://github.com/cowboygneox)) - Serge Weinstock ([@sweinst](https://github.com/sweinst)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078a6ad6e..1863a0806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Changed ### Fixed +- ci: properly exclude job (#2542) + ## [3.0.5](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.5) - 2024-12-13 ### Added From a6746588785516f0f470d01ebbcd14267ad0c17c Mon Sep 17 00:00:00 2001 From: Victor Nova Date: Wed, 18 Dec 2024 21:11:06 +0000 Subject: [PATCH 42/77] added NixOS FHS shell.nix --- pythonnet.sln | 1 + shell.nix | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 shell.nix diff --git a/pythonnet.sln b/pythonnet.sln index 5bf4a2dbf..cf684c0e1 100644 --- a/pythonnet.sln +++ b/pythonnet.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repo", "Repo", "{441A0123-F LICENSE = LICENSE README.rst = README.rst version.txt = version.txt + shell.nix = shell.nix EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CI", "CI", "{D301657F-5EAF-4534-B280-B858D651B2E5}" diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..fe653deb7 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {}}: +let + fhs = pkgs.buildFHSUserEnv { + name = "my-fhs-environment"; + + targetPkgs = _: [ + pkgs.python3 + ]; + }; +in fhs.env From ac605fd3e3ccad28c105ef2d57b955a23f51c2a9 Mon Sep 17 00:00:00 2001 From: Victor Nova Date: Wed, 18 Dec 2024 21:13:13 +0000 Subject: [PATCH 43/77] index setter was leaking memory --- src/runtime/Types/ClassBase.cs | 10 ++-------- src/runtime/Types/Indexer.cs | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index 7296a1900..8b2a98903 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -497,14 +497,8 @@ static int mp_ass_subscript_impl(BorrowedReference ob, BorrowedReference idx, Bo // Add value to argument list Runtime.PyTuple_SetItem(real.Borrow(), i, v); - cls.indexer.SetItem(ob, real.Borrow()); - - if (Exceptions.ErrorOccurred()) - { - return -1; - } - - return 0; + using var result = cls.indexer.SetItem(ob, real.Borrow()); + return result.IsNull() ? -1 : 0; } static NewReference tp_call_impl(BorrowedReference ob, BorrowedReference args, BorrowedReference kw) diff --git a/src/runtime/Types/Indexer.cs b/src/runtime/Types/Indexer.cs index 4903b6f76..fe6dab4c9 100644 --- a/src/runtime/Types/Indexer.cs +++ b/src/runtime/Types/Indexer.cs @@ -50,9 +50,9 @@ internal NewReference GetItem(BorrowedReference inst, BorrowedReference args) } - internal void SetItem(BorrowedReference inst, BorrowedReference args) + internal NewReference SetItem(BorrowedReference inst, BorrowedReference args) { - SetterBinder.Invoke(inst, args, null); + return SetterBinder.Invoke(inst, args, null); } internal bool NeedsDefaultArgs(BorrowedReference args) From 77bdf6d2bc5b7b6697d51f1b4ed765b01c322e59 Mon Sep 17 00:00:00 2001 From: Victor Nova Date: Wed, 18 Dec 2024 21:14:40 +0000 Subject: [PATCH 44/77] implemented __delitem__ for IDictionary and IList fixed crash for all other types (now properly throws TypeError) fixes https://github.com/pythonnet/pythonnet/issues/2530 --- CHANGELOG.md | 4 +++ src/runtime/ClassManager.cs | 17 ++++++++++++ src/runtime/MethodBinder.cs | 5 ++++ src/runtime/Types/ArrayObject.cs | 6 ++++ src/runtime/Types/ClassBase.cs | 44 ++++++++++++++++++++++++++++++ src/runtime/Util/ReflectionUtil.cs | 7 +++++ tests/test_indexer.py | 36 ++++++++++++++++++++++++ 7 files changed, 119 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1863a0806..df68fbb39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,13 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ## Unreleased ### Added + +- Support `del obj[...]` for types derived from `IList` and `IDictionary` + ### Changed ### Fixed +- Fixed crash when trying to `del clrObj[...]` for non-arrays - ci: properly exclude job (#2542) ## [3.0.5](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.5) - 2024-12-13 diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index d743bc006..b884bfa92 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -213,6 +213,7 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p ClassInfo info = GetClassInfo(type, impl); impl.indexer = info.indexer; + impl.del = info.del; impl.richcompare.Clear(); @@ -538,6 +539,21 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ob = new MethodObject(type, name, mlist); ci.members[name] = ob.AllocObject(); + if (name == nameof(IDictionary.Remove) + && mlist.Any(m => m.DeclaringType?.GetInterfaces() + .Any(i => i.TryGetGenericDefinition() == typeof(IDictionary<,>)) is true)) + { + ci.del = new(); + ci.del.AddRange(mlist.Where(m => !m.IsStatic)); + } + else if (name == nameof(IList.RemoveAt) + && mlist.Any(m => m.DeclaringType?.GetInterfaces() + .Any(i => i.TryGetGenericDefinition() == typeof(IList<>)) is true)) + { + ci.del = new(); + ci.del.AddRange(mlist.Where(m => !m.IsStatic)); + } + if (mlist.Any(OperatorMethod.IsOperatorMethod)) { string pyName = OperatorMethod.GetPyMethodName(name); @@ -581,6 +597,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) private class ClassInfo { public Indexer? indexer; + public MethodBinder? del; public readonly Dictionary members = new(); internal ClassInfo() diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 9a5515c8e..af75a34b4 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -54,6 +54,11 @@ internal void AddMethod(MethodBase m) list.Add(m); } + internal void AddRange(IEnumerable methods) + { + list.AddRange(methods.Select(m => new MaybeMethodBase(m))); + } + /// /// Given a sequence of MethodInfo and a sequence of types, return the /// MethodInfo that matches the signature represented by those types. diff --git a/src/runtime/Types/ArrayObject.cs b/src/runtime/Types/ArrayObject.cs index b95934baf..f220d53fb 100644 --- a/src/runtime/Types/ArrayObject.cs +++ b/src/runtime/Types/ArrayObject.cs @@ -247,6 +247,12 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference /// public static int mp_ass_subscript(BorrowedReference ob, BorrowedReference idx, BorrowedReference v) { + if (v.IsNull) + { + Exceptions.RaiseTypeError("'System.Array' object does not support item deletion"); + return -1; + } + var obj = (CLRObject)GetManagedObject(ob)!; var items = (Array)obj.inst; Type itemType = obj.inst.GetType().GetElementType(); diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index 8b2a98903..2d6ce8a47 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -25,6 +25,7 @@ internal class ClassBase : ManagedType, IDeserializationCallback [NonSerialized] internal List dotNetMembers = new(); internal Indexer? indexer; + internal MethodBinder? del; internal readonly Dictionary richcompare = new(); internal MaybeType type; @@ -465,6 +466,11 @@ static int mp_ass_subscript_impl(BorrowedReference ob, BorrowedReference idx, Bo // with the index arg (method binders expect arg tuples). NewReference argsTuple = default; + if (v.IsNull) + { + return DelImpl(ob, idx, cls); + } + if (!Runtime.PyTuple_Check(idx)) { argsTuple = Runtime.PyTuple_New(1); @@ -501,6 +507,44 @@ static int mp_ass_subscript_impl(BorrowedReference ob, BorrowedReference idx, Bo return result.IsNull() ? -1 : 0; } + /// Implements __delitem__ (del x[...]) for IList<T> and IDictionary<TKey, TValue>. + private static int DelImpl(BorrowedReference ob, BorrowedReference idx, ClassBase cls) + { + if (cls.del is null) + { + Exceptions.SetError(Exceptions.TypeError, "object does not support item deletion"); + return -1; + } + + if (Runtime.PyTuple_Check(idx)) + { + Exceptions.SetError(Exceptions.TypeError, "multi-index deletion not supported"); + return -1; + } + + using var argsTuple = Runtime.PyTuple_New(1); + Runtime.PyTuple_SetItem(argsTuple.Borrow(), 0, idx); + using var result = cls.del.Invoke(ob, argsTuple.Borrow(), kw: null); + if (result.IsNull()) + return -1; + + if (Runtime.PyBool_CheckExact(result.Borrow())) + { + if (Runtime.PyObject_IsTrue(result.Borrow()) != 0) + return 0; + + Exceptions.SetError(Exceptions.KeyError, "key not found"); + return -1; + } + + if (!result.IsNone()) + { + Exceptions.warn("unsupported return type for __delitem__", Exceptions.TypeError); + } + + return 0; + } + static NewReference tp_call_impl(BorrowedReference ob, BorrowedReference args, BorrowedReference kw) { BorrowedReference tp = Runtime.PyObject_TYPE(ob); diff --git a/src/runtime/Util/ReflectionUtil.cs b/src/runtime/Util/ReflectionUtil.cs index 58d0a506e..0fad2d4b2 100644 --- a/src/runtime/Util/ReflectionUtil.cs +++ b/src/runtime/Util/ReflectionUtil.cs @@ -53,4 +53,11 @@ public static BindingFlags GetBindingFlags(this PropertyInfo property) flags |= accessor.IsPublic ? BindingFlags.Public : BindingFlags.NonPublic; return flags; } + + public static Type? TryGetGenericDefinition(this Type type) + { + if (type is null) throw new ArgumentNullException(nameof(type)); + + return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; + } } diff --git a/tests/test_indexer.py b/tests/test_indexer.py index 8cf3150ba..108573f0d 100644 --- a/tests/test_indexer.py +++ b/tests/test_indexer.py @@ -668,3 +668,39 @@ def test_public_inherited_overloaded_indexer(): with pytest.raises(TypeError): ob[[]] + +def test_del_indexer_dict(): + """Test deleting indexers (__delitem__).""" + from System.Collections.Generic import Dictionary, KeyNotFoundException + d = Dictionary[str, str]() + d["delme"] = "1" + with pytest.raises(KeyError): + del d["nonexistent"] + del d["delme"] + with pytest.raises(KeyError): + del d["delme"] + +def test_del_indexer_list(): + """Test deleting indexers (__delitem__).""" + from System import ArgumentOutOfRangeException + from System.Collections.Generic import List + l = List[str]() + l.Add("1") + with pytest.raises(ArgumentOutOfRangeException): + del l[3] + del l[0] + assert len(l) == 0 + +def test_del_indexer_array(): + """Test deleting indexers (__delitem__).""" + from System import Array + l = Array[str](0) + with pytest.raises(TypeError): + del l[0] + +def test_del_indexer_absent(): + """Test deleting indexers (__delitem__).""" + from System import Uri + l = Uri("http://www.example.com") + with pytest.raises(TypeError): + del l[0] From a21c7972809500edd6364003c37695193ac1a5d4 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 14 Apr 2025 20:12:49 +0200 Subject: [PATCH 45/77] Bump action versions in doc workflow (#2584) --- .github/workflows/docs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5b782c8b4..30163cd14 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,9 +6,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Doxygen Action - uses: mattnotmitt/doxygen-action@1.9.4 + uses: mattnotmitt/doxygen-action@1.12.0 with: working-directory: "doc/" @@ -19,7 +19,7 @@ jobs: - name: Upload artifact # Automatically uploads an artifact from the './_site' directory by default - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: doc/build/html/ @@ -37,4 +37,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 From 06dbd41d8a3ca86999d84a2239a108794d9c1c65 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 19 Oct 2025 11:46:03 +0100 Subject: [PATCH 46/77] Drop EOLd Python versions (#2632) --- .github/workflows/main.yml | 4 +- pyproject.toml | 2 +- src/runtime/Native/TypeOffset37.cs | 136 ---------------------------- src/runtime/Native/TypeOffset38.cs | 138 ----------------------------- src/runtime/Native/TypeOffset39.cs | 138 ----------------------------- 5 files changed, 3 insertions(+), 415 deletions(-) delete mode 100644 src/runtime/Native/TypeOffset37.cs delete mode 100644 src/runtime/Native/TypeOffset38.cs delete mode 100644 src/runtime/Native/TypeOffset39.cs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8485189e1..5676b44a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,12 +32,12 @@ jobs: platform: x64 instance: macos-13 - python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.10", "3.11", "3.12", "3.13"] # This fails in pytest with: # CSC : error CS4023: /platform:anycpu32bitpreferred can only be used with /t:exe, /t:winexe and /t:appcontainerexe [D:\a\pythonnet\pythonnet\src\runtime\Python.Runtime.csproj] exclude: - - os: + - os: category: windows platform: x86 python: "3.13" diff --git a/pyproject.toml b/pyproject.toml index 968998e8d..87018826b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "clr_loader>=0.2.7,<0.3.0" ] -requires-python = ">=3.7, <3.14" +requires-python = ">=3.10, <3.14" classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/src/runtime/Native/TypeOffset37.cs b/src/runtime/Native/TypeOffset37.cs deleted file mode 100644 index 951cb1068..000000000 --- a/src/runtime/Native/TypeOffset37.cs +++ /dev/null @@ -1,136 +0,0 @@ - -// Auto-generated by geninterop.py. -// DO NOT MODIFY BY HAND. - -// Python 3.7: ABI flags: '' - -// ReSharper disable InconsistentNaming -// ReSharper disable IdentifierTypo - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using Python.Runtime.Native; - -namespace Python.Runtime -{ - [SuppressMessage("Style", "IDE1006:Naming Styles", - Justification = "Following CPython", - Scope = "type")] - - [StructLayout(LayoutKind.Sequential)] - internal class TypeOffset37 : GeneratedTypeOffsets, ITypeOffsets - { - public TypeOffset37() { } - // Auto-generated from PyHeapTypeObject in Python.h - public int ob_refcnt { get; private set; } - public int ob_type { get; private set; } - public int ob_size { get; private set; } - public int tp_name { get; private set; } - public int tp_basicsize { get; private set; } - public int tp_itemsize { get; private set; } - public int tp_dealloc { get; private set; } - public int tp_print { get; private set; } - public int tp_getattr { get; private set; } - public int tp_setattr { get; private set; } - public int tp_as_async { get; private set; } - public int tp_repr { get; private set; } - public int tp_as_number { get; private set; } - public int tp_as_sequence { get; private set; } - public int tp_as_mapping { get; private set; } - public int tp_hash { get; private set; } - public int tp_call { get; private set; } - public int tp_str { get; private set; } - public int tp_getattro { get; private set; } - public int tp_setattro { get; private set; } - public int tp_as_buffer { get; private set; } - public int tp_flags { get; private set; } - public int tp_doc { get; private set; } - public int tp_traverse { get; private set; } - public int tp_clear { get; private set; } - public int tp_richcompare { get; private set; } - public int tp_weaklistoffset { get; private set; } - public int tp_iter { get; private set; } - public int tp_iternext { get; private set; } - public int tp_methods { get; private set; } - public int tp_members { get; private set; } - public int tp_getset { get; private set; } - public int tp_base { get; private set; } - public int tp_dict { get; private set; } - public int tp_descr_get { get; private set; } - public int tp_descr_set { get; private set; } - public int tp_dictoffset { get; private set; } - public int tp_init { get; private set; } - public int tp_alloc { get; private set; } - public int tp_new { get; private set; } - public int tp_free { get; private set; } - public int tp_is_gc { get; private set; } - public int tp_bases { get; private set; } - public int tp_mro { get; private set; } - public int tp_cache { get; private set; } - public int tp_subclasses { get; private set; } - public int tp_weaklist { get; private set; } - public int tp_del { get; private set; } - public int tp_version_tag { get; private set; } - public int tp_finalize { get; private set; } - public int am_await { get; private set; } - public int am_aiter { get; private set; } - public int am_anext { get; private set; } - public int nb_add { get; private set; } - public int nb_subtract { get; private set; } - public int nb_multiply { get; private set; } - public int nb_remainder { get; private set; } - public int nb_divmod { get; private set; } - public int nb_power { get; private set; } - public int nb_negative { get; private set; } - public int nb_positive { get; private set; } - public int nb_absolute { get; private set; } - public int nb_bool { get; private set; } - public int nb_invert { get; private set; } - public int nb_lshift { get; private set; } - public int nb_rshift { get; private set; } - public int nb_and { get; private set; } - public int nb_xor { get; private set; } - public int nb_or { get; private set; } - public int nb_int { get; private set; } - public int nb_reserved { get; private set; } - public int nb_float { get; private set; } - public int nb_inplace_add { get; private set; } - public int nb_inplace_subtract { get; private set; } - public int nb_inplace_multiply { get; private set; } - public int nb_inplace_remainder { get; private set; } - public int nb_inplace_power { get; private set; } - public int nb_inplace_lshift { get; private set; } - public int nb_inplace_rshift { get; private set; } - public int nb_inplace_and { get; private set; } - public int nb_inplace_xor { get; private set; } - public int nb_inplace_or { get; private set; } - public int nb_floor_divide { get; private set; } - public int nb_true_divide { get; private set; } - public int nb_inplace_floor_divide { get; private set; } - public int nb_inplace_true_divide { get; private set; } - public int nb_index { get; private set; } - public int nb_matrix_multiply { get; private set; } - public int nb_inplace_matrix_multiply { get; private set; } - public int mp_length { get; private set; } - public int mp_subscript { get; private set; } - public int mp_ass_subscript { get; private set; } - public int sq_length { get; private set; } - public int sq_concat { get; private set; } - public int sq_repeat { get; private set; } - public int sq_item { get; private set; } - public int was_sq_slice { get; private set; } - public int sq_ass_item { get; private set; } - public int was_sq_ass_slice { get; private set; } - public int sq_contains { get; private set; } - public int sq_inplace_concat { get; private set; } - public int sq_inplace_repeat { get; private set; } - public int bf_getbuffer { get; private set; } - public int bf_releasebuffer { get; private set; } - public int name { get; private set; } - public int ht_slots { get; private set; } - public int qualname { get; private set; } - public int ht_cached_keys { get; private set; } - } -} diff --git a/src/runtime/Native/TypeOffset38.cs b/src/runtime/Native/TypeOffset38.cs deleted file mode 100644 index 67a40eabd..000000000 --- a/src/runtime/Native/TypeOffset38.cs +++ /dev/null @@ -1,138 +0,0 @@ - -// Auto-generated by geninterop.py. -// DO NOT MODIFY BY HAND. - -// Python 3.8: ABI flags: '' - -// ReSharper disable InconsistentNaming -// ReSharper disable IdentifierTypo - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using Python.Runtime.Native; - -namespace Python.Runtime -{ - [SuppressMessage("Style", "IDE1006:Naming Styles", - Justification = "Following CPython", - Scope = "type")] - - [StructLayout(LayoutKind.Sequential)] - internal class TypeOffset38 : GeneratedTypeOffsets, ITypeOffsets - { - public TypeOffset38() { } - // Auto-generated from PyHeapTypeObject in Python.h - public int ob_refcnt { get; private set; } - public int ob_type { get; private set; } - public int ob_size { get; private set; } - public int tp_name { get; private set; } - public int tp_basicsize { get; private set; } - public int tp_itemsize { get; private set; } - public int tp_dealloc { get; private set; } - public int tp_vectorcall_offset { get; private set; } - public int tp_getattr { get; private set; } - public int tp_setattr { get; private set; } - public int tp_as_async { get; private set; } - public int tp_repr { get; private set; } - public int tp_as_number { get; private set; } - public int tp_as_sequence { get; private set; } - public int tp_as_mapping { get; private set; } - public int tp_hash { get; private set; } - public int tp_call { get; private set; } - public int tp_str { get; private set; } - public int tp_getattro { get; private set; } - public int tp_setattro { get; private set; } - public int tp_as_buffer { get; private set; } - public int tp_flags { get; private set; } - public int tp_doc { get; private set; } - public int tp_traverse { get; private set; } - public int tp_clear { get; private set; } - public int tp_richcompare { get; private set; } - public int tp_weaklistoffset { get; private set; } - public int tp_iter { get; private set; } - public int tp_iternext { get; private set; } - public int tp_methods { get; private set; } - public int tp_members { get; private set; } - public int tp_getset { get; private set; } - public int tp_base { get; private set; } - public int tp_dict { get; private set; } - public int tp_descr_get { get; private set; } - public int tp_descr_set { get; private set; } - public int tp_dictoffset { get; private set; } - public int tp_init { get; private set; } - public int tp_alloc { get; private set; } - public int tp_new { get; private set; } - public int tp_free { get; private set; } - public int tp_is_gc { get; private set; } - public int tp_bases { get; private set; } - public int tp_mro { get; private set; } - public int tp_cache { get; private set; } - public int tp_subclasses { get; private set; } - public int tp_weaklist { get; private set; } - public int tp_del { get; private set; } - public int tp_version_tag { get; private set; } - public int tp_finalize { get; private set; } - public int tp_vectorcall { get; private set; } - public int tp_print { get; private set; } - public int am_await { get; private set; } - public int am_aiter { get; private set; } - public int am_anext { get; private set; } - public int nb_add { get; private set; } - public int nb_subtract { get; private set; } - public int nb_multiply { get; private set; } - public int nb_remainder { get; private set; } - public int nb_divmod { get; private set; } - public int nb_power { get; private set; } - public int nb_negative { get; private set; } - public int nb_positive { get; private set; } - public int nb_absolute { get; private set; } - public int nb_bool { get; private set; } - public int nb_invert { get; private set; } - public int nb_lshift { get; private set; } - public int nb_rshift { get; private set; } - public int nb_and { get; private set; } - public int nb_xor { get; private set; } - public int nb_or { get; private set; } - public int nb_int { get; private set; } - public int nb_reserved { get; private set; } - public int nb_float { get; private set; } - public int nb_inplace_add { get; private set; } - public int nb_inplace_subtract { get; private set; } - public int nb_inplace_multiply { get; private set; } - public int nb_inplace_remainder { get; private set; } - public int nb_inplace_power { get; private set; } - public int nb_inplace_lshift { get; private set; } - public int nb_inplace_rshift { get; private set; } - public int nb_inplace_and { get; private set; } - public int nb_inplace_xor { get; private set; } - public int nb_inplace_or { get; private set; } - public int nb_floor_divide { get; private set; } - public int nb_true_divide { get; private set; } - public int nb_inplace_floor_divide { get; private set; } - public int nb_inplace_true_divide { get; private set; } - public int nb_index { get; private set; } - public int nb_matrix_multiply { get; private set; } - public int nb_inplace_matrix_multiply { get; private set; } - public int mp_length { get; private set; } - public int mp_subscript { get; private set; } - public int mp_ass_subscript { get; private set; } - public int sq_length { get; private set; } - public int sq_concat { get; private set; } - public int sq_repeat { get; private set; } - public int sq_item { get; private set; } - public int was_sq_slice { get; private set; } - public int sq_ass_item { get; private set; } - public int was_sq_ass_slice { get; private set; } - public int sq_contains { get; private set; } - public int sq_inplace_concat { get; private set; } - public int sq_inplace_repeat { get; private set; } - public int bf_getbuffer { get; private set; } - public int bf_releasebuffer { get; private set; } - public int name { get; private set; } - public int ht_slots { get; private set; } - public int qualname { get; private set; } - public int ht_cached_keys { get; private set; } - } -} diff --git a/src/runtime/Native/TypeOffset39.cs b/src/runtime/Native/TypeOffset39.cs deleted file mode 100644 index cf3acc984..000000000 --- a/src/runtime/Native/TypeOffset39.cs +++ /dev/null @@ -1,138 +0,0 @@ - -// Auto-generated by geninterop.py. -// DO NOT MODIFY BY HAND. - -// Python 3.9: ABI flags: '' - -// ReSharper disable InconsistentNaming -// ReSharper disable IdentifierTypo - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using Python.Runtime.Native; - -namespace Python.Runtime -{ - [SuppressMessage("Style", "IDE1006:Naming Styles", - Justification = "Following CPython", - Scope = "type")] - - [StructLayout(LayoutKind.Sequential)] - internal class TypeOffset39 : GeneratedTypeOffsets, ITypeOffsets - { - public TypeOffset39() { } - // Auto-generated from PyHeapTypeObject in Python.h - public int ob_refcnt { get; private set; } - public int ob_type { get; private set; } - public int ob_size { get; private set; } - public int tp_name { get; private set; } - public int tp_basicsize { get; private set; } - public int tp_itemsize { get; private set; } - public int tp_dealloc { get; private set; } - public int tp_vectorcall_offset { get; private set; } - public int tp_getattr { get; private set; } - public int tp_setattr { get; private set; } - public int tp_as_async { get; private set; } - public int tp_repr { get; private set; } - public int tp_as_number { get; private set; } - public int tp_as_sequence { get; private set; } - public int tp_as_mapping { get; private set; } - public int tp_hash { get; private set; } - public int tp_call { get; private set; } - public int tp_str { get; private set; } - public int tp_getattro { get; private set; } - public int tp_setattro { get; private set; } - public int tp_as_buffer { get; private set; } - public int tp_flags { get; private set; } - public int tp_doc { get; private set; } - public int tp_traverse { get; private set; } - public int tp_clear { get; private set; } - public int tp_richcompare { get; private set; } - public int tp_weaklistoffset { get; private set; } - public int tp_iter { get; private set; } - public int tp_iternext { get; private set; } - public int tp_methods { get; private set; } - public int tp_members { get; private set; } - public int tp_getset { get; private set; } - public int tp_base { get; private set; } - public int tp_dict { get; private set; } - public int tp_descr_get { get; private set; } - public int tp_descr_set { get; private set; } - public int tp_dictoffset { get; private set; } - public int tp_init { get; private set; } - public int tp_alloc { get; private set; } - public int tp_new { get; private set; } - public int tp_free { get; private set; } - public int tp_is_gc { get; private set; } - public int tp_bases { get; private set; } - public int tp_mro { get; private set; } - public int tp_cache { get; private set; } - public int tp_subclasses { get; private set; } - public int tp_weaklist { get; private set; } - public int tp_del { get; private set; } - public int tp_version_tag { get; private set; } - public int tp_finalize { get; private set; } - public int tp_vectorcall { get; private set; } - public int am_await { get; private set; } - public int am_aiter { get; private set; } - public int am_anext { get; private set; } - public int nb_add { get; private set; } - public int nb_subtract { get; private set; } - public int nb_multiply { get; private set; } - public int nb_remainder { get; private set; } - public int nb_divmod { get; private set; } - public int nb_power { get; private set; } - public int nb_negative { get; private set; } - public int nb_positive { get; private set; } - public int nb_absolute { get; private set; } - public int nb_bool { get; private set; } - public int nb_invert { get; private set; } - public int nb_lshift { get; private set; } - public int nb_rshift { get; private set; } - public int nb_and { get; private set; } - public int nb_xor { get; private set; } - public int nb_or { get; private set; } - public int nb_int { get; private set; } - public int nb_reserved { get; private set; } - public int nb_float { get; private set; } - public int nb_inplace_add { get; private set; } - public int nb_inplace_subtract { get; private set; } - public int nb_inplace_multiply { get; private set; } - public int nb_inplace_remainder { get; private set; } - public int nb_inplace_power { get; private set; } - public int nb_inplace_lshift { get; private set; } - public int nb_inplace_rshift { get; private set; } - public int nb_inplace_and { get; private set; } - public int nb_inplace_xor { get; private set; } - public int nb_inplace_or { get; private set; } - public int nb_floor_divide { get; private set; } - public int nb_true_divide { get; private set; } - public int nb_inplace_floor_divide { get; private set; } - public int nb_inplace_true_divide { get; private set; } - public int nb_index { get; private set; } - public int nb_matrix_multiply { get; private set; } - public int nb_inplace_matrix_multiply { get; private set; } - public int mp_length { get; private set; } - public int mp_subscript { get; private set; } - public int mp_ass_subscript { get; private set; } - public int sq_length { get; private set; } - public int sq_concat { get; private set; } - public int sq_repeat { get; private set; } - public int sq_item { get; private set; } - public int was_sq_slice { get; private set; } - public int sq_ass_item { get; private set; } - public int was_sq_ass_slice { get; private set; } - public int sq_contains { get; private set; } - public int sq_inplace_concat { get; private set; } - public int sq_inplace_repeat { get; private set; } - public int bf_getbuffer { get; private set; } - public int bf_releasebuffer { get; private set; } - public int name { get; private set; } - public int ht_slots { get; private set; } - public int qualname { get; private set; } - public int ht_cached_keys { get; private set; } - public int ht_module { get; private set; } - } -} From 4cbc6a914cdb4460b8658f45818474654c0bcfa3 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 19 Oct 2025 12:13:39 +0100 Subject: [PATCH 47/77] Bump setuptools and adjust license information (#2633) The `project.license` entry in `pyproject.toml` should be a bare string now and the license tag is removed. --- pyproject.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87018826b..20947be0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] -requires = ["setuptools>=61", "wheel"] +requires = ["setuptools>=80"] build-backend = "setuptools.build_meta" [project] name = "pythonnet" description = ".NET and Mono integration for Python" -license = {text = "MIT"} +license = "MIT" readme = "README.rst" @@ -18,7 +18,6 @@ requires-python = ">=3.10, <3.14" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: C#", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", @@ -55,7 +54,6 @@ Sources = "https://github.com/pythonnet/pythonnet" [tool.setuptools] zip-safe = false py-modules = ["clr"] -license-files = [] [tool.setuptools.dynamic.version] file = "version.txt" From c6a3ed58afc9b5011182e6b0e252dc58b82fba49 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 19 Oct 2025 12:27:42 +0100 Subject: [PATCH 48/77] Minimal .NET 8 usage changes (#2634) - Disables NonCopyableAnalyzer for now as it breaks the build --- .github/workflows/ARM.yml | 6 +++--- .github/workflows/main.yml | 2 +- Directory.Build.props | 8 ++++---- src/embed_tests/Python.EmbeddingTest.csproj | 2 +- src/python_tests_runner/Python.PythonTestsRunner.csproj | 2 +- src/testing/Python.Test.csproj | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ARM.yml b/.github/workflows/ARM.yml index eef0e666d..c2b89edfe 100644 --- a/.github/workflows/ARM.yml +++ b/.github/workflows/ARM.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Clean previous install run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5676b44a5..4e37eabcd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,7 +55,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 diff --git a/Directory.Build.props b/Directory.Build.props index e45c16f6a..fcbae2c8f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - Copyright (c) 2006-2022 The Contributors of the Python.NET Project + Copyright (c) 2006-2025 The Contributors of the Python.NET Project pythonnet Python.NET 10.0 @@ -12,13 +12,13 @@ - + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 4993994d3..2b3d6610e 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -1,7 +1,7 @@ - net472;net6.0 + net472;net8.0 ..\pythonnet.snk true diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj index 63981c424..964166907 100644 --- a/src/python_tests_runner/Python.PythonTestsRunner.csproj +++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj @@ -1,7 +1,7 @@ - net472;net6.0 + net472;net8.0 diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj index 3adc5c0c6..b7ba6cd4e 100644 --- a/src/testing/Python.Test.csproj +++ b/src/testing/Python.Test.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net6.0 + netstandard2.0;net8.0 true true ..\pythonnet.snk From 117c66a0ea7a37e129fd4f4260729710e29ba174 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 21 Oct 2025 18:44:28 +0200 Subject: [PATCH 49/77] Drop performance tests (#2636) --- .github/workflows/main.yml | 6 -- pythonnet.sln | 2 - .../BaselineComparisonBenchmarkBase.cs | 74 ------------------- src/perf_tests/BaselineComparisonConfig.cs | 53 ------------- src/perf_tests/BenchmarkTests.cs | 74 ------------------- src/perf_tests/Python.PerformanceTests.csproj | 48 ------------ src/perf_tests/PythonCallingNetBenchmark.cs | 58 --------------- src/perf_tests/baseline/.gitkeep | 0 8 files changed, 315 deletions(-) delete mode 100644 src/perf_tests/BaselineComparisonBenchmarkBase.cs delete mode 100644 src/perf_tests/BaselineComparisonConfig.cs delete mode 100644 src/perf_tests/BenchmarkTests.cs delete mode 100644 src/perf_tests/Python.PerformanceTests.csproj delete mode 100644 src/perf_tests/PythonCallingNetBenchmark.cs delete mode 100644 src/perf_tests/baseline/.gitkeep diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4e37eabcd..ea6b2c99a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -107,10 +107,4 @@ jobs: if: ${{ matrix.python != '3.13' }} run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ - - name: Perf tests - if: ${{ (matrix.python == '3.8') && (matrix.os.platform == 'x64') }} - run: | - pip install --force --no-deps --target src/perf_tests/baseline/ pythonnet==2.5.2 - dotnet test --configuration Release --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/perf_tests/ - # TODO: Run mono tests on Windows? diff --git a/pythonnet.sln b/pythonnet.sln index cf684c0e1..9dfeb44b1 100644 --- a/pythonnet.sln +++ b/pythonnet.sln @@ -8,8 +8,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.EmbeddingTest", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.Test", "src\testing\Python.Test.csproj", "{14EF9518-5BB7-4F83-8686-015BD2CC788E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.PerformanceTests", "src\perf_tests\Python.PerformanceTests.csproj", "{4F2EA4A1-7ECA-48B5-8077-7A3C366F9931}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.DomainReloadTests", "tests\domain_tests\Python.DomainReloadTests.csproj", "{F2FB6DA3-318E-4F30-9A1F-932C667E38C5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repo", "Repo", "{441A0123-F4C6-4EE4-9AEE-315FD79BE2D5}" diff --git a/src/perf_tests/BaselineComparisonBenchmarkBase.cs b/src/perf_tests/BaselineComparisonBenchmarkBase.cs deleted file mode 100644 index 06adcbc67..000000000 --- a/src/perf_tests/BaselineComparisonBenchmarkBase.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; - -using Python.Runtime; - -namespace Python.PerformanceTests -{ - public class BaselineComparisonBenchmarkBase - { - public BaselineComparisonBenchmarkBase() - { - Console.WriteLine($"CWD: {Environment.CurrentDirectory}"); - Console.WriteLine($"Using Python.Runtime from {typeof(PythonEngine).Assembly.Location} {typeof(PythonEngine).Assembly.GetName()}"); - - try { - PythonEngine.Initialize(); - Console.WriteLine("Python Initialized"); - Trace.Assert(PythonEngine.BeginAllowThreads() != IntPtr.Zero); - Console.WriteLine("Threading enabled"); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - } - - static BaselineComparisonBenchmarkBase() - { - SetupRuntimeResolve(); - } - - public static void SetupRuntimeResolve() - { - string pythonRuntimeDll = Environment.GetEnvironmentVariable(BaselineComparisonConfig.EnvironmentVariableName); - if (string.IsNullOrEmpty(pythonRuntimeDll)) - { - throw new ArgumentException( - "Required environment variable is missing", - BaselineComparisonConfig.EnvironmentVariableName); - } - - Console.WriteLine("Preloading " + pythonRuntimeDll); - Assembly.LoadFrom(pythonRuntimeDll); - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - if (assembly.FullName.StartsWith("Python.Runtime")) - Console.WriteLine(assembly.Location); - foreach(var dependency in assembly.GetReferencedAssemblies()) - if (dependency.FullName.Contains("Python.Runtime")) { - Console.WriteLine($"{assembly} -> {dependency}"); - } - } - - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; - } - - static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) { - if (!args.Name.StartsWith("Python.Runtime")) - return null; - - var preloaded = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == "Python.Runtime"); - if (preloaded != null) return preloaded; - - string pythonRuntimeDll = Environment.GetEnvironmentVariable(BaselineComparisonConfig.EnvironmentVariableName); - if (string.IsNullOrEmpty(pythonRuntimeDll)) - return null; - - return Assembly.LoadFrom(pythonRuntimeDll); - } - } -} diff --git a/src/perf_tests/BaselineComparisonConfig.cs b/src/perf_tests/BaselineComparisonConfig.cs deleted file mode 100644 index 3f6766554..000000000 --- a/src/perf_tests/BaselineComparisonConfig.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; - -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; - -using Perfolizer.Horology; - -namespace Python.PerformanceTests -{ - public class BaselineComparisonConfig : ManualConfig - { - public const string EnvironmentVariableName = "PythonRuntimeDLL"; - - public BaselineComparisonConfig() - { - this.Options |= ConfigOptions.DisableOptimizationsValidator; - - string deploymentRoot = BenchmarkTests.DeploymentRoot; - - var baseJob = Job.Default - .WithLaunchCount(1) - .WithWarmupCount(3) - .WithMaxIterationCount(100) - .WithIterationTime(TimeInterval.FromMilliseconds(100)); - this.Add(baseJob - .WithId("baseline") - .WithEnvironmentVariable(EnvironmentVariableName, - Path.Combine(deploymentRoot, "baseline", "Python.Runtime.dll")) - .WithBaseline(true)); - this.Add(baseJob - .WithId("new") - .WithEnvironmentVariable(EnvironmentVariableName, - Path.Combine(deploymentRoot, "new", "Python.Runtime.dll"))); - } - - static BaselineComparisonConfig() { - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; - } - - static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) { - Console.WriteLine(args.Name); - if (!args.Name.StartsWith("Python.Runtime")) - return null; - string pythonRuntimeDll = Environment.GetEnvironmentVariable(EnvironmentVariableName); - if (string.IsNullOrEmpty(pythonRuntimeDll)) - pythonRuntimeDll = Path.Combine(BenchmarkTests.DeploymentRoot, "baseline", "Python.Runtime.dll"); - return Assembly.LoadFrom(pythonRuntimeDll); - } - } -} diff --git a/src/perf_tests/BenchmarkTests.cs b/src/perf_tests/BenchmarkTests.cs deleted file mode 100644 index 9e033d11f..000000000 --- a/src/perf_tests/BenchmarkTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Reflection; - -using BenchmarkDotNet.Reports; -using BenchmarkDotNet.Running; -using NUnit.Framework; - -namespace Python.PerformanceTests -{ - public class BenchmarkTests - { - Summary summary; - - [OneTimeSetUp] - public void SetUp() - { - Environment.CurrentDirectory = Path.Combine(DeploymentRoot, "new"); - this.summary = BenchmarkRunner.Run(); - Assert.IsNotEmpty(this.summary.Reports); - Assert.IsTrue( - condition: this.summary.Reports.All(r => r.Success), - message: "BenchmarkDotNet failed to execute or collect results of performance tests. See logs above."); - } - - [Test] - public void ReadInt64Property() - { - double optimisticPerfRatio = GetOptimisticPerfRatio(this.summary.Reports); - AssertPerformanceIsBetterOrSame(optimisticPerfRatio, target: 1.35); - } - - [Test] - public void WriteInt64Property() - { - double optimisticPerfRatio = GetOptimisticPerfRatio(this.summary.Reports); - AssertPerformanceIsBetterOrSame(optimisticPerfRatio, target: 1.25); - } - - static double GetOptimisticPerfRatio( - IReadOnlyList reports, - [CallerMemberName] string methodName = null) - { - reports = reports.Where(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name == methodName).ToArray(); - if (reports.Count == 0) - throw new ArgumentException( - message: $"No reports found for {methodName}. " - + "You have to match test method name to benchmark method name or " - + "pass benchmark method name explicitly", - paramName: nameof(methodName)); - - var baseline = reports.Single(r => r.BenchmarkCase.Job.ResolvedId == "baseline").ResultStatistics; - var @new = reports.Single(r => r.BenchmarkCase.Job.ResolvedId != "baseline").ResultStatistics; - - double newTimeOptimistic = @new.Mean - (@new.StandardDeviation + baseline.StandardDeviation) * 0.5; - - return newTimeOptimistic / baseline.Mean; - } - - public static string DeploymentRoot => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - - public static void AssertPerformanceIsBetterOrSame( - double actual, double target, - double wiggleRoom = 1.1, [CallerMemberName] string testName = null) { - double threshold = target * wiggleRoom; - Assert.LessOrEqual(actual, threshold, - $"{testName}: {actual:F3} > {threshold:F3} (target: {target:F3})" - + ": perf result is higher than the failure threshold."); - } - } -} diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj deleted file mode 100644 index bde07ecab..000000000 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - net472 - false - x64 - x64 - - - - - - PreserveNewest - - - - - - false - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - diff --git a/src/perf_tests/PythonCallingNetBenchmark.cs b/src/perf_tests/PythonCallingNetBenchmark.cs deleted file mode 100644 index d7edd4583..000000000 --- a/src/perf_tests/PythonCallingNetBenchmark.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; - -using BenchmarkDotNet.Attributes; -using Python.Runtime; - -namespace Python.PerformanceTests -{ - [Config(typeof(BaselineComparisonConfig))] - public class PythonCallingNetBenchmark: BaselineComparisonBenchmarkBase - { - [Benchmark] - public void ReadInt64Property() - { - using (Py.GIL()) - { - var locals = new PyDict(); - locals.SetItem("a", new NetObject().ToPython()); - Exec($@" -s = 0 -for i in range(50000): - s += a.{nameof(NetObject.LongProperty)} -", locals: locals); - } - } - - [Benchmark] - public void WriteInt64Property() { - using (Py.GIL()) { - var locals = new PyDict(); - locals.SetItem("a", new NetObject().ToPython()); - Exec($@" -s = 0 -for i in range(50000): - a.{nameof(NetObject.LongProperty)} += i -", locals: locals); - } - } - - static void Exec(string code, PyDict locals) - { - MethodInfo exec = typeof(PythonEngine).GetMethod(nameof(PythonEngine.Exec)); - object localsArg = typeof(PyObject).Assembly.GetName().Version.Major >= 3 - ? locals : locals.Handle; - exec.Invoke(null, new[] - { - code, localsArg, null - }); - } - } - - class NetObject - { - public long LongProperty { get; set; } = 42; - } -} diff --git a/src/perf_tests/baseline/.gitkeep b/src/perf_tests/baseline/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 0258688e684c0a7ad59b4d76384a1d4a21735472 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 21 Oct 2025 19:20:12 +0200 Subject: [PATCH 50/77] Properly detect availability of BinaryFormatter (#2639) --- src/runtime/StateSerialization/RuntimeData.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runtime/StateSerialization/RuntimeData.cs b/src/runtime/StateSerialization/RuntimeData.cs index 8eda9ce0b..61e377aa4 100644 --- a/src/runtime/StateSerialization/RuntimeData.cs +++ b/src/runtime/StateSerialization/RuntimeData.cs @@ -20,7 +20,9 @@ public static class RuntimeData { try { - return new BinaryFormatter(); + var res = new BinaryFormatter(); + res.Serialize(new MemoryStream(), 1); // test if BinaryFormatter is usable + return res; } catch { From cccb2956d5ac581093ac8fe5702ea06e28d5e4cc Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 21 Oct 2025 19:26:13 +0200 Subject: [PATCH 51/77] Use last compiler toolset version that support .NET 8 (#2640) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index fcbae2c8f..0288a4d64 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers From 4cb57084f9de54eff9f16e0ce69dd70ec9a7e8d6 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 22 Oct 2025 19:22:08 +0200 Subject: [PATCH 52/77] Add dependabot file (#2642) --- .github/dependabot.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..47db1e621 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From dcf7abc44848a11d46dc28e274a35681d3210177 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 22 Oct 2025 19:22:52 +0200 Subject: [PATCH 53/77] Use official ARM runners (#2641) --- .github/workflows/ARM.yml | 56 -------------------------------------- .github/workflows/main.yml | 4 +++ 2 files changed, 4 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/ARM.yml diff --git a/.github/workflows/ARM.yml b/.github/workflows/ARM.yml deleted file mode 100644 index c2b89edfe..000000000 --- a/.github/workflows/ARM.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Main (ARM) - -on: - push: - branches: - - master - pull_request: - -jobs: - build-test-arm: - name: Build and Test ARM64 - runs-on: [self-hosted, linux, ARM64] - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: '8.0.x' - - - name: Clean previous install - run: | - pip uninstall -y pythonnet - - - name: Install dependencies - run: | - pip3.8 install -r requirements.txt - pip3.8 install pytest numpy # for tests - - - name: Build and Install - run: | - pip3.8 install -v . - - - name: Set Python DLL path (non Windows) - run: | - echo PYTHONNET_PYDLL=$(python3.8 -m find_libpython) >> $GITHUB_ENV - - - name: Embedding tests - run: dotnet test --logger "console;verbosity=detailed" src/embed_tests/ - - - name: Python Tests (Mono) - run: python3.8 -m pytest --runtime mono - - - name: Python Tests (.NET Core) - run: python3.8 -m pytest --runtime coreclr - - - name: Python tests run from .NET - run: dotnet test src/python_tests_runner/ - - #- name: Perf tests - # run: | - # pip install --force --no-deps --target src/perf_tests/baseline/ pythonnet==2.5.2 - # dotnet test --configuration Release --logger "console;verbosity=detailed" src/perf_tests/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea6b2c99a..93c76064a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,10 @@ jobs: platform: x64 instance: ubuntu-22.04 + - category: ubuntu + platform: arm64 + instance: ubuntu-22.04-arm + - category: macos platform: x64 instance: macos-13 From 08381a40608ed15826414ee9c0237e6dfd94a0f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:27:10 +0200 Subject: [PATCH 54/77] Bump actions/upload-pages-artifact from 3 to 4 (#2644) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 30163cd14..5e6b76fdd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Upload artifact # Automatically uploads an artifact from the './_site' directory by default - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: doc/build/html/ From 6cedd18bc7cc3a6ad0bccca1f298d454853b9d36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:27:19 +0200 Subject: [PATCH 55/77] Bump actions/setup-python from 2 to 6 (#2646) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- .github/workflows/nuget-preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93c76064a..dc9148a55 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,7 +62,7 @@ jobs: dotnet-version: '8.0.x' - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.os.platform }} diff --git a/.github/workflows/nuget-preview.yml b/.github/workflows/nuget-preview.yml index d652f4b1e..1aa071480 100644 --- a/.github/workflows/nuget-preview.yml +++ b/.github/workflows/nuget-preview.yml @@ -29,7 +29,7 @@ jobs: dotnet-version: '6.0.x' - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: 3.8 architecture: x64 From 36d5d59d03536bf9bf0fca2bba402dd42267b584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:27:28 +0200 Subject: [PATCH 56/77] Bump actions/checkout from 2 to 5 (#2648) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/nuget-preview.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5e6b76fdd..2b1dda732 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Doxygen Action uses: mattnotmitt/doxygen-action@1.12.0 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dc9148a55..26f48aaaa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,7 +54,7 @@ jobs: mono-version: latest - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v1 diff --git a/.github/workflows/nuget-preview.yml b/.github/workflows/nuget-preview.yml index 1aa071480..0f6d3ea41 100644 --- a/.github/workflows/nuget-preview.yml +++ b/.github/workflows/nuget-preview.yml @@ -21,7 +21,7 @@ jobs: echo "DATE_VER=$(date "+%Y-%m-%d")" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v1 From e4211db55c989b133859d107ae71bb37b19be5cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:27:47 +0200 Subject: [PATCH 57/77] Bump actions/setup-dotnet from 1 to 5 (#2645) Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 1 to 5. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v1...v5) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- .github/workflows/nuget-preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 26f48aaaa..5499cc020 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: '8.0.x' diff --git a/.github/workflows/nuget-preview.yml b/.github/workflows/nuget-preview.yml index 0f6d3ea41..4d1476560 100644 --- a/.github/workflows/nuget-preview.yml +++ b/.github/workflows/nuget-preview.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: '6.0.x' From 7d3a6dc5cc12ffa869a79106cac890014110b66c Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 22 Oct 2025 21:31:52 +0200 Subject: [PATCH 58/77] Bump some dependencies --- pyproject.toml | 2 +- src/embed_tests/Python.EmbeddingTest.csproj | 4 ++-- src/python_tests_runner/Python.PythonTestsRunner.csproj | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 20947be0e..e3fba0dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dynamic = ["version"] [dependency-groups] dev = [ "pytest >= 6", - "find_libpython >= 0.3.0", + "find_libpython >= 0.3", "numpy >=2 ; python_version >= '3.10'", "numpy <2 ; python_version < '3.10'", "psutil" diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 2b3d6610e..da6799912 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -20,11 +20,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + 1.0.0 all diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj index 964166907..e55c1af37 100644 --- a/src/python_tests_runner/Python.PythonTestsRunner.csproj +++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj @@ -10,14 +10,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - 1.0.0 + 1.* all runtime; build; native; contentfiles; analyzers From 3426bfd34fe9274012d16e82431441ad4a6773a1 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sat, 25 Oct 2025 19:46:01 +0200 Subject: [PATCH 59/77] Use uv and derive as much as possible from the environment, if available (#2652) * Use uv * Derive program name from venv if possible * Implement venv derivation --- .github/workflows/main.yml | 31 +-- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Runtime.cs | 43 ++-- src/runtime/Util/PythonEnvironment.cs | 188 ++++++++++++++++ tools/geninterop/geninterop.py | 7 +- uv.lock | 295 ++++++++++++++++++++++++++ 6 files changed, 512 insertions(+), 54 deletions(-) create mode 100644 src/runtime/Util/PythonEnvironment.cs create mode 100644 uv.lock diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5499cc020..11762a875 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,34 +62,14 @@ jobs: dotnet-version: '8.0.x' - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v6 + uses: astral-sh/setup-uv@v6 with: - python-version: ${{ matrix.python }} architecture: ${{ matrix.os.platform }} - - - name: Install dependencies - run: | - pip install --upgrade -r requirements.txt - pip install numpy # for tests - - - name: Build and Install - run: | - pip install -v . - - - name: Set Python DLL path and PYTHONHOME (non Windows) - if: ${{ matrix.os.category != 'windows' }} - run: | - echo PYTHONNET_PYDLL=$(python -m find_libpython) >> $GITHUB_ENV - echo PYTHONHOME=$(python -c 'import sys; print(sys.prefix)') >> $GITHUB_ENV - - - name: Set Python DLL path and PYTHONHOME (Windows) - if: ${{ matrix.os.category == 'windows' }} - run: | - Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONNET_PYDLL=$(python -m find_libpython)" - Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')" + python-version: ${{ matrix.python }} + activate-environment: true + enable-cache: true - name: Embedding tests - if: ${{ matrix.python != '3.13' }} run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ env: MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 @@ -108,7 +88,6 @@ jobs: run: pytest --runtime netfx - name: Python tests run from .NET - if: ${{ matrix.python != '3.13' }} - run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ + run: uv run dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ # TODO: Run mono tests on Windows? diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 0a7f58a3c..307b2c3ad 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -2,7 +2,7 @@ netstandard2.0 AnyCPU - 10.0 + 10 Python.Runtime Python.Runtime enable diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index c8f022860..399608733 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading; using System.Collections.Generic; +using System.IO; using Python.Runtime.Native; using System.Linq; using static System.FormattableString; @@ -18,6 +19,8 @@ namespace Python.Runtime ///
public unsafe partial class Runtime { + internal static PythonEnvironment PythonEnvironment = PythonEnvironment.FromEnv(); + public static string? PythonDLL { get => _PythonDll; @@ -25,33 +28,11 @@ public static string? PythonDLL { if (_isInitialized) throw new InvalidOperationException("This property must be set before runtime is initialized"); - _PythonDll = value; + PythonEnvironment.LibPython = value; } } - static string? _PythonDll = GetDefaultDllName(); - private static string? GetDefaultDllName() - { - string dll = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL"); - if (dll is not null) return dll; - - string verString = Environment.GetEnvironmentVariable("PYTHONNET_PYVER"); - if (!Version.TryParse(verString, out var version)) return null; - - return GetDefaultDllName(version); - } - - private static string GetDefaultDllName(Version version) - { - string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib"; - string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Invariant($"{version.Major}{version.Minor}") - : Invariant($"{version.Major}.{version.Minor}"); - string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" - : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" - : ".so"; - return prefix + "python" + suffix + ext; - } + static string? _PythonDll => PythonEnvironment.LibPython; private static bool _isInitialized = false; internal static bool IsInitialized => _isInitialized; @@ -96,6 +77,18 @@ internal static int GetRun() return runNumber; } + static void EnsureProgramName() + { + if (!string.IsNullOrEmpty(PythonEngine.ProgramName)) + return; + + if (PythonEnvironment.IsValid) + { + PythonEngine.ProgramName = PythonEnvironment.ProgramName!; + return; + } + } + internal static bool HostedInPython; internal static bool ProcessIsTerminating; @@ -117,6 +110,8 @@ internal static void Initialize(bool initSigs = false) ); if (!interpreterAlreadyInitialized) { + EnsureProgramName(); + Py_InitializeEx(initSigs ? 1 : 0); NewRun(); diff --git a/src/runtime/Util/PythonEnvironment.cs b/src/runtime/Util/PythonEnvironment.cs new file mode 100644 index 000000000..701db3c93 --- /dev/null +++ b/src/runtime/Util/PythonEnvironment.cs @@ -0,0 +1,188 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using static System.FormattableString; + +namespace Python.Runtime; + + +internal class PythonEnvironment +{ + readonly static string PYDLL_ENV_VAR = "PYTHONNET_PYDLL"; + readonly static string PYEXE_ENV_VAR = "PYTHONNET_PYEXE"; + readonly static string PYNET_VENV_ENV_VAR = "PYTHONNET_VENV"; + readonly static string VENV_ENV_VAR = "VIRTUAL_ENV"; + + public string? VenvPath { get; private set; } + public string? Home { get; private set; } + public Version? Version { get; private set; } + public string? ProgramName { get; set; } + public string? LibPython { get; set; } + + public bool IsValid => + !string.IsNullOrEmpty(ProgramName) && !string.IsNullOrEmpty(LibPython); + + + // TODO: Move the lib-guessing step to separate function, use together with + // PYTHONNET_PYEXE or a path lookup as last resort + + // Initialize PythonEnvironment instance from environment variables. + // + // If PYTHONNET_PYEXE and PYTHONNET_PYDLL are set, these always have precedence. + // If PYTHONNET_VENV or VIRTUAL_ENV is set, we interpret the environment as a venv + // and set the ProgramName/LibPython accordingly. PYTHONNET_VENV takes precedence. + public static PythonEnvironment FromEnv() + { + var pydll = Environment.GetEnvironmentVariable(PYDLL_ENV_VAR); + var pydllSet = !string.IsNullOrEmpty(pydll); + var pyexe = Environment.GetEnvironmentVariable(PYEXE_ENV_VAR); + var pyexeSet = !string.IsNullOrEmpty(pyexe); + var pynetVenv = Environment.GetEnvironmentVariable(PYNET_VENV_ENV_VAR); + var pynetVenvSet = !string.IsNullOrEmpty(pynetVenv); + var venv = Environment.GetEnvironmentVariable(VENV_ENV_VAR); + var venvSet = !string.IsNullOrEmpty(venv); + + PythonEnvironment? res = new(); + + if (pynetVenvSet) + res = FromVenv(pynetVenv) ?? res; + else if (venvSet) + res = FromVenv(venv) ?? res; + + if (pyexeSet) + res.ProgramName = pyexe; + + if (pydllSet) + res.LibPython = pydll; + + return res; + } + + public static PythonEnvironment? FromVenv(string path) + { + var env = new PythonEnvironment + { + VenvPath = path + }; + + string venvCfg = Path.Combine(path, "pyvenv.cfg"); + + if (!File.Exists(venvCfg)) + return null; + + var settings = TryParse(venvCfg); + + if (!settings.ContainsKey("home")) + return null; + + env.Home = settings["home"]; + var pname = ProgramNameFromPath(path); + if (File.Exists(pname)) + env.ProgramName = pname; + + if (settings.TryGetValue("version", out string versionStr)) + { + _ = Version.TryParse(versionStr, out Version versionObj); + env.Version = versionObj; + } + else if (settings.TryGetValue("version_info", out versionStr)) + { + _ = Version.TryParse(versionStr, out Version versionObj); + env.Version = versionObj; + } + + env.LibPython = FindLibPython(env.Home, env.Version); + + return env; + } + + private static Dictionary TryParse(string venvCfg) + { + var settings = new Dictionary(); + + string[] lines = File.ReadAllLines(venvCfg); + + // The actually used format is really primitive: " = " + foreach (string line in lines) + { + var split = line.Split(new[] { '=' }, 2); + + if (split.Length != 2) + continue; + + settings[split[0].Trim()] = split[1].Trim(); + } + + return settings; + } + + private static string? FindLibPython(string home, Version? maybeVersion) + { + // TODO: Check whether there is a .dll/.so/.dylib next to the executable + + if (maybeVersion is Version version) + { + return FindLibPythonInHome(home, version); + } + + return null; + } + + private static string? FindLibPythonInHome(string home, Version version) + { + var libPythonName = GetDefaultDllName(version); + + List pathsToCheck = new(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var arch = RuntimeInformation.ProcessArchitecture; + if (arch == Architecture.X64 || arch == Architecture.Arm64) + { + // multilib systems + pathsToCheck.Add("../lib64"); + } + pathsToCheck.Add("../lib"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + pathsToCheck.Add("."); + } + else + { + pathsToCheck.Add("../lib"); + } + + return pathsToCheck + .Select(path => Path.Combine(home, path, libPythonName)) + .FirstOrDefault(File.Exists); + } + + private static string ProgramNameFromPath(string path) + { + if (Runtime.IsWindows) + { + return Path.Combine(path, "Scripts", "python.exe"); + } + else + { + return Path.Combine(path, "bin", "python"); + } + } + + internal static string GetDefaultDllName(Version version) + { + string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib"; + + string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Invariant($"{version.Major}{version.Minor}") + : Invariant($"{version.Major}.{version.Minor}"); + + string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" + : ".so"; + + return prefix + "python" + suffix + ext; + } +} diff --git a/tools/geninterop/geninterop.py b/tools/geninterop/geninterop.py index 6d80bcfa6..89186737f 100755 --- a/tools/geninterop/geninterop.py +++ b/tools/geninterop/geninterop.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - +#!/usr/bin/env uv run +# /// script +# dependencies = ["pycparser"] +# /// """ TypeOffset is a C# class that mirrors the in-memory layout of heap allocated Python objects. diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..9f73eb110 --- /dev/null +++ b/uv.lock @@ -0,0 +1,295 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.14" + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "clr-loader" +version = "0.2.7.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b3/8ae917e458394e2cebdbf17bed0a8204f8d4ffc79a093a7b1141c7731d3c/clr_loader-0.2.7.post0.tar.gz", hash = "sha256:b7a8b3f8fbb1bcbbb6382d887e21d1742d4f10b5ea209e4ad95568fe97e1c7c6", size = 56701, upload-time = "2024-12-12T20:15:15.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/c0/06e64a54bced4e8b885c1e7ec03ee1869e52acf69e87da40f92391a214ad/clr_loader-0.2.7.post0-py3-none-any.whl", hash = "sha256:e0b9fcc107d48347a4311a28ffe3ae78c4968edb216ffb6564cb03f7ace0bb47", size = 50649, upload-time = "2024-12-12T20:15:13.714Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, +] + +[[package]] +name = "find-libpython" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/86/b1d3a9c49d907cac74f9d8bcead2c8e807a878c0e218d8ef1d38e6a4f59a/find_libpython-0.4.0.tar.gz", hash = "sha256:46f9cdcd397ddb563b2d7592ded3796a41c1df5222443bd9d981721c906c03e6", size = 8979, upload-time = "2024-03-13T17:01:10.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/89/6b4624122d5c61a86e8aebcebd377866338b705ce4f115c45b046dc09b99/find_libpython-0.4.0-py3-none-any.whl", hash = "sha256:034a4253bd57da3408aefc59aeac1650150f6c1f42e10fdd31615cf1df0842e3", size = 8670, upload-time = "2024-03-13T17:01:09.712Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701, upload-time = "2025-03-16T18:27:00.648Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/89/a79e86e5c1433926ed7d60cb267fb64aa578b6101ab645800fd43b4801de/numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9", size = 21250661, upload-time = "2025-03-16T18:02:13.017Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/f50921beb8afd60ed9589ad880332cfefdb805422210d327fb48f12b7a81/numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae", size = 14389926, upload-time = "2025-03-16T18:02:39.022Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/2c4e96130b0b0f97b0ef4a06d6dae3b39d058b21a5e2fa2decd7fd6b1c8f/numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775", size = 5428329, upload-time = "2025-03-16T18:02:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a5/3d7094aa898f4fc5c84cdfb26beeae780352d43f5d8bdec966c4393d644c/numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9", size = 6963559, upload-time = "2025-03-16T18:03:02.523Z" }, + { url = "https://files.pythonhosted.org/packages/4c/22/fb1be710a14434c09080dd4a0acc08939f612ec02efcb04b9e210474782d/numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2", size = 14368066, upload-time = "2025-03-16T18:03:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/c2/07/2e5cc71193e3ef3a219ffcf6ca4858e46ea2be09c026ddd480d596b32867/numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020", size = 16417040, upload-time = "2025-03-16T18:03:55.999Z" }, + { url = "https://files.pythonhosted.org/packages/1a/97/3b1537776ad9a6d1a41813818343745e8dd928a2916d4c9edcd9a8af1dac/numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3", size = 15879862, upload-time = "2025-03-16T18:04:23.56Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/4472f603dd45ef36ff3d8e84e84fe02d9467c78f92cc121633dce6da307b/numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017", size = 18206032, upload-time = "2025-03-16T18:04:53.694Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/6a092963fb82e6c5aa0d0440635827bbb2910da229545473bbb58c537ed3/numpy-2.2.4-cp310-cp310-win32.whl", hash = "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a", size = 6608517, upload-time = "2025-03-16T18:05:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/01/e3/cb04627bc2a1638948bc13e818df26495aa18e20d5be1ed95ab2b10b6847/numpy-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542", size = 12943498, upload-time = "2025-03-16T18:05:28.591Z" }, + { url = "https://files.pythonhosted.org/packages/16/fb/09e778ee3a8ea0d4dc8329cca0a9c9e65fed847d08e37eba74cb7ed4b252/numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4", size = 21254989, upload-time = "2025-03-16T18:06:04.092Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0a/1212befdbecab5d80eca3cde47d304cad986ad4eec7d85a42e0b6d2cc2ef/numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4", size = 14425910, upload-time = "2025-03-16T18:06:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3e/e7247c1d4f15086bb106c8d43c925b0b2ea20270224f5186fa48d4fb5cbd/numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f", size = 5426490, upload-time = "2025-03-16T18:06:39.901Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fa/aa7cd6be51419b894c5787a8a93c3302a1ed4f82d35beb0613ec15bdd0e2/numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880", size = 6967754, upload-time = "2025-03-16T18:06:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/96457c943265de9fadeb3d2ffdbab003f7fba13d971084a9876affcda095/numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1", size = 14373079, upload-time = "2025-03-16T18:07:16.297Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/ceefca458559f0ccc7a982319f37ed07b0d7b526964ae6cc61f8ad1b6119/numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5", size = 16428819, upload-time = "2025-03-16T18:07:44.188Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/9b2ac8eee99e001eb6add9fa27514ef5e9faf176169057a12860af52704c/numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687", size = 15881470, upload-time = "2025-03-16T18:08:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/f0/dc/8569b5f25ff30484b555ad8a3f537e0225d091abec386c9420cf5f7a2976/numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6", size = 18218144, upload-time = "2025-03-16T18:08:42.042Z" }, + { url = "https://files.pythonhosted.org/packages/5e/05/463c023a39bdeb9bb43a99e7dee2c664cb68d5bb87d14f92482b9f6011cc/numpy-2.2.4-cp311-cp311-win32.whl", hash = "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09", size = 6606368, upload-time = "2025-03-16T18:08:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/10c1d2d82101c468a28adc35de6c77b308f288cfd0b88e1070f15b98e00c/numpy-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91", size = 12947526, upload-time = "2025-03-16T18:09:16.844Z" }, + { url = "https://files.pythonhosted.org/packages/a2/30/182db21d4f2a95904cec1a6f779479ea1ac07c0647f064dea454ec650c42/numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4", size = 20947156, upload-time = "2025-03-16T18:09:51.975Z" }, + { url = "https://files.pythonhosted.org/packages/24/6d/9483566acfbda6c62c6bc74b6e981c777229d2af93c8eb2469b26ac1b7bc/numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854", size = 14133092, upload-time = "2025-03-16T18:10:16.329Z" }, + { url = "https://files.pythonhosted.org/packages/27/f6/dba8a258acbf9d2bed2525cdcbb9493ef9bae5199d7a9cb92ee7e9b2aea6/numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24", size = 5163515, upload-time = "2025-03-16T18:10:26.19Z" }, + { url = "https://files.pythonhosted.org/packages/62/30/82116199d1c249446723c68f2c9da40d7f062551036f50b8c4caa42ae252/numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee", size = 6696558, upload-time = "2025-03-16T18:10:38.996Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b2/54122b3c6df5df3e87582b2e9430f1bdb63af4023c739ba300164c9ae503/numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", size = 14084742, upload-time = "2025-03-16T18:11:02.76Z" }, + { url = "https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", size = 16134051, upload-time = "2025-03-16T18:11:32.767Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/efd47800e4affc993e8be50c1b768de038363dd88865920439ef7b422c60/numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", size = 15578972, upload-time = "2025-03-16T18:11:59.877Z" }, + { url = "https://files.pythonhosted.org/packages/04/1e/f8bb88f6157045dd5d9b27ccf433d016981032690969aa5c19e332b138c0/numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", size = 17898106, upload-time = "2025-03-16T18:12:31.487Z" }, + { url = "https://files.pythonhosted.org/packages/2b/93/df59a5a3897c1f036ae8ff845e45f4081bb06943039ae28a3c1c7c780f22/numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", size = 6311190, upload-time = "2025-03-16T18:12:44.46Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/8c4f928741c2a8efa255fdc7e9097527c6dc4e4df147e3cadc5d9357ce85/numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", size = 12644305, upload-time = "2025-03-16T18:13:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623, upload-time = "2025-03-16T18:13:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681, upload-time = "2025-03-16T18:14:08.031Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759, upload-time = "2025-03-16T18:14:18.613Z" }, + { url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092, upload-time = "2025-03-16T18:14:31.386Z" }, + { url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422, upload-time = "2025-03-16T18:14:54.83Z" }, + { url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202, upload-time = "2025-03-16T18:15:22.035Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131, upload-time = "2025-03-16T18:15:48.546Z" }, + { url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270, upload-time = "2025-03-16T18:16:20.274Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141, upload-time = "2025-03-16T18:20:15.297Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885, upload-time = "2025-03-16T18:20:36.982Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829, upload-time = "2025-03-16T18:16:56.191Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419, upload-time = "2025-03-16T18:17:22.811Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414, upload-time = "2025-03-16T18:17:34.066Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379, upload-time = "2025-03-16T18:17:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725, upload-time = "2025-03-16T18:18:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638, upload-time = "2025-03-16T18:18:40.749Z" }, + { url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717, upload-time = "2025-03-16T18:19:04.512Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998, upload-time = "2025-03-16T18:19:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896, upload-time = "2025-03-16T18:19:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119, upload-time = "2025-03-16T18:20:03.94Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5c/f09c33a511aff41a098e6ef3498465d95f6360621034a3d95f47edbc9119/numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8", size = 21081956, upload-time = "2025-03-16T18:21:12.955Z" }, + { url = "https://files.pythonhosted.org/packages/ba/30/74c48b3b6494c4b820b7fa1781d441e94d87a08daa5b35d222f06ba41a6f/numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c", size = 6827143, upload-time = "2025-03-16T18:21:26.748Z" }, + { url = "https://files.pythonhosted.org/packages/54/f5/ab0d2f48b490535c7a80e05da4a98902b632369efc04f0e47bb31ca97d8f/numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d", size = 16233350, upload-time = "2025-03-16T18:21:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/2f6d8c1f8e45d496bca6baaec93208035faeb40d5735c25afac092ec9a12/numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d", size = 12857565, upload-time = "2025-03-16T18:22:17.631Z" }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pythonnet" +source = { editable = "." } +dependencies = [ + { name = "clr-loader" }, +] + +[package.dev-dependencies] +dev = [ + { name = "find-libpython" }, + { name = "numpy" }, + { name = "psutil" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "clr-loader", specifier = ">=0.2.7,<0.3.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "find-libpython", specifier = ">=0.3" }, + { name = "numpy", marker = "python_full_version < '3.10'", specifier = "<2" }, + { name = "numpy", marker = "python_full_version >= '3.10'", specifier = ">=2" }, + { name = "psutil" }, + { name = "pytest", specifier = ">=6" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] From 5af3b273753c5db4c4dc6770558a6ddb971a6bd5 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 26 Oct 2025 18:35:06 +0100 Subject: [PATCH 60/77] Disable 32bit tests for now --- .github/workflows/main.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 11762a875..46b1c0c7c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,9 +16,11 @@ jobs: fail-fast: false matrix: os: - - category: windows - platform: x86 - instance: windows-latest + # Disabled for now, will require some work (#2653) + # + # - category: windows + # platform: x86 + # instance: windows-latest - category: windows platform: x64 @@ -38,14 +40,6 @@ jobs: python: ["3.10", "3.11", "3.12", "3.13"] - # This fails in pytest with: - # CSC : error CS4023: /platform:anycpu32bitpreferred can only be used with /t:exe, /t:winexe and /t:appcontainerexe [D:\a\pythonnet\pythonnet\src\runtime\Python.Runtime.csproj] - exclude: - - os: - category: windows - platform: x86 - python: "3.13" - steps: - name: Set Environment on macOS uses: maxim-lobanov/setup-xamarin@v1 From ec651f13e42fcb32346f371bd1fd5df50ff8a85e Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 26 Oct 2025 18:38:39 +0100 Subject: [PATCH 61/77] Always run the embedding tests, and run them separately for Mono and .NET Core --- .github/workflows/main.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 46b1c0c7c..57ef57682 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,18 +63,21 @@ jobs: activate-environment: true enable-cache: true - - name: Embedding tests - run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/ + - name: Embedding tests (Mono/.NET Framework) + run: dotnet test --runtime any-${{ matrix.os.platform }} --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-${{ matrix.os.platform }} --framework net8.0 --logger "console;verbosity=detailed" src/embed_tests/ + if: always() + - name: Python Tests (Mono) if: ${{ matrix.os.category != 'windows' }} run: pytest --runtime mono - # TODO: Run these tests on Windows x86 - name: Python Tests (.NET Core) - if: ${{ matrix.os.platform == 'x64' }} run: pytest --runtime coreclr - name: Python Tests (.NET Framework) @@ -82,6 +85,4 @@ jobs: run: pytest --runtime netfx - name: Python tests run from .NET - run: uv run dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ - - # TODO: Run mono tests on Windows? + run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ From 13dff92a39e2d3b13331171c6b9db269cfa242ba Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 26 Oct 2025 18:54:21 +0100 Subject: [PATCH 62/77] Synchronize the environment --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 57ef57682..8c700af49 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,9 @@ jobs: activate-environment: true enable-cache: true + - name: Synchronize the virtual environment + run: uv sync + - name: Embedding tests (Mono/.NET Framework) run: dotnet test --runtime any-${{ matrix.os.platform }} --framework net472 --logger "console;verbosity=detailed" src/embed_tests/ if: always() From 85c8c2cff42b69253cd032fba2a2bf269f8e46bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:55:05 +0100 Subject: [PATCH 63/77] Bump astral-sh/setup-uv from 6 to 7 (#2656) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c700af49..f2611e77d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,7 +56,7 @@ jobs: dotnet-version: '8.0.x' - name: Set up Python ${{ matrix.python }} - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: architecture: ${{ matrix.os.platform }} python-version: ${{ matrix.python }} From 5276ac6b16ded87226258049789446152617a37b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:19:13 +0100 Subject: [PATCH 64/77] Bump actions/checkout from 5 to 6 (#2663) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/nuget-preview.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2b1dda732..3937d85e0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Doxygen Action uses: mattnotmitt/doxygen-action@1.12.0 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f2611e77d..ca2562d04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,7 @@ jobs: mono-version: latest - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 diff --git a/.github/workflows/nuget-preview.yml b/.github/workflows/nuget-preview.yml index 4d1476560..6de97d50e 100644 --- a/.github/workflows/nuget-preview.yml +++ b/.github/workflows/nuget-preview.yml @@ -21,7 +21,7 @@ jobs: echo "DATE_VER=$(date "+%Y-%m-%d")" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 From b871d3532bbc1a17f949949aa61cfc0d84316244 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 22 Oct 2025 22:11:27 +0200 Subject: [PATCH 65/77] Only init/shutdown Python once --- Directory.Build.props | 2 +- src/embed_tests/CallableObject.cs | 25 +++++---- src/embed_tests/ClassManagerTests.cs | 12 ---- src/embed_tests/CodecGroups.cs | 62 ++++++++++----------- src/embed_tests/Codecs.cs | 12 ---- src/embed_tests/{dynamic.cs => Dynamic.cs} | 12 ---- src/embed_tests/ExtensionTypes.cs | 14 +---- src/embed_tests/GlobalTestsSetup.cs | 1 + src/embed_tests/Inheritance.cs | 52 ++++++++++------- src/embed_tests/Inspect.cs | 14 +---- src/embed_tests/Modules.cs | 48 ++++++++-------- src/embed_tests/NumPyTests.cs | 5 +- src/embed_tests/Python.EmbeddingTest.csproj | 4 ++ src/embed_tests/References.cs | 16 +----- src/embed_tests/TestCallbacks.cs | 12 +--- src/embed_tests/TestConverter.cs | 46 ++++++--------- src/embed_tests/TestCustomMarshal.cs | 18 +----- src/embed_tests/TestFinalizer.cs | 27 +++++---- src/embed_tests/TestGILState.cs | 12 ---- src/embed_tests/TestInstanceWrapping.cs | 16 +----- src/embed_tests/TestInterrupt.cs | 12 ++-- src/embed_tests/TestNamedArguments.cs | 16 +----- src/embed_tests/TestNativeTypeOffset.cs | 12 ---- src/embed_tests/TestOperator.cs | 3 +- src/embed_tests/TestPyBuffer.cs | 21 ++++--- src/embed_tests/TestPyFloat.cs | 32 ++++------- src/embed_tests/pyimport.cs | 18 +++--- src/embed_tests/pyinitialize.cs | 6 +- src/embed_tests/pyrunstring.cs | 20 ++----- 29 files changed, 193 insertions(+), 357 deletions(-) rename src/embed_tests/{dynamic.cs => Dynamic.cs} (95%) diff --git a/Directory.Build.props b/Directory.Build.props index 0288a4d64..5f8157f2e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ Copyright (c) 2006-2025 The Contributors of the Python.NET Project pythonnet Python.NET - 10.0 + 12.0 false $([System.IO.File]::ReadAllText("$(MSBuildThisFileDirectory)version.txt").Trim()) $(FullVersion.Split('-', 2)[0]) diff --git a/src/embed_tests/CallableObject.cs b/src/embed_tests/CallableObject.cs index 8466f5ad8..d450598d2 100644 --- a/src/embed_tests/CallableObject.cs +++ b/src/embed_tests/CallableObject.cs @@ -9,34 +9,37 @@ namespace Python.EmbeddingTest { public class CallableObject { + IPythonBaseTypeProvider BaseTypeProvider; + [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); using var locals = new PyDict(); PythonEngine.Exec(CallViaInheritance.BaseClassSource, locals: locals); - CustomBaseTypeProvider.BaseClass = new PyType(locals[CallViaInheritance.BaseClassName]); - PythonEngine.InteropConfiguration.PythonBaseTypeProviders.Add(new CustomBaseTypeProvider()); + BaseTypeProvider = new CustomBaseTypeProvider(new PyType(locals[CallViaInheritance.BaseClassName])); + PythonEngine.InteropConfiguration.PythonBaseTypeProviders.Add(BaseTypeProvider); } [OneTimeTearDown] public void Dispose() { - PythonEngine.Shutdown(); + PythonEngine.InteropConfiguration.PythonBaseTypeProviders.Remove(BaseTypeProvider); } + [Test] public void CallMethodMakesObjectCallable() { var doubler = new DerivedDoubler(); dynamic applyObjectTo21 = PythonEngine.Eval("lambda o: o(21)"); - Assert.AreEqual(doubler.__call__(21), (int)applyObjectTo21(doubler.ToPython())); + Assert.That((int)applyObjectTo21(doubler.ToPython()), Is.EqualTo(doubler.__call__(21))); } + [Test] public void CallMethodCanBeInheritedFromPython() { var callViaInheritance = new CallViaInheritance(); dynamic applyObjectTo14 = PythonEngine.Eval("lambda o: o(14)"); - Assert.AreEqual(callViaInheritance.Call(14), (int)applyObjectTo14(callViaInheritance.ToPython())); + Assert.That((int)applyObjectTo14(callViaInheritance.ToPython()), Is.EqualTo(callViaInheritance.Call(14))); } [Test] @@ -48,7 +51,7 @@ public void CanOverwriteCall() scope.Exec("orig_call = o.Call"); scope.Exec("o.Call = lambda a: orig_call(a*7)"); int result = scope.Eval("o.Call(5)"); - Assert.AreEqual(105, result); + Assert.That(result, Is.EqualTo(105)); } class Doubler @@ -71,16 +74,14 @@ class {BaseClassName}(MyCallableBase): pass public int Call(int arg) => 3 * arg; } - class CustomBaseTypeProvider : IPythonBaseTypeProvider + class CustomBaseTypeProvider(PyType BaseClass) : IPythonBaseTypeProvider { - internal static PyType BaseClass; - public IEnumerable GetBaseTypes(Type type, IList existingBases) { - Assert.Greater(BaseClass.Refcount, 0); + Assert.That(BaseClass.Refcount, Is.GreaterThan(0)); return type != typeof(CallViaInheritance) ? existingBases - : new[] { BaseClass }; + : [BaseClass]; } } } diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 72025a28b..83bfa7bc2 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class ClassManagerTests { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void NestedClassDerivingFromParent() { diff --git a/src/embed_tests/CodecGroups.cs b/src/embed_tests/CodecGroups.cs index 689e5b24c..22ed0df72 100644 --- a/src/embed_tests/CodecGroups.cs +++ b/src/embed_tests/CodecGroups.cs @@ -20,7 +20,7 @@ public void GetEncodersByType() }; var got = group.GetEncoders(typeof(Uri)).ToArray(); - CollectionAssert.AreEqual(new[]{encoder1, encoder2}, got); + Assert.That(got, Is.EqualTo(new[] { encoder1, encoder2 }).AsCollection); } [Test] @@ -31,9 +31,13 @@ public void CanEncode() new ObjectToEncoderInstanceEncoder(), }; - Assert.IsTrue(group.CanEncode(typeof(Tuple))); - Assert.IsTrue(group.CanEncode(typeof(Uri))); - Assert.IsFalse(group.CanEncode(typeof(string))); + Assert.Multiple(() => + { + Assert.That(group.CanEncode(typeof(Tuple)), Is.True); + Assert.That(group.CanEncode(typeof(Uri)), Is.True); + Assert.That(group.CanEncode(typeof(string)), Is.False); + }); + } [Test] @@ -50,12 +54,12 @@ public void Encodes() var uri = group.TryEncode(new Uri("data:")); var clrObject = (CLRObject)ManagedType.GetManagedObject(uri); - Assert.AreSame(encoder1, clrObject.inst); - Assert.AreNotSame(encoder2, clrObject.inst); + Assert.That(clrObject.inst, Is.SameAs(encoder1)); + Assert.That(clrObject.inst, Is.Not.SameAs(encoder2)); var tuple = group.TryEncode(Tuple.Create(1)); clrObject = (CLRObject)ManagedType.GetManagedObject(tuple); - Assert.AreSame(encoder0, clrObject.inst); + Assert.That(clrObject.inst, Is.SameAs(encoder0)); } [Test] @@ -72,11 +76,11 @@ public void GetDecodersByTypes() }; var decoder = group.GetDecoder(pyfloat, typeof(string)); - Assert.AreSame(decoder2, decoder); + Assert.That(decoder, Is.SameAs(decoder2)); decoder = group.GetDecoder(pystr, typeof(string)); - Assert.IsNull(decoder); + Assert.That(decoder, Is.Null); decoder = group.GetDecoder(pyint, typeof(long)); - Assert.AreSame(decoder1, decoder); + Assert.That(decoder, Is.SameAs(decoder1)); } [Test] public void CanDecode() @@ -91,10 +95,14 @@ public void CanDecode() decoder2, }; - Assert.IsTrue(group.CanDecode(pyint, typeof(long))); - Assert.IsFalse(group.CanDecode(pyint, typeof(int))); - Assert.IsTrue(group.CanDecode(pyfloat, typeof(string))); - Assert.IsFalse(group.CanDecode(pystr, typeof(string))); + Assert.Multiple(() => + { + Assert.That(group.CanDecode(pyint, typeof(long))); + Assert.That(group.CanDecode(pyint, typeof(int)), Is.False); + Assert.That(group.CanDecode(pyfloat, typeof(string))); + Assert.That(group.CanDecode(pystr, typeof(string)), Is.False); + }); + } [Test] @@ -109,24 +117,14 @@ public void Decodes() decoder2, }; - Assert.IsTrue(group.TryDecode(new PyInt(10), out long longResult)); - Assert.AreEqual(42, longResult); - Assert.IsTrue(group.TryDecode(new PyFloat(10), out string strResult)); - Assert.AreSame("atad:", strResult); - - Assert.IsFalse(group.TryDecode(new PyInt(10), out int _)); - } - - [SetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [TearDown] - public void Dispose() - { - PythonEngine.Shutdown(); + Assert.Multiple(() => + { + Assert.That(group.TryDecode(new PyInt(10), out long longResult)); + Assert.That(longResult, Is.EqualTo(42)); + Assert.That(group.TryDecode(new PyFloat(10), out string strResult)); + Assert.That(strResult, Is.SameAs("atad:")); + Assert.That(group.TryDecode(new PyInt(10), out int _), Is.False); + }); } } } diff --git a/src/embed_tests/Codecs.cs b/src/embed_tests/Codecs.cs index c8b8ecb6e..cecc6825d 100644 --- a/src/embed_tests/Codecs.cs +++ b/src/embed_tests/Codecs.cs @@ -8,18 +8,6 @@ namespace Python.EmbeddingTest { public class Codecs { - [SetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [TearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TupleConversionsGeneric() { diff --git a/src/embed_tests/dynamic.cs b/src/embed_tests/Dynamic.cs similarity index 95% rename from src/embed_tests/dynamic.cs rename to src/embed_tests/Dynamic.cs index 6e3bfc4cb..174167118 100644 --- a/src/embed_tests/dynamic.cs +++ b/src/embed_tests/Dynamic.cs @@ -8,18 +8,6 @@ namespace Python.EmbeddingTest { public class DynamicTest { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - /// /// Set the attribute of a PyObject with a .NET object. /// diff --git a/src/embed_tests/ExtensionTypes.cs b/src/embed_tests/ExtensionTypes.cs index 803845960..3e8ead142 100644 --- a/src/embed_tests/ExtensionTypes.cs +++ b/src/embed_tests/ExtensionTypes.cs @@ -8,18 +8,6 @@ namespace Python.EmbeddingTest; public class ExtensionTypes { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void WeakrefIsNone_AfterBoundMethodIsGone() { @@ -27,6 +15,6 @@ public void WeakrefIsNone_AfterBoundMethodIsGone() var boundMethod = new UriBuilder().ToPython().GetAttr(nameof(UriBuilder.GetHashCode)); var weakref = makeref.Invoke(boundMethod); boundMethod.Dispose(); - Assert.IsTrue(weakref.Invoke().IsNone()); + Assert.That(weakref.Invoke().IsNone(), Is.True); } } diff --git a/src/embed_tests/GlobalTestsSetup.cs b/src/embed_tests/GlobalTestsSetup.cs index dff58b978..4f681dd9f 100644 --- a/src/embed_tests/GlobalTestsSetup.cs +++ b/src/embed_tests/GlobalTestsSetup.cs @@ -13,6 +13,7 @@ public partial class GlobalTestsSetup public void GlobalSetup() { Finalizer.Instance.ErrorHandler += FinalizerErrorHandler; + PythonEngine.Initialize(); } private void FinalizerErrorHandler(object sender, Finalizer.ErrorArgs e) diff --git a/src/embed_tests/Inheritance.cs b/src/embed_tests/Inheritance.cs index ebbc24dc4..1074fa288 100644 --- a/src/embed_tests/Inheritance.cs +++ b/src/embed_tests/Inheritance.cs @@ -9,23 +9,31 @@ namespace Python.EmbeddingTest { public class Inheritance { + ExtraBaseTypeProvider ExtraBaseTypeProvider; + NoEffectBaseTypeProvider NoEffectBaseTypeProvider; + + [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); using var locals = new PyDict(); PythonEngine.Exec(InheritanceTestBaseClassWrapper.ClassSourceCode, locals: locals); - ExtraBaseTypeProvider.ExtraBase = new PyType(locals[InheritanceTestBaseClassWrapper.ClassName]); + + NoEffectBaseTypeProvider = new NoEffectBaseTypeProvider(); + ExtraBaseTypeProvider = new ExtraBaseTypeProvider(new PyType(locals[InheritanceTestBaseClassWrapper.ClassName])); + var baseTypeProviders = PythonEngine.InteropConfiguration.PythonBaseTypeProviders; - baseTypeProviders.Add(new ExtraBaseTypeProvider()); - baseTypeProviders.Add(new NoEffectBaseTypeProvider()); + baseTypeProviders.Add(ExtraBaseTypeProvider); + baseTypeProviders.Add(NoEffectBaseTypeProvider); } [OneTimeTearDown] public void Dispose() { - ExtraBaseTypeProvider.ExtraBase.Dispose(); - PythonEngine.Shutdown(); + var baseTypeProviders = PythonEngine.InteropConfiguration.PythonBaseTypeProviders; + baseTypeProviders.Remove(NoEffectBaseTypeProvider); + baseTypeProviders.Remove(ExtraBaseTypeProvider); + ExtraBaseTypeProvider.Dispose(); } [Test] @@ -33,7 +41,7 @@ public void ExtraBase_PassesInstanceCheck() { var inherited = new Inherited(); bool properlyInherited = PyIsInstance(inherited, ExtraBaseTypeProvider.ExtraBase); - Assert.IsTrue(properlyInherited); + Assert.That(properlyInherited, Is.True); } static dynamic PyIsInstance => PythonEngine.Eval("isinstance"); @@ -44,7 +52,7 @@ public void InheritingWithExtraBase_CreatesNewClass() PyObject a = ExtraBaseTypeProvider.ExtraBase; var inherited = new Inherited(); PyObject inheritedClass = inherited.ToPython().GetAttr("__class__"); - Assert.IsFalse(PythonReferenceComparer.Instance.Equals(a, inheritedClass)); + Assert.That(PythonReferenceComparer.Instance.Equals(a, inheritedClass), Is.False); } [Test] @@ -56,7 +64,7 @@ public void InheritedFromInheritedClassIsSelf() PyObject b = scope.Eval("B"); PyObject bInstance = b.Invoke(); PyObject bInstanceClass = bInstance.GetAttr("__class__"); - Assert.IsTrue(PythonReferenceComparer.Instance.Equals(b, bInstanceClass)); + Assert.That(PythonReferenceComparer.Instance.Equals(b, bInstanceClass), Is.True); } // https://github.com/pythonnet/pythonnet/issues/1420 @@ -76,7 +84,7 @@ public void Grandchild_PassesExtraBaseInstanceCheck() PyObject b = scope.Eval("B"); PyObject bInst = b.Invoke(); bool properlyInherited = PyIsInstance(bInst, ExtraBaseTypeProvider.ExtraBase); - Assert.IsTrue(properlyInherited); + Assert.That(properlyInherited, Is.True); } [Test] @@ -84,7 +92,7 @@ public void CallInheritedClrMethod_WithExtraPythonBase() { var instance = new Inherited().ToPython(); string result = instance.InvokeMethod(nameof(PythonWrapperBase.WrapperBaseMethod)).As(); - Assert.AreEqual(result, nameof(PythonWrapperBase.WrapperBaseMethod)); + Assert.That(nameof(PythonWrapperBase.WrapperBaseMethod), Is.EqualTo(result)); } [Test] @@ -94,7 +102,7 @@ public void CallExtraBaseMethod() using var scope = Py.CreateScope(); scope.Set(nameof(instance), instance); int actual = instance.ToPython().InvokeMethod("callVirt").As(); - Assert.AreEqual(expected: Inherited.OverridenVirtValue, actual); + Assert.That(actual, Is.EqualTo(Inherited.OverridenVirtValue)); } [Test] @@ -105,7 +113,7 @@ public void SetAdHocAttributes_WhenExtraBasePresent() scope.Set(nameof(instance), instance); scope.Exec($"super({nameof(instance)}.__class__, {nameof(instance)}).set_x_to_42()"); int actual = scope.Eval($"{nameof(instance)}.{nameof(Inherited.XProp)}"); - Assert.AreEqual(expected: Inherited.X, actual); + Assert.That(actual, Is.EqualTo(Inherited.X)); } // https://github.com/pythonnet/pythonnet/issues/1476 @@ -115,9 +123,9 @@ public void BaseClearIsCalled() using var scope = Py.CreateScope(); scope.Set("exn", new Exception("42")); var msg = scope.Eval("exn.args[0]"); - Assert.AreEqual(2, msg.Refcount); + Assert.That(msg.Refcount, Is.EqualTo(2)); scope.Set("exn", null); - Assert.AreEqual(1, msg.Refcount); + Assert.That(msg.Refcount, Is.EqualTo(1)); } // https://github.com/pythonnet/pythonnet/issues/1455 @@ -126,18 +134,24 @@ public void PropertyAccessorOverridden() { using var derived = new PropertyAccessorDerived().ToPython(); derived.SetAttr(nameof(PropertyAccessorDerived.VirtualProp), "hi".ToPython()); - Assert.AreEqual("HI", derived.GetAttr(nameof(PropertyAccessorDerived.VirtualProp)).As()); + Assert.That(derived.GetAttr(nameof(PropertyAccessorDerived.VirtualProp)).As(), Is.EqualTo("HI")); } } - class ExtraBaseTypeProvider : IPythonBaseTypeProvider + class ExtraBaseTypeProvider(PyType ExtraBase) : IPythonBaseTypeProvider, IDisposable { - internal static PyType ExtraBase; + public PyType ExtraBase { get; } = ExtraBase; + + public void Dispose() + { + ExtraBase.Dispose(); + } + public IEnumerable GetBaseTypes(Type type, IList existingBases) { if (type == typeof(InheritanceTestBaseClassWrapper)) { - return new[] { PyType.Get(type.BaseType), ExtraBase }; + return [PyType.Get(type.BaseType), ExtraBase]; } return existingBases; } diff --git a/src/embed_tests/Inspect.cs b/src/embed_tests/Inspect.cs index 8ff94e02c..0c4ce43f3 100644 --- a/src/embed_tests/Inspect.cs +++ b/src/embed_tests/Inspect.cs @@ -9,18 +9,6 @@ namespace Python.EmbeddingTest { public class Inspect { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void InstancePropertiesVisibleOnClass() { @@ -28,7 +16,7 @@ public void InstancePropertiesVisibleOnClass() var uriClass = uri.GetPythonType(); var property = uriClass.GetAttr(nameof(Uri.AbsoluteUri)); var pyProp = (PropertyObject)ManagedType.GetManagedObject(property.Reference); - Assert.AreEqual(nameof(Uri.AbsoluteUri), pyProp.info.Value.Name); + Assert.That(pyProp.info.Value.Name, Is.EqualTo(nameof(Uri.AbsoluteUri))); } [Test] diff --git a/src/embed_tests/Modules.cs b/src/embed_tests/Modules.cs index 6cab4dd07..fc9b8f398 100644 --- a/src/embed_tests/Modules.cs +++ b/src/embed_tests/Modules.cs @@ -50,7 +50,7 @@ public void TestEval() { ps.Set("a", 1); var result = ps.Eval("a + 2"); - Assert.AreEqual(3, result); + Assert.That(result, Is.EqualTo(3)); } } @@ -66,7 +66,7 @@ public void TestExec() ps.Set("cc", 10); //declare a local variable ps.Exec("aa = bb + cc + 3"); var result = ps.Get("aa"); - Assert.AreEqual(113, result); + Assert.That(result, Is.EqualTo(113)); } } @@ -83,7 +83,7 @@ public void TestCompileExpression() ps.Set("cc", 10); //declare a local variable PyObject script = PythonEngine.Compile("bb + cc + 3", "", RunFlagType.Eval); var result = ps.Execute(script); - Assert.AreEqual(113, result); + Assert.That(result, Is.EqualTo(113)); } } @@ -102,7 +102,7 @@ public void TestCompileStatements() PyObject script = PythonEngine.Compile("aa = bb + cc + 3", "", RunFlagType.File); ps.Execute(script); var result = ps.Get("aa"); - Assert.AreEqual(113, result); + Assert.That(result, Is.EqualTo(113)); } } @@ -123,7 +123,7 @@ public void TestScopeFunction() dynamic func1 = ps.Get("func1"); func1(); //call the function, it can be called any times var result = ps.Get("bb"); - Assert.AreEqual(100, result); + Assert.That(result, Is.EqualTo(100)); ps.Set("bb", 100); ps.Set("cc", 10); @@ -134,7 +134,7 @@ public void TestScopeFunction() dynamic func2 = ps.Get("func2"); func2(); result = ps.Get("bb"); - Assert.AreEqual(20, result); + Assert.That(result, Is.EqualTo(20)); } } @@ -219,10 +219,10 @@ public void TestCreateModuleWithFilename() using var modWithName = PyModule.FromString("mod_with_name", "", "some_filename"); - Assert.AreEqual("none", mod.Get("__file__")); - Assert.AreEqual("none", modWithoutName.Get("__file__")); - Assert.AreEqual("none", modNullName.Get("__file__")); - Assert.AreEqual("some_filename", modWithName.Get("__file__")); + Assert.That(mod.Get("__file__"), Is.EqualTo("none")); + Assert.That(modWithoutName.Get("__file__"), Is.EqualTo("none")); + Assert.That(modNullName.Get("__file__"), Is.EqualTo("none")); + Assert.That(modWithName.Get("__file__"), Is.EqualTo("some_filename")); } /// @@ -235,17 +235,17 @@ public void TestImportModule() using (Py.GIL()) { dynamic sys = ps.Import("sys"); - Assert.IsTrue(ps.Contains("sys")); + Assert.That(ps.Contains("sys"), Is.True); ps.Exec("sys.attr1 = 2"); var value1 = ps.Eval("sys.attr1"); var value2 = sys.attr1.As(); - Assert.AreEqual(2, value1); + Assert.That(value1, Is.EqualTo(2)); Assert.AreEqual(2, value2); //import as ps.Import("sys", "sys1"); - Assert.IsTrue(ps.Contains("sys1")); + Assert.That(ps.Contains("sys1"), Is.True); } } @@ -266,10 +266,10 @@ public void TestImportScope() scope.Import(ps, "ps"); scope.Exec("aa = ps.bb + ps.cc + 3"); var result = scope.Get("aa"); - Assert.AreEqual(113, result); + Assert.That(result, Is.EqualTo(113)); } - Assert.IsFalse(ps.Contains("aa")); + Assert.That(ps.Contains("aa"), Is.False); } } @@ -289,10 +289,10 @@ public void TestImportAllFromScope() { scope.Exec("aa = bb + cc + 3"); var result = scope.Get("aa"); - Assert.AreEqual(113, result); + Assert.That(result, Is.EqualTo(113)); } - Assert.IsFalse(ps.Contains("aa")); + Assert.That(ps.Contains("aa"), Is.False); } } @@ -345,22 +345,22 @@ public void TestVariables() { (ps.Variables() as dynamic)["ee"] = new PyInt(200); var a0 = ps.Get("ee"); - Assert.AreEqual(200, a0); + Assert.That(a0, Is.EqualTo(200)); ps.Exec("locals()['ee'] = 210"); var a1 = ps.Get("ee"); - Assert.AreEqual(210, a1); + Assert.That(a1, Is.EqualTo(210)); ps.Exec("globals()['ee'] = 220"); var a2 = ps.Get("ee"); - Assert.AreEqual(220, a2); + Assert.That(a2, Is.EqualTo(220)); using (var item = ps.Variables()) { item["ee"] = new PyInt(230); } var a3 = ps.Get("ee"); - Assert.AreEqual(230, a3); + Assert.That(a3, Is.EqualTo(230)); } } @@ -420,7 +420,7 @@ public void TestThread() using (Py.GIL()) { var result = ps.Get("res"); - Assert.AreEqual(101 * th_cnt, result); + Assert.That(result, Is.EqualTo(101 * th_cnt)); } } finally @@ -434,7 +434,7 @@ public void TestCreate() { using var scope = Py.CreateScope(); - Assert.IsFalse(PyModule.SysModules.HasKey("testmod")); + Assert.That(PyModule.SysModules.HasKey("testmod"), Is.False); PyModule testmod = new PyModule("testmod"); @@ -448,7 +448,7 @@ public void TestCreate() ); scope.Execute(code); - Assert.IsTrue(scope.TryGet("x", out dynamic x)); + Assert.That(scope.TryGet("x", out dynamic x), Is.True); Assert.AreEqual("True", x.ToString()); } diff --git a/src/embed_tests/NumPyTests.cs b/src/embed_tests/NumPyTests.cs index e102ddb99..6f4a85716 100644 --- a/src/embed_tests/NumPyTests.cs +++ b/src/embed_tests/NumPyTests.cs @@ -13,14 +13,13 @@ public class NumPyTests [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); TupleCodec.Register(); } [OneTimeTearDown] public void Dispose() { - PythonEngine.Shutdown(); + PyObjectConversions.Reset(); } [Test] @@ -32,7 +31,7 @@ public void TestReadme() StringAssert.StartsWith("-0.95892", sin(5).ToString()); double c = (double)(np.cos(5) + sin(5)); - Assert.AreEqual(-0.675262, c, 0.01); + Assert.That(c, Is.EqualTo(-0.675262).Within(0.01)); dynamic a = np.array(new List { 1, 2, 3 }); Assert.AreEqual("float64", a.dtype.ToString()); diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index da6799912..cb3b52052 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -20,6 +20,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/embed_tests/References.cs b/src/embed_tests/References.cs index c416c5ebe..af9e74336 100644 --- a/src/embed_tests/References.cs +++ b/src/embed_tests/References.cs @@ -5,18 +5,6 @@ namespace Python.EmbeddingTest public class References { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void MoveToPyObject_SetsNull() { @@ -24,10 +12,10 @@ public void MoveToPyObject_SetsNull() NewReference reference = Runtime.PyDict_Items(dict.Reference); try { - Assert.IsFalse(reference.IsNull()); + Assert.That(reference.IsNull(), Is.False); using (reference.MoveToPyObject()) - Assert.IsTrue(reference.IsNull()); + Assert.That(reference.IsNull(), Is.True); } finally { diff --git a/src/embed_tests/TestCallbacks.cs b/src/embed_tests/TestCallbacks.cs index 88b84d0c3..7e9583364 100644 --- a/src/embed_tests/TestCallbacks.cs +++ b/src/embed_tests/TestCallbacks.cs @@ -5,16 +5,6 @@ namespace Python.EmbeddingTest { public class TestCallbacks { - [OneTimeSetUp] - public void SetUp() { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() { - PythonEngine.Shutdown(); - } - [Test] public void TestNoOverloadException() { int passed = 0; @@ -23,7 +13,7 @@ public void TestNoOverloadException() { using dynamic callWith42 = PythonEngine.Eval("lambda f: f([42])"); using var pyFunc = aFunctionThatCallsIntoPython.ToPython(); var error = Assert.Throws(() => callWith42(pyFunc)); - Assert.AreEqual("TypeError", error.Type.Name); + Assert.That(error.Type.Name, Is.EqualTo("TypeError")); string expectedArgTypes = "()"; StringAssert.EndsWith(expectedArgTypes, error.Message); error.Traceback.Dispose(); diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index a59b9c97b..3feced8d0 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -23,18 +23,6 @@ public class TestConverter typeof(ulong) }; - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestConvertSingleToManaged( [Values(float.PositiveInfinity, float.NegativeInfinity, float.MinValue, float.MaxValue, float.NaN, @@ -45,8 +33,8 @@ public void TestConvertSingleToManaged( object convertedValue; var converted = Converter.ToManaged(pyFloat, typeof(float), out convertedValue, false); - Assert.IsTrue(converted); - Assert.IsTrue(((float) convertedValue).Equals(testValue)); + Assert.That(converted, Is.True); + Assert.That(((float)convertedValue).Equals(testValue), Is.True); } [Test] @@ -59,8 +47,8 @@ public void TestConvertDoubleToManaged( object convertedValue; var converted = Converter.ToManaged(pyFloat, typeof(double), out convertedValue, false); - Assert.IsTrue(converted); - Assert.IsTrue(((double) convertedValue).Equals(testValue)); + Assert.That(converted, Is.True); + Assert.That(((double)convertedValue).Equals(testValue), Is.True); } [Test] @@ -79,10 +67,10 @@ public void CovertTypeError() try { bool res = Converter.ToManaged(s, type, out value, true); - Assert.IsFalse(res); + Assert.That(res, Is.False); var bo = Exceptions.ExceptionMatches(Exceptions.TypeError); - Assert.IsTrue(Exceptions.ExceptionMatches(Exceptions.TypeError) - || Exceptions.ExceptionMatches(Exceptions.ValueError)); + Assert.That(Exceptions.ExceptionMatches(Exceptions.TypeError) + || Exceptions.ExceptionMatches(Exceptions.ValueError), Is.True); } finally { @@ -104,8 +92,8 @@ public void ConvertOverflow() foreach (var type in _numTypes) { bool res = Converter.ToManaged(largeNum.BorrowOrThrow(), type, out value, true); - Assert.IsFalse(res); - Assert.IsTrue(Exceptions.ExceptionMatches(Exceptions.OverflowError)); + Assert.That(res, Is.False); + Assert.That(Exceptions.ExceptionMatches(Exceptions.OverflowError), Is.True); Exceptions.Clear(); } } @@ -129,7 +117,7 @@ public void ToNullable() const int Const = 42; var i = new PyInt(Const); var ni = i.As(); - Assert.AreEqual(Const, ni); + Assert.That(ni, Is.EqualTo(Const)); } [Test] @@ -138,9 +126,9 @@ public void BigIntExplicit() BigInteger val = 42; var i = new PyInt(val); var ni = i.As(); - Assert.AreEqual(val, ni); + Assert.That(ni, Is.EqualTo(val)); var nullable = i.As(); - Assert.AreEqual(val, nullable); + Assert.That(nullable, Is.EqualTo(val)); } [Test] @@ -148,7 +136,7 @@ public void PyIntImplicit() { var i = new PyInt(1); var ni = (PyObject)i.As(); - Assert.IsTrue(PythonReferenceComparer.Instance.Equals(i, ni)); + Assert.That(PythonReferenceComparer.Instance.Equals(i, ni), Is.True); } [Test] @@ -158,7 +146,7 @@ public void ToPyList() list.Append("hello".ToPython()); list.Append("world".ToPython()); var back = list.ToPython().As(); - Assert.AreEqual(list.Length(), back.Length()); + Assert.That(back.Length(), Is.EqualTo(list.Length())); } [Test] @@ -182,7 +170,7 @@ public void RawPyObjectProxy() const string handlePropertyName = nameof(PyObject.Handle); #pragma warning restore CS0612 // Type or member is obsolete var proxiedHandle = pyObjectProxy.GetAttr(handlePropertyName).As(); - Assert.AreEqual(pyObject.DangerousGetAddressOrNull(), proxiedHandle); + Assert.That(proxiedHandle, Is.EqualTo(pyObject.DangerousGetAddressOrNull())); } [Test] @@ -191,7 +179,7 @@ public void GenericToPython() int i = 42; var pyObject = i.ToPythonAs(); var type = pyObject.GetPythonType(); - Assert.AreEqual(nameof(IConvertible), type.Name); + Assert.That(type.Name, Is.EqualTo(nameof(IConvertible))); } // regression for https://github.com/pythonnet/pythonnet/issues/451 @@ -207,7 +195,7 @@ class PyGetListImpl(test.GetListImpl): var pyImpl = scope.Get("PyGetListImpl"); dynamic inst = pyImpl.Invoke(); List result = inst.GetList(); - CollectionAssert.AreEqual(new[] { "testing" }, result); + Assert.That(result, Is.EqualTo(new[] { "testing" }).AsCollection); } } diff --git a/src/embed_tests/TestCustomMarshal.cs b/src/embed_tests/TestCustomMarshal.cs index 312863d0c..3bcb6b2e6 100644 --- a/src/embed_tests/TestCustomMarshal.cs +++ b/src/embed_tests/TestCustomMarshal.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestCustomMarshal { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public static void GetManagedStringTwice() { @@ -27,9 +15,9 @@ public static void GetManagedStringTwice() string s1 = Runtime.Runtime.GetManagedString(op.BorrowOrThrow()); string s2 = Runtime.Runtime.GetManagedString(op.Borrow()); - Assert.AreEqual(1, Runtime.Runtime.Refcount32(op.Borrow())); - Assert.AreEqual(expected, s1); - Assert.AreEqual(expected, s2); + Assert.That(Runtime.Runtime.Refcount32(op.Borrow()), Is.EqualTo(1)); + Assert.That(s1, Is.EqualTo(expected)); + Assert.That(s2, Is.EqualTo(expected)); } } } diff --git a/src/embed_tests/TestFinalizer.cs b/src/embed_tests/TestFinalizer.cs index b748a2244..9ef3e25b6 100644 --- a/src/embed_tests/TestFinalizer.cs +++ b/src/embed_tests/TestFinalizer.cs @@ -17,7 +17,6 @@ public class TestFinalizer public void SetUp() { _oldThreshold = Finalizer.Instance.Threshold; - PythonEngine.Initialize(); Exceptions.Clear(); } @@ -25,7 +24,6 @@ public void SetUp() public void TearDown() { Finalizer.Instance.Threshold = _oldThreshold; - PythonEngine.Shutdown(); } private static void FullGCCollect() @@ -38,7 +36,7 @@ private static void FullGCCollect() [Obsolete("GC tests are not guaranteed")] public void CollectBasicObject() { - Assert.IsTrue(Finalizer.Instance.Enable); + Assert.That(Finalizer.Instance.Enable, Is.True); Finalizer.Instance.Threshold = 1; bool called = false; @@ -49,7 +47,7 @@ public void CollectBasicObject() called = true; }; - Assert.IsFalse(called, "The event handler was called before it was installed"); + Assert.That(called, Is.False, "The event handler was called before it was installed"); Finalizer.Instance.BeforeCollect += handler; IntPtr pyObj = MakeAGarbage(out var shortWeak, out var longWeak); @@ -60,10 +58,10 @@ public void CollectBasicObject() "The referenced object is alive although it should have been collected", shortWeak ); - Assert.IsTrue( + Assert.That( longWeak.IsAlive, - "The reference object is not alive although it should still be", - longWeak + Is.True, + $"The reference object is not alive although it should still be" ); { @@ -83,7 +81,7 @@ public void CollectBasicObject() { Finalizer.Instance.BeforeCollect -= handler; } - Assert.IsTrue(called, "The event handler was not called during finalization"); + Assert.That(called, Is.True, "The event handler was not called during finalization"); Assert.GreaterOrEqual(objectCount, 1); } @@ -93,10 +91,11 @@ public void CollectOnShutdown() { IntPtr op = MakeAGarbage(out var shortWeak, out var longWeak); FullGCCollect(); - Assert.IsFalse(shortWeak.IsAlive); + Assert.That(shortWeak.IsAlive, Is.False); List garbage = Finalizer.Instance.GetCollectedObjects(); Assert.IsNotEmpty(garbage, "The garbage object should be collected"); - Assert.IsTrue(garbage.Contains(op), + Assert.That(garbage.Contains(op), + Is.True, "Garbage should contains the collected object"); PythonEngine.Shutdown(); @@ -133,7 +132,7 @@ private static IntPtr MakeAGarbage(out WeakReference shortWeak, out WeakReferenc handle = obj.Handle; }); garbageGen.Start(); - Assert.IsTrue(garbageGen.Join(TimeSpan.FromSeconds(5)), "Garbage creation timed out"); + Assert.That(garbageGen.Join(TimeSpan.FromSeconds(5)), Is.True, "Garbage creation timed out"); shortWeak = @short; longWeak = @long; return handle; @@ -209,8 +208,8 @@ public void ValidateRefCount() Finalizer.IncorrectRefCntHandler handler = (s, e) => { called = true; - Assert.AreEqual(ptr, e.Handle); - Assert.AreEqual(2, e.ImpactedObjects.Count); + Assert.That(e.Handle, Is.EqualTo(ptr)); + Assert.That(e.ImpactedObjects.Count, Is.EqualTo(2)); // Fix for this test, don't do this on general environment #pragma warning disable CS0618 // Type or member is obsolete Runtime.Runtime.XIncref(e.Reference); @@ -223,7 +222,7 @@ public void ValidateRefCount() ptr = CreateStringGarbage(); FullGCCollect(); Assert.Throws(() => Finalizer.Instance.Collect()); - Assert.IsTrue(called); + Assert.That(called, Is.True); } finally { diff --git a/src/embed_tests/TestGILState.cs b/src/embed_tests/TestGILState.cs index bf6f02dc6..ba2ab500f 100644 --- a/src/embed_tests/TestGILState.cs +++ b/src/embed_tests/TestGILState.cs @@ -17,17 +17,5 @@ public void CanDisposeMultipleTimes() gilState.Dispose(); } } - - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } } } diff --git a/src/embed_tests/TestInstanceWrapping.cs b/src/embed_tests/TestInstanceWrapping.cs index 0a441c823..c6996fd87 100644 --- a/src/embed_tests/TestInstanceWrapping.cs +++ b/src/embed_tests/TestInstanceWrapping.cs @@ -7,18 +7,6 @@ namespace Python.EmbeddingTest { public class TestInstanceWrapping { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - // regression test for https://github.com/pythonnet/pythonnet/issues/811 [Test] public void OverloadResolution_UnknownToObject() @@ -30,7 +18,7 @@ public void OverloadResolution_UnknownToObject() dynamic callWithSelf = PythonEngine.Eval("lambda o: o.ObjOrClass(object())"); callWithSelf(o); - Assert.AreEqual(Overloaded.Object, overloaded.Value); + Assert.That(overloaded.Value, Is.EqualTo(Overloaded.Object)); } } @@ -41,7 +29,7 @@ public void WeakRefIsNone_AfterObjectIsGone() var ub = new UriBuilder().ToPython(); using var weakref = makeref.Invoke(ub); ub.Dispose(); - Assert.IsTrue(weakref.Invoke().IsNone()); + Assert.That(weakref.Invoke().IsNone(), Is.True); } class Base {} diff --git a/src/embed_tests/TestInterrupt.cs b/src/embed_tests/TestInterrupt.cs index e6546adb2..d48f7c73b 100644 --- a/src/embed_tests/TestInterrupt.cs +++ b/src/embed_tests/TestInterrupt.cs @@ -15,7 +15,6 @@ public class TestInterrupt [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); // workaround for assert tlock.locked() warning threading = Py.Import("threading"); } @@ -24,7 +23,6 @@ public void SetUp() public void Dispose() { threading.Dispose(); - PythonEngine.Shutdown(); } [Test] @@ -50,9 +48,9 @@ public void PythonThreadIDStable() } PythonEngine.EndAllowThreads(threadState); - Assert.IsTrue(asyncCall.Wait(TimeSpan.FromSeconds(5)), "Async thread has not finished in time"); + Assert.That(asyncCall.Wait(TimeSpan.FromSeconds(5)), Is.True, "Async thread has not finished in time"); - Assert.AreEqual(pythonThreadID, pythonThreadID2); + Assert.That(pythonThreadID2, Is.EqualTo(pythonThreadID)); Assert.NotZero(pythonThreadID); } @@ -86,13 +84,13 @@ import time PythonEngine.EndAllowThreads(threadState); int interruptReturnValue = PythonEngine.Interrupt((ulong)Interlocked.Read(ref pythonThreadID)); - Assert.AreEqual(1, interruptReturnValue); + Assert.That(interruptReturnValue, Is.EqualTo(1)); threadState = PythonEngine.BeginAllowThreads(); - Assert.IsTrue(asyncCall.Wait(TimeSpan.FromSeconds(5)), "Async thread was not interrupted in time"); + Assert.That(asyncCall.Wait(TimeSpan.FromSeconds(5)), Is.True, "Async thread was not interrupted in time"); PythonEngine.EndAllowThreads(threadState); - Assert.AreEqual(0, asyncCall.Result); + Assert.That(asyncCall.Result, Is.EqualTo(0)); } } } diff --git a/src/embed_tests/TestNamedArguments.cs b/src/embed_tests/TestNamedArguments.cs index c86302038..16d7d5b8f 100644 --- a/src/embed_tests/TestNamedArguments.cs +++ b/src/embed_tests/TestNamedArguments.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestNamedArguments { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - /// /// Test named arguments support through Py.kw method /// @@ -27,7 +15,7 @@ public void TestKeywordArgs() dynamic a = CreateTestClass(); var result = (int)a.Test3(2, Py.kw("a4", 8)); - Assert.AreEqual(12, result); + Assert.That(result, Is.EqualTo(12)); } @@ -40,7 +28,7 @@ public void TestNamedArgs() dynamic a = CreateTestClass(); var result = (int)a.Test3(2, a4: 8); - Assert.AreEqual(12, result); + Assert.That(result, Is.EqualTo(12)); } diff --git a/src/embed_tests/TestNativeTypeOffset.cs b/src/embed_tests/TestNativeTypeOffset.cs index d692c24e6..61b6903c5 100644 --- a/src/embed_tests/TestNativeTypeOffset.cs +++ b/src/embed_tests/TestNativeTypeOffset.cs @@ -13,18 +13,6 @@ namespace Python.EmbeddingTest { public class TestNativeTypeOffset { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - /// /// Tests that installation has generated code for NativeTypeOffset and that it can be loaded. /// diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 6bfb81bdb..ab71ed9b3 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -14,14 +14,13 @@ public class TestOperator [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); OwnIntCodec.Setup(); } [OneTimeTearDown] public void Dispose() { - PythonEngine.Shutdown(); + PyObjectConversions.Reset(); } // Mock Integer class to test math ops on non-native dotnet types diff --git a/src/embed_tests/TestPyBuffer.cs b/src/embed_tests/TestPyBuffer.cs index 1b4e28d12..89ddf9370 100644 --- a/src/embed_tests/TestPyBuffer.cs +++ b/src/embed_tests/TestPyBuffer.cs @@ -11,14 +11,13 @@ class TestPyBuffer [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); TupleCodec.Register(); } [OneTimeTearDown] public void Dispose() { - PythonEngine.Shutdown(); + PyObjectConversions.Reset(); } [Test] @@ -38,7 +37,7 @@ public void TestBufferWrite() } string result = pythonArray.InvokeMethod("decode", "utf-8".ToPython()).As(); - Assert.IsTrue(result == bufferTestString2); + Assert.That(result == bufferTestString2, Is.True); } [Test] @@ -58,7 +57,7 @@ public void TestBufferRead() } string result = new UTF8Encoding().GetString(managedArray); - Assert.IsTrue(result == " " + bufferTestString.Substring(1)); + Assert.That(result == " " + bufferTestString.Substring(1), Is.True); } [Test] @@ -67,8 +66,8 @@ public void ArrayHasBuffer() var array = new[,] {{1, 2}, {3,4}}; var memoryView = PythonEngine.Eval("memoryview"); var mem = memoryView.Invoke(array.ToPython()); - Assert.AreEqual(1, mem[(0, 0).ToPython()].As()); - Assert.AreEqual(array[1,0], mem[(1, 0).ToPython()].As()); + Assert.That(mem[(0, 0).ToPython()].As(), Is.EqualTo(1)); + Assert.That(mem[(1, 0).ToPython()].As(), Is.EqualTo(array[1, 0])); } [Test] @@ -77,14 +76,14 @@ public void RefCount() using var _ = Py.GIL(); using var arr = ByteArrayFromAsciiString("hello world! !$%&/()=?"); - Assert.AreEqual(1, arr.Refcount); + Assert.That(arr.Refcount, Is.EqualTo(1)); using (PyBuffer buf = arr.GetBuffer()) { - Assert.AreEqual(2, arr.Refcount); + Assert.That(arr.Refcount, Is.EqualTo(2)); } - Assert.AreEqual(1, arr.Refcount); + Assert.That(arr.Refcount, Is.EqualTo(1)); } [Test] @@ -99,7 +98,7 @@ public void Finalization() using var _ = Py.GIL(); using var arr = ByteArrayFromAsciiString("hello world! !$%&/()=?"); - Assert.AreEqual(1, arr.Refcount); + Assert.That(arr.Refcount, Is.EqualTo(1)); MakeBufAndLeak(arr); @@ -107,7 +106,7 @@ public void Finalization() GC.WaitForPendingFinalizers(); Finalizer.Instance.Collect(); - Assert.AreEqual(1, arr.Refcount); + Assert.That(arr.Refcount, Is.EqualTo(1)); } [Test] diff --git a/src/embed_tests/TestPyFloat.cs b/src/embed_tests/TestPyFloat.cs index 89e29e5fd..c6111f180 100644 --- a/src/embed_tests/TestPyFloat.cs +++ b/src/embed_tests/TestPyFloat.cs @@ -9,18 +9,6 @@ namespace Python.EmbeddingTest /// public class TestPyFloat { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void FloatCtor() { @@ -132,14 +120,14 @@ public void CompareTo() { var v = new PyFloat(42); - Assert.AreEqual(0, v.CompareTo(42f)); - Assert.AreEqual(0, v.CompareTo(42d)); + Assert.That(v.CompareTo(42f), Is.EqualTo(0)); + Assert.That(v.CompareTo(42d), Is.EqualTo(0)); - Assert.AreEqual(1, v.CompareTo(41f)); - Assert.AreEqual(1, v.CompareTo(41d)); + Assert.That(v.CompareTo(41f), Is.EqualTo(1)); + Assert.That(v.CompareTo(41d), Is.EqualTo(1)); - Assert.AreEqual(-1, v.CompareTo(43f)); - Assert.AreEqual(-1, v.CompareTo(43d)); + Assert.That(v.CompareTo(43f), Is.EqualTo(-1)); + Assert.That(v.CompareTo(43d), Is.EqualTo(-1)); } [Test] @@ -147,11 +135,11 @@ public void Equals() { var v = new PyFloat(42); - Assert.IsTrue(v.Equals(42f)); - Assert.IsTrue(v.Equals(42d)); + Assert.That(v.Equals(42f), Is.True); + Assert.That(v.Equals(42d), Is.True); - Assert.IsFalse(v.Equals(41f)); - Assert.IsFalse(v.Equals(41d)); + Assert.That(v.Equals(41f), Is.False); + Assert.That(v.Equals(41d), Is.False); } } } diff --git a/src/embed_tests/pyimport.cs b/src/embed_tests/pyimport.cs index b828d5315..551df1915 100644 --- a/src/embed_tests/pyimport.cs +++ b/src/embed_tests/pyimport.cs @@ -21,28 +21,28 @@ namespace Python.EmbeddingTest /// public class PyImportTest { + string TestPath; + [OneTimeSetUp] public void SetUp() { - PythonEngine.Initialize(); - /* Append the tests directory to sys.path * using reflection to circumvent the private * modifiers placed on most Runtime methods. */ - string testPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "fixtures"); - TestContext.Out.WriteLine(testPath); + TestPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "fixtures"); + TestContext.Out.WriteLine(TestPath); - using var str = Runtime.Runtime.PyString_FromString(testPath); - Assert.IsFalse(str.IsNull()); + using var str = Runtime.Runtime.PyString_FromString(TestPath); + Assert.That(str.IsNull(), Is.False); BorrowedReference path = Runtime.Runtime.PySys_GetObject("path"); - Assert.IsFalse(path.IsNull); + Assert.That(path.IsNull, Is.False); Runtime.Runtime.PyList_Append(path, str.Borrow()); } [OneTimeTearDown] public void Dispose() { - PythonEngine.Shutdown(); + // TODO Undo the above } /// @@ -89,7 +89,7 @@ public void BadAssembly() path = @"C:\Windows\System32\kernel32.dll"; } - Assert.IsTrue(File.Exists(path), $"Test DLL {path} does not exist!"); + Assert.That(File.Exists(path), Is.True, $"Test DLL {path} does not exist!"); string code = $@" import clr diff --git a/src/embed_tests/pyinitialize.cs b/src/embed_tests/pyinitialize.cs index 25dafb686..cde463e61 100644 --- a/src/embed_tests/pyinitialize.cs +++ b/src/embed_tests/pyinitialize.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Python.Runtime; +#if false namespace Python.EmbeddingTest { public class PyInitializeTest @@ -42,8 +43,8 @@ public static void LoadSpecificArgs() { using var v0 = argv[0]; using var v1 = argv[1]; - Assert.AreEqual(args[0], v0.ToString()); - Assert.AreEqual(args[1], v1.ToString()); + Assert.That(v0.ToString(), Is.EqualTo(args[0])); + Assert.That(v1.ToString(), Is.EqualTo(args[1])); } } } @@ -155,3 +156,4 @@ public void ShutdownHandlers() public class ImportClassShutdownRefcountClass { } } +#endif \ No newline at end of file diff --git a/src/embed_tests/pyrunstring.cs b/src/embed_tests/pyrunstring.cs index 57c133c00..fb1302800 100644 --- a/src/embed_tests/pyrunstring.cs +++ b/src/embed_tests/pyrunstring.cs @@ -6,26 +6,14 @@ namespace Python.EmbeddingTest { public class RunStringTest { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestRunSimpleString() { int aa = PythonEngine.RunSimpleString("import sys"); - Assert.AreEqual(0, aa); + Assert.That(aa, Is.EqualTo(0)); int bb = PythonEngine.RunSimpleString("import 1234"); - Assert.AreEqual(-1, bb); + Assert.That(bb, Is.EqualTo(-1)); } [Test] @@ -39,7 +27,7 @@ public void TestEval() object b = PythonEngine.Eval("sys.attr1 + a + 1", null, locals) .AsManagedObject(typeof(int)); - Assert.AreEqual(111, b); + Assert.That(b, Is.EqualTo(111)); } [Test] @@ -53,7 +41,7 @@ public void TestExec() PythonEngine.Exec("c = sys.attr1 + a + 1", null, locals); object c = locals.GetItem("c").AsManagedObject(typeof(int)); - Assert.AreEqual(111, c); + Assert.That(c, Is.EqualTo(111)); } [Test] From 05b8e32bd4f20a355992bba86108cb88b5fb2cf9 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 23 Oct 2025 11:47:03 +0200 Subject: [PATCH 66/77] Disable NUnit analyzer for now --- src/embed_tests/Python.EmbeddingTest.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index cb3b52052..eb2048c76 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -20,10 +20,10 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From ca50a92ccbfb1f57db9d81c74d0f128cd03c1e0b Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 23 Oct 2025 11:47:25 +0200 Subject: [PATCH 67/77] Reset conversions after each codec test --- src/embed_tests/Codecs.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/embed_tests/Codecs.cs b/src/embed_tests/Codecs.cs index cecc6825d..d4d22dcac 100644 --- a/src/embed_tests/Codecs.cs +++ b/src/embed_tests/Codecs.cs @@ -8,6 +8,12 @@ namespace Python.EmbeddingTest { public class Codecs { + [TearDown] + public void TearDown() + { + PyObjectConversions.Reset(); + } + [Test] public void TupleConversionsGeneric() { From e7b51d770d6ea9705139a17d8b23a8f506239964 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 23 Oct 2025 21:37:40 +0200 Subject: [PATCH 68/77] Remove shutdown from most tests, disable the rest for now --- src/embed_tests/Events.cs | 12 ------------ src/embed_tests/Modules.cs | 12 ------------ .../StateSerialization/MethodSerialization.cs | 6 +++++- src/embed_tests/TestFinalizer.cs | 1 + src/embed_tests/TestPyInt.cs | 12 ------------ src/embed_tests/TestPyIter.cs | 12 ------------ src/embed_tests/TestPyList.cs | 12 ------------ src/embed_tests/TestPyNumber.cs | 12 ------------ src/embed_tests/TestPyObject.cs | 12 ------------ src/embed_tests/TestPySequence.cs | 12 ------------ src/embed_tests/TestPyString.cs | 12 ------------ src/embed_tests/TestPyTuple.cs | 12 ------------ src/embed_tests/TestPyType.cs | 12 ------------ src/embed_tests/TestPyWith.cs | 12 ------------ src/embed_tests/TestPythonEngineProperties.cs | 15 ++++----------- src/embed_tests/TestPythonException.cs | 12 ------------ src/embed_tests/TestRuntime.cs | 1 + src/embed_tests/pyimport.cs | 3 ++- src/embed_tests/pyinitialize.cs | 3 +-- 19 files changed, 14 insertions(+), 171 deletions(-) diff --git a/src/embed_tests/Events.cs b/src/embed_tests/Events.cs index c216f4214..94a30726b 100644 --- a/src/embed_tests/Events.cs +++ b/src/embed_tests/Events.cs @@ -10,18 +10,6 @@ namespace Python.EmbeddingTest; public class Events { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void UsingDoesNotLeak() { diff --git a/src/embed_tests/Modules.cs b/src/embed_tests/Modules.cs index fc9b8f398..67fa3d0fc 100644 --- a/src/embed_tests/Modules.cs +++ b/src/embed_tests/Modules.cs @@ -28,18 +28,6 @@ public void Dispose() } } - [OneTimeSetUp] - public void OneTimeSetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - PythonEngine.Shutdown(); - } - /// /// Eval a Python expression and obtain its return value. /// diff --git a/src/embed_tests/StateSerialization/MethodSerialization.cs b/src/embed_tests/StateSerialization/MethodSerialization.cs index 80b7a08ee..d565c1e7a 100644 --- a/src/embed_tests/StateSerialization/MethodSerialization.cs +++ b/src/embed_tests/StateSerialization/MethodSerialization.cs @@ -20,7 +20,7 @@ public void GenericRoundtrip() } [Test] - public void ConstrctorRoundtrip() + public void ConstructorRoundtrip() { var ctor = typeof(MethodTestHost).GetConstructor(new[] { typeof(int) }); var maybeConstructor = new MaybeMethodBase(ctor); @@ -33,6 +33,10 @@ static T SerializationRoundtrip(T item) { using var buf = new MemoryStream(); var formatter = RuntimeData.CreateFormatter(); + if (typeof(NoopFormatter).IsAssignableFrom(formatter.GetType())) + { + Assert.Inconclusive("NoopFormatter in use, cannot perform serialization test."); + } formatter.Serialize(buf, item); buf.Position = 0; return (T)formatter.Deserialize(buf); diff --git a/src/embed_tests/TestFinalizer.cs b/src/embed_tests/TestFinalizer.cs index 9ef3e25b6..89dcf137e 100644 --- a/src/embed_tests/TestFinalizer.cs +++ b/src/embed_tests/TestFinalizer.cs @@ -86,6 +86,7 @@ public void CollectBasicObject() } [Test] + [Ignore("Requires explicit shutdown")] [Obsolete("GC tests are not guaranteed")] public void CollectOnShutdown() { diff --git a/src/embed_tests/TestPyInt.cs b/src/embed_tests/TestPyInt.cs index d2767e664..36319cf1a 100644 --- a/src/embed_tests/TestPyInt.cs +++ b/src/embed_tests/TestPyInt.cs @@ -10,18 +10,6 @@ namespace Python.EmbeddingTest { public class TestPyInt { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestCtorInt() { diff --git a/src/embed_tests/TestPyIter.cs b/src/embed_tests/TestPyIter.cs index 7428da979..5da660242 100644 --- a/src/embed_tests/TestPyIter.cs +++ b/src/embed_tests/TestPyIter.cs @@ -9,18 +9,6 @@ namespace Python.EmbeddingTest { class TestPyIter { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void KeepOldObjects() { diff --git a/src/embed_tests/TestPyList.cs b/src/embed_tests/TestPyList.cs index eee129f2d..a380f0b2d 100644 --- a/src/embed_tests/TestPyList.cs +++ b/src/embed_tests/TestPyList.cs @@ -7,18 +7,6 @@ namespace Python.EmbeddingTest { public class TestPyList { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestStringIsListType() { diff --git a/src/embed_tests/TestPyNumber.cs b/src/embed_tests/TestPyNumber.cs index 0261c15c1..d8e275521 100644 --- a/src/embed_tests/TestPyNumber.cs +++ b/src/embed_tests/TestPyNumber.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestPyNumber { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void IsNumberTypeTrue() { diff --git a/src/embed_tests/TestPyObject.cs b/src/embed_tests/TestPyObject.cs index 2f27eba1b..f762b94e9 100644 --- a/src/embed_tests/TestPyObject.cs +++ b/src/embed_tests/TestPyObject.cs @@ -8,18 +8,6 @@ namespace Python.EmbeddingTest { public class TestPyObject { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestGetDynamicMemberNames() { diff --git a/src/embed_tests/TestPySequence.cs b/src/embed_tests/TestPySequence.cs index dc35a2633..339ea1e83 100644 --- a/src/embed_tests/TestPySequence.cs +++ b/src/embed_tests/TestPySequence.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestPySequence { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestIsSequenceTrue() { diff --git a/src/embed_tests/TestPyString.cs b/src/embed_tests/TestPyString.cs index 35c6339ee..a1fdd6079 100644 --- a/src/embed_tests/TestPyString.cs +++ b/src/embed_tests/TestPyString.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestPyString { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestStringCtor() { diff --git a/src/embed_tests/TestPyTuple.cs b/src/embed_tests/TestPyTuple.cs index 5d76116aa..3a3fbf2a0 100644 --- a/src/embed_tests/TestPyTuple.cs +++ b/src/embed_tests/TestPyTuple.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestPyTuple { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - /// /// Test IsTupleType without having to Initialize a tuple. /// PyTuple constructor use IsTupleType. This decouples the tests. diff --git a/src/embed_tests/TestPyType.cs b/src/embed_tests/TestPyType.cs index d98dfda2e..c29032a8a 100644 --- a/src/embed_tests/TestPyType.cs +++ b/src/embed_tests/TestPyType.cs @@ -10,18 +10,6 @@ namespace Python.EmbeddingTest { public class TestPyType { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void CanCreateHeapType() { diff --git a/src/embed_tests/TestPyWith.cs b/src/embed_tests/TestPyWith.cs index d1c9aac28..fbce811da 100644 --- a/src/embed_tests/TestPyWith.cs +++ b/src/embed_tests/TestPyWith.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestPyWith { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - /// /// Test that exception is raised in context manager that ignores it. /// diff --git a/src/embed_tests/TestPythonEngineProperties.cs b/src/embed_tests/TestPythonEngineProperties.cs index be91d7f45..c6b4a2024 100644 --- a/src/embed_tests/TestPythonEngineProperties.cs +++ b/src/embed_tests/TestPythonEngineProperties.cs @@ -9,7 +9,6 @@ public class TestPythonEngineProperties [Test] public static void GetBuildinfoDoesntCrash() { - PythonEngine.Initialize(); using (Py.GIL()) { string s = PythonEngine.BuildInfo; @@ -22,7 +21,6 @@ public static void GetBuildinfoDoesntCrash() [Test] public static void GetCompilerDoesntCrash() { - PythonEngine.Initialize(); using (Py.GIL()) { string s = PythonEngine.Compiler; @@ -36,7 +34,6 @@ public static void GetCompilerDoesntCrash() [Test] public static void GetCopyrightDoesntCrash() { - PythonEngine.Initialize(); using (Py.GIL()) { string s = PythonEngine.Copyright; @@ -49,7 +46,6 @@ public static void GetCopyrightDoesntCrash() [Test] public static void GetPlatformDoesntCrash() { - PythonEngine.Initialize(); using (Py.GIL()) { string s = PythonEngine.Platform; @@ -62,7 +58,6 @@ public static void GetPlatformDoesntCrash() [Test] public static void GetVersionDoesntCrash() { - PythonEngine.Initialize(); using (Py.GIL()) { string s = PythonEngine.Version; @@ -75,21 +70,17 @@ public static void GetVersionDoesntCrash() [Test] public static void GetPythonPathDefault() { - PythonEngine.Initialize(); string s = PythonEngine.PythonPath; StringAssert.Contains("python", s.ToLower()); - PythonEngine.Shutdown(); } [Test] public static void GetProgramNameDefault() { - PythonEngine.Initialize(); string s = PythonEngine.ProgramName; Assert.NotNull(s); - PythonEngine.Shutdown(); } /// @@ -101,13 +92,12 @@ public static void GetPythonHomeDefault() { string envPythonHome = Environment.GetEnvironmentVariable("PYTHONHOME") ?? ""; - PythonEngine.Initialize(); string enginePythonHome = PythonEngine.PythonHome; Assert.AreEqual(envPythonHome, enginePythonHome); - PythonEngine.Shutdown(); } + [Ignore("Only works if we can shutdown and re-init the interpreter")] [Test] public void SetPythonHome() { @@ -130,6 +120,7 @@ public void SetPythonHome() PythonEngine.PythonHome = pythonHomeBackup; } + [Ignore("Only works if we can shutdown and re-init the interpreter")] [Test] public void SetPythonHomeTwice() { @@ -172,6 +163,7 @@ public void SetPythonHomeEmptyString() PythonEngine.Shutdown(); } + [Ignore("Only works if we can shutdown and re-init the interpreter")] [Test] public void SetProgramName() { @@ -193,6 +185,7 @@ public void SetProgramName() PythonEngine.ProgramName = programNameBackup; } + [Ignore("Only works if we can shutdown and re-init the interpreter")] [Test] public void SetPythonPath() { diff --git a/src/embed_tests/TestPythonException.cs b/src/embed_tests/TestPythonException.cs index a248b6a1f..91a412749 100644 --- a/src/embed_tests/TestPythonException.cs +++ b/src/embed_tests/TestPythonException.cs @@ -6,18 +6,6 @@ namespace Python.EmbeddingTest { public class TestPythonException { - [OneTimeSetUp] - public void SetUp() - { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - [Test] public void TestMessage() { diff --git a/src/embed_tests/TestRuntime.cs b/src/embed_tests/TestRuntime.cs index 77696fd96..88cef1557 100644 --- a/src/embed_tests/TestRuntime.cs +++ b/src/embed_tests/TestRuntime.cs @@ -5,6 +5,7 @@ namespace Python.EmbeddingTest { + [Ignore("Only works if we can shutdown and re-initialize the Python runtime")] public class TestRuntime { [OneTimeSetUp] diff --git a/src/embed_tests/pyimport.cs b/src/embed_tests/pyimport.cs index 551df1915..c774af345 100644 --- a/src/embed_tests/pyimport.cs +++ b/src/embed_tests/pyimport.cs @@ -42,7 +42,8 @@ public void SetUp() [OneTimeTearDown] public void Dispose() { - // TODO Undo the above + using var _ = Py.GIL(); + Py.Import("sys").GetAttr("path").InvokeMethod("remove", new PyString(TestPath)); } /// diff --git a/src/embed_tests/pyinitialize.cs b/src/embed_tests/pyinitialize.cs index cde463e61..dbb8f0bbe 100644 --- a/src/embed_tests/pyinitialize.cs +++ b/src/embed_tests/pyinitialize.cs @@ -2,9 +2,9 @@ using NUnit.Framework; using Python.Runtime; -#if false namespace Python.EmbeddingTest { + [Ignore("Only works if we can re-initialize the Python engine")] public class PyInitializeTest { /// @@ -156,4 +156,3 @@ public void ShutdownHandlers() public class ImportClassShutdownRefcountClass { } } -#endif \ No newline at end of file From 595c5fb2ccc6036e45c1f09846c20e3d0730e6a5 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 26 Oct 2025 18:50:59 +0100 Subject: [PATCH 69/77] Use python -m pytest, path seemingly not properly updated --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ca2562d04..ebf84bc77 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,14 +78,14 @@ jobs: - name: Python Tests (Mono) if: ${{ matrix.os.category != 'windows' }} - run: pytest --runtime mono + run: python -m pytest --runtime mono - name: Python Tests (.NET Core) - run: pytest --runtime coreclr + run: python -m pytest --runtime coreclr - name: Python Tests (.NET Framework) if: ${{ matrix.os.category == 'windows' }} - run: pytest --runtime netfx + run: python -m pytest --runtime netfx - name: Python tests run from .NET run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ From 9bd783995243cd34fd33a7d399784a5561ab8ffe Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 26 Oct 2025 18:52:04 +0100 Subject: [PATCH 70/77] Remove unused architecture from uv env activation --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ebf84bc77..6c5d84dcb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,7 +58,6 @@ jobs: - name: Set up Python ${{ matrix.python }} uses: astral-sh/setup-uv@v7 with: - architecture: ${{ matrix.os.platform }} python-version: ${{ matrix.python }} activate-environment: true enable-cache: true From 5272e346944e87ccc0ddcfd2a4f17a4b15f3883e Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 26 Oct 2025 18:54:21 +0100 Subject: [PATCH 71/77] Synchronize the environment --- .github/workflows/main.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c5d84dcb..286d43559 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,12 +58,17 @@ jobs: - name: Set up Python ${{ matrix.python }} uses: astral-sh/setup-uv@v7 with: + architecture: ${{ matrix.os.platform }} python-version: ${{ matrix.python }} + cache-python: true activate-environment: true enable-cache: true - name: Synchronize the virtual environment - run: uv sync + 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-${{ matrix.os.platform }} --framework net472 --logger "console;verbosity=detailed" src/embed_tests/ @@ -77,14 +82,14 @@ jobs: - name: Python Tests (Mono) if: ${{ matrix.os.category != 'windows' }} - run: python -m pytest --runtime mono + run: pytest --runtime mono - name: Python Tests (.NET Core) - run: python -m pytest --runtime coreclr + run: pytest --runtime coreclr - name: Python Tests (.NET Framework) if: ${{ matrix.os.category == 'windows' }} - run: python -m pytest --runtime netfx + run: pytest --runtime netfx - name: Python tests run from .NET run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ From bd33b3fe934971650263026a90bee5656ba1b017 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 7 Dec 2025 15:12:32 +0100 Subject: [PATCH 72/77] Include the probed PythonDLL value in the exception --- src/runtime/Runtime.Delegates.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs index 262dc1e19..dc4a4b0a9 100644 --- a/src/runtime/Runtime.Delegates.cs +++ b/src/runtime/Runtime.Delegates.cs @@ -308,7 +308,8 @@ static Delegates() { throw new BadPythonDllException( "Runtime.PythonDLL was not set or does not point to a supported Python runtime DLL." + - " See https://github.com/pythonnet/pythonnet#embedding-python-in-net", + " See https://github.com/pythonnet/pythonnet#embedding-python-in-net." + + $" Value of PythonDLL: {PythonDLL ?? "null"}", e); } } From a38eddcd71375a7478aed1a650c8d15245ab4d8b Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sun, 7 Dec 2025 23:11:00 +0100 Subject: [PATCH 73/77] Use the actual pytest runner --- .github/workflows/main.yml | 4 ++ Directory.Build.props | 1 + .../Python.PythonTestsRunner.csproj | 8 ++++ src/python_tests_runner/PythonTestRunner.cs | 47 ++++++------------- src/runtime/Util/PythonEnvironment.cs | 2 +- 5 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 286d43559..53c0934ae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,4 +92,8 @@ jobs: run: pytest --runtime netfx - name: Python tests run from .NET + # For some reason, it won't find pytest on the Windows + 3.10 + # combination, which hints that it does not handle the venv properly in + # this combination. + if: ${{ matrix.os.category != 'windows' || matrix.python != '3.10' }} run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/ diff --git a/Directory.Build.props b/Directory.Build.props index 5f8157f2e..85e4039b9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,6 +9,7 @@ $([System.IO.File]::ReadAllText("$(MSBuildThisFileDirectory)version.txt").Trim()) $(FullVersion.Split('-', 2)[0]) $(FullVersion.Split('-', 2)[1]) + $(MSBuildThisFileDirectory) diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj index e55c1af37..5fc55d158 100644 --- a/src/python_tests_runner/Python.PythonTestsRunner.csproj +++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj @@ -23,4 +23,12 @@ + + + + diff --git a/src/python_tests_runner/PythonTestRunner.cs b/src/python_tests_runner/PythonTestRunner.cs index f97cc5aec..3df22ec2e 100644 --- a/src/python_tests_runner/PythonTestRunner.cs +++ b/src/python_tests_runner/PythonTestRunner.cs @@ -8,22 +8,29 @@ using NUnit.Framework; using Python.Runtime; -using Python.Test; namespace Python.PythonTestsRunner { public class PythonTestRunner { + string OriginalDirectory; + [OneTimeSetUp] public void SetUp() { PythonEngine.Initialize(); + OriginalDirectory = Environment.CurrentDirectory; + + var codeDir = File.ReadAllText("tests_location.txt").Trim(); + TestContext.Progress.WriteLine($"Changing working directory to {codeDir}"); + Environment.CurrentDirectory = codeDir; } [OneTimeTearDown] public void Dispose() { PythonEngine.Shutdown(); + Environment.CurrentDirectory = OriginalDirectory; } /// @@ -46,39 +53,15 @@ static IEnumerable PythonTestCases() [TestCaseSource(nameof(PythonTestCases))] public void RunPythonTest(string testFile, string testName) { - // Find the tests directory - string folder = typeof(PythonTestRunner).Assembly.Location; - while (Path.GetFileName(folder) != "src") + using dynamic pytest = Py.Import("pytest"); + + using var args = new PyList(); + args.Append(new PyString($"{testFile}.py::{testName}")); + int res = pytest.main(args); + if (res != 0) { - folder = Path.GetDirectoryName(folder); + Assert.Fail($"Python test {testFile}.{testName} failed"); } - folder = Path.Combine(folder, "..", "tests"); - string path = Path.Combine(folder, testFile + ".py"); - if (!File.Exists(path)) throw new FileNotFoundException("Cannot find test file", path); - - // We could use 'import' below, but importlib gives more helpful error messages than 'import' - // https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly - // Because the Python tests sometimes have relative imports, the module name must be inside the tests package - PythonEngine.Exec($@" -import sys -import os -sys.path.append(os.path.dirname(r'{folder}')) -sys.path.append(os.path.join(r'{folder}', 'fixtures')) -import clr -clr.AddReference('Python.Test') -import tests -module_name = 'tests.{testFile}' -file_path = r'{path}' -import importlib.util -spec = importlib.util.spec_from_file_location(module_name, file_path) -module = importlib.util.module_from_spec(spec) -sys.modules[module_name] = module -try: - spec.loader.exec_module(module) -except ImportError as error: - raise ImportError(str(error) + ' when sys.path=' + os.pathsep.join(sys.path)) -module.{testName}() -"); } } } diff --git a/src/runtime/Util/PythonEnvironment.cs b/src/runtime/Util/PythonEnvironment.cs index 701db3c93..b1ebc7fa5 100644 --- a/src/runtime/Util/PythonEnvironment.cs +++ b/src/runtime/Util/PythonEnvironment.cs @@ -161,7 +161,7 @@ private static Dictionary TryParse(string venvCfg) private static string ProgramNameFromPath(string path) { - if (Runtime.IsWindows) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return Path.Combine(path, "Scripts", "python.exe"); } From 2f24a35d9912df97a9463fdc011b6fc7d8d1d3e7 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 8 Dec 2025 10:34:50 +0100 Subject: [PATCH 74/77] Move tests that require reinit and only run on .NET Framework --- .../NeedsReinit/StopAndRestartEngine.cs | 28 ++++ .../{ => NeedsReinit}/TestDomainReload.cs | 7 +- .../TestPyInitialize.cs} | 6 +- .../NeedsReinit/TestPythonEngineProperties.cs | 133 ++++++++++++++++++ .../{ => NeedsReinit}/TestRuntime.cs | 16 +-- src/embed_tests/Python.EmbeddingTest.csproj | 5 + src/embed_tests/TestPythonEngineProperties.cs | 127 ----------------- 7 files changed, 177 insertions(+), 145 deletions(-) create mode 100644 src/embed_tests/NeedsReinit/StopAndRestartEngine.cs rename src/embed_tests/{ => NeedsReinit}/TestDomainReload.cs (99%) rename src/embed_tests/{pyinitialize.cs => NeedsReinit/TestPyInitialize.cs} (97%) create mode 100644 src/embed_tests/NeedsReinit/TestPythonEngineProperties.cs rename src/embed_tests/{ => NeedsReinit}/TestRuntime.cs (92%) diff --git a/src/embed_tests/NeedsReinit/StopAndRestartEngine.cs b/src/embed_tests/NeedsReinit/StopAndRestartEngine.cs new file mode 100644 index 000000000..9ea4f73c5 --- /dev/null +++ b/src/embed_tests/NeedsReinit/StopAndRestartEngine.cs @@ -0,0 +1,28 @@ +using Python.Runtime; +using NUnit.Framework; + +namespace Python.EmbeddingTest.NeedsReinit; + +public class StopAndRestartEngine +{ + bool WasInitialized = false; + + [OneTimeSetUp] + public void Setup() + { + WasInitialized = PythonEngine.IsInitialized; + if (WasInitialized) + { + PythonEngine.Shutdown(); + } + } + + [OneTimeTearDown] + public void Teardown() + { + if (WasInitialized && !PythonEngine.IsInitialized) + { + PythonEngine.Initialize(); + } + } +} diff --git a/src/embed_tests/TestDomainReload.cs b/src/embed_tests/NeedsReinit/TestDomainReload.cs similarity index 99% rename from src/embed_tests/TestDomainReload.cs rename to src/embed_tests/NeedsReinit/TestDomainReload.cs index a0f9b63eb..a8d2cd3d8 100644 --- a/src/embed_tests/TestDomainReload.cs +++ b/src/embed_tests/NeedsReinit/TestDomainReload.cs @@ -15,10 +15,13 @@ // Unfortunately this means no continuous integration testing for this case. // #if NETFRAMEWORK -namespace Python.EmbeddingTest +namespace Python.EmbeddingTest.NeedsReinit { - class TestDomainReload + [Category("NeedsReinit")] + class TestDomainReload : StopAndRestartEngine { + + abstract class CrossCaller : MarshalByRefObject { public abstract ValueType Execute(ValueType arg); diff --git a/src/embed_tests/pyinitialize.cs b/src/embed_tests/NeedsReinit/TestPyInitialize.cs similarity index 97% rename from src/embed_tests/pyinitialize.cs rename to src/embed_tests/NeedsReinit/TestPyInitialize.cs index dbb8f0bbe..1ef4127b8 100644 --- a/src/embed_tests/pyinitialize.cs +++ b/src/embed_tests/NeedsReinit/TestPyInitialize.cs @@ -2,10 +2,10 @@ using NUnit.Framework; using Python.Runtime; -namespace Python.EmbeddingTest +namespace Python.EmbeddingTest.NeedsReinit { - [Ignore("Only works if we can re-initialize the Python engine")] - public class PyInitializeTest + [Category("NeedsReinit")] + public class TestPyInitialize : StopAndRestartEngine { /// /// Tests issue with multiple simple Initialize/Shutdowns. diff --git a/src/embed_tests/NeedsReinit/TestPythonEngineProperties.cs b/src/embed_tests/NeedsReinit/TestPythonEngineProperties.cs new file mode 100644 index 000000000..8eb9e975d --- /dev/null +++ b/src/embed_tests/NeedsReinit/TestPythonEngineProperties.cs @@ -0,0 +1,133 @@ +using System; +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest.NeedsReinit +{ + [Category("NeedsReinit")] + public class TestPythonEngineProperties : StopAndRestartEngine + { + [Test] + public void SetPythonHome() + { + PythonEngine.Initialize(); + var pythonHomeBackup = PythonEngine.PythonHome; + PythonEngine.Shutdown(); + + if (pythonHomeBackup == "") + Assert.Inconclusive("Can't reset PythonHome to empty string, skipping"); + + var pythonHome = "/dummypath/"; + + PythonEngine.PythonHome = pythonHome; + PythonEngine.Initialize(); + + Assert.AreEqual(pythonHome, PythonEngine.PythonHome); + PythonEngine.Shutdown(); + + // Restoring valid pythonhome. + PythonEngine.PythonHome = pythonHomeBackup; + } + + [Test] + public void SetPythonHomeTwice() + { + PythonEngine.Initialize(); + var pythonHomeBackup = PythonEngine.PythonHome; + PythonEngine.Shutdown(); + + if (pythonHomeBackup == "") + Assert.Inconclusive("Can't reset PythonHome to empty string, skipping"); + + var pythonHome = "/dummypath/"; + + PythonEngine.PythonHome = "/dummypath2/"; + PythonEngine.PythonHome = pythonHome; + PythonEngine.Initialize(); + + Assert.AreEqual(pythonHome, PythonEngine.PythonHome); + PythonEngine.Shutdown(); + + PythonEngine.PythonHome = pythonHomeBackup; + } + + [Test] + [Ignore("Currently buggy in Python")] + public void SetPythonHomeEmptyString() + { + PythonEngine.Initialize(); + + var backup = PythonEngine.PythonHome; + if (backup == "") + { + PythonEngine.Shutdown(); + Assert.Inconclusive("Can't reset PythonHome to empty string, skipping"); + } + PythonEngine.PythonHome = ""; + + Assert.AreEqual("", PythonEngine.PythonHome); + + PythonEngine.PythonHome = backup; + PythonEngine.Shutdown(); + } + + [Test] + public void SetProgramName() + { + if (PythonEngine.IsInitialized) + { + PythonEngine.Shutdown(); + } + + var programNameBackup = PythonEngine.ProgramName; + + var programName = "FooBar"; + + PythonEngine.ProgramName = programName; + PythonEngine.Initialize(); + + Assert.AreEqual(programName, PythonEngine.ProgramName); + PythonEngine.Shutdown(); + + PythonEngine.ProgramName = programNameBackup; + } + + [Test] + public void SetPythonPath() + { + PythonEngine.Initialize(); + + const string moduleName = "pytest"; + bool importShouldSucceed; + try + { + Py.Import(moduleName); + importShouldSucceed = true; + } + catch + { + importShouldSucceed = false; + } + + string[] paths = Py.Import("sys").GetAttr("path").As(); + string path = string.Join(System.IO.Path.PathSeparator.ToString(), paths); + + // path should not be set to PythonEngine.PythonPath here. + // PythonEngine.PythonPath gets the default module search path, not the full search path. + // The list sys.path is initialized with this value on interpreter startup; + // it can be (and usually is) modified later to change the search path for loading modules. + // See https://docs.python.org/3/c-api/init.html#c.Py_GetPath + // After PythonPath is set, then PythonEngine.PythonPath will correctly return the full search path. + + PythonEngine.Shutdown(); + + PythonEngine.PythonPath = path; + PythonEngine.Initialize(); + + Assert.AreEqual(path, PythonEngine.PythonPath); + if (importShouldSucceed) Py.Import(moduleName); + + PythonEngine.Shutdown(); + } + } +} diff --git a/src/embed_tests/TestRuntime.cs b/src/embed_tests/NeedsReinit/TestRuntime.cs similarity index 92% rename from src/embed_tests/TestRuntime.cs rename to src/embed_tests/NeedsReinit/TestRuntime.cs index 88cef1557..193bf57d3 100644 --- a/src/embed_tests/TestRuntime.cs +++ b/src/embed_tests/NeedsReinit/TestRuntime.cs @@ -3,21 +3,11 @@ using NUnit.Framework; using Python.Runtime; -namespace Python.EmbeddingTest +namespace Python.EmbeddingTest.NeedsReinit { - [Ignore("Only works if we can shutdown and re-initialize the Python runtime")] - public class TestRuntime + [Ignore("Tests for low-level Runtime functions, crashing currently")] + public class TestRuntime : StopAndRestartEngine { - [OneTimeSetUp] - public void SetUp() - { - // We needs to ensure that no any engines are running. - if (PythonEngine.IsInitialized) - { - PythonEngine.Shutdown(); - } - } - [Test] public static void Py_IsInitializedValue() { diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index eb2048c76..36b9a31f9 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -14,6 +14,11 @@ + + + + + $(DefineConstants);$(ConfiguredConstants) diff --git a/src/embed_tests/TestPythonEngineProperties.cs b/src/embed_tests/TestPythonEngineProperties.cs index c6b4a2024..485931cfb 100644 --- a/src/embed_tests/TestPythonEngineProperties.cs +++ b/src/embed_tests/TestPythonEngineProperties.cs @@ -96,132 +96,5 @@ public static void GetPythonHomeDefault() Assert.AreEqual(envPythonHome, enginePythonHome); } - - [Ignore("Only works if we can shutdown and re-init the interpreter")] - [Test] - public void SetPythonHome() - { - PythonEngine.Initialize(); - var pythonHomeBackup = PythonEngine.PythonHome; - PythonEngine.Shutdown(); - - if (pythonHomeBackup == "") - Assert.Inconclusive("Can't reset PythonHome to empty string, skipping"); - - var pythonHome = "/dummypath/"; - - PythonEngine.PythonHome = pythonHome; - PythonEngine.Initialize(); - - Assert.AreEqual(pythonHome, PythonEngine.PythonHome); - PythonEngine.Shutdown(); - - // Restoring valid pythonhome. - PythonEngine.PythonHome = pythonHomeBackup; - } - - [Ignore("Only works if we can shutdown and re-init the interpreter")] - [Test] - public void SetPythonHomeTwice() - { - PythonEngine.Initialize(); - var pythonHomeBackup = PythonEngine.PythonHome; - PythonEngine.Shutdown(); - - if (pythonHomeBackup == "") - Assert.Inconclusive("Can't reset PythonHome to empty string, skipping"); - - var pythonHome = "/dummypath/"; - - PythonEngine.PythonHome = "/dummypath2/"; - PythonEngine.PythonHome = pythonHome; - PythonEngine.Initialize(); - - Assert.AreEqual(pythonHome, PythonEngine.PythonHome); - PythonEngine.Shutdown(); - - PythonEngine.PythonHome = pythonHomeBackup; - } - - [Test] - [Ignore("Currently buggy in Python")] - public void SetPythonHomeEmptyString() - { - PythonEngine.Initialize(); - - var backup = PythonEngine.PythonHome; - if (backup == "") - { - PythonEngine.Shutdown(); - Assert.Inconclusive("Can't reset PythonHome to empty string, skipping"); - } - PythonEngine.PythonHome = ""; - - Assert.AreEqual("", PythonEngine.PythonHome); - - PythonEngine.PythonHome = backup; - PythonEngine.Shutdown(); - } - - [Ignore("Only works if we can shutdown and re-init the interpreter")] - [Test] - public void SetProgramName() - { - if (PythonEngine.IsInitialized) - { - PythonEngine.Shutdown(); - } - - var programNameBackup = PythonEngine.ProgramName; - - var programName = "FooBar"; - - PythonEngine.ProgramName = programName; - PythonEngine.Initialize(); - - Assert.AreEqual(programName, PythonEngine.ProgramName); - PythonEngine.Shutdown(); - - PythonEngine.ProgramName = programNameBackup; - } - - [Ignore("Only works if we can shutdown and re-init the interpreter")] - [Test] - public void SetPythonPath() - { - PythonEngine.Initialize(); - - const string moduleName = "pytest"; - bool importShouldSucceed; - try - { - Py.Import(moduleName); - importShouldSucceed = true; - } - catch - { - importShouldSucceed = false; - } - - string[] paths = Py.Import("sys").GetAttr("path").As(); - string path = string.Join(System.IO.Path.PathSeparator.ToString(), paths); - - // path should not be set to PythonEngine.PythonPath here. - // PythonEngine.PythonPath gets the default module search path, not the full search path. - // The list sys.path is initialized with this value on interpreter startup; - // it can be (and usually is) modified later to change the search path for loading modules. - // See https://docs.python.org/3/c-api/init.html#c.Py_GetPath - // After PythonPath is set, then PythonEngine.PythonPath will correctly return the full search path. - - PythonEngine.Shutdown(); - - PythonEngine.PythonPath = path; - PythonEngine.Initialize(); - - Assert.AreEqual(path, PythonEngine.PythonPath); - if (importShouldSucceed) Py.Import(moduleName); - - PythonEngine.Shutdown(); - } } } From 9a3c04e3520d68d38d0fef3d0cf189bfc744f0b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:22:17 +0100 Subject: [PATCH 75/77] Bump NUnit3TestAdapter from 5.2.0 to 6.0.0 (#2667) --- updated-dependencies: - dependency-name: NUnit3TestAdapter dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: NUnit3TestAdapter dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/embed_tests/Python.EmbeddingTest.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 36b9a31f9..28076120a 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -29,7 +29,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive From fd7c7e1cbd8e1d7bdb2ec2423c3cf9f95a3abed1 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 9 Dec 2025 08:01:57 +0100 Subject: [PATCH 76/77] Fix line endings --- doc/make.bat | 70 +++++++------- src/runtime/Codecs/IterableDecoder.cs | 110 +++++++++++----------- src/runtime/Codecs/ListDecoder.cs | 100 ++++++++++---------- src/runtime/Codecs/SequenceDecoder.cs | 96 ++++++++++---------- src/runtime/Types/ManagedTypes.cd | 2 +- tests/domain_tests/App.config | 2 +- tests/test_mp_length.py | 126 +++++++++++++------------- 7 files changed, 253 insertions(+), 253 deletions(-) diff --git a/doc/make.bat b/doc/make.bat index 747ffb7b3..dc1312ab0 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/src/runtime/Codecs/IterableDecoder.cs b/src/runtime/Codecs/IterableDecoder.cs index bcc2eca01..b864850d5 100644 --- a/src/runtime/Codecs/IterableDecoder.cs +++ b/src/runtime/Codecs/IterableDecoder.cs @@ -1,55 +1,55 @@ -using System; -using System.Collections.Generic; - -namespace Python.Runtime.Codecs -{ - public class IterableDecoder : IPyObjectDecoder - { - internal static bool IsIterable(Type targetType) - { - //if it is a plain IEnumerable, we can decode it using sequence protocol. - if (targetType == typeof(System.Collections.IEnumerable)) - return true; - - if (!targetType.IsGenericType) - return false; - - return targetType.GetGenericTypeDefinition() == typeof(IEnumerable<>); - } - - internal static bool IsIterable(PyType objectType) - { - return objectType.HasAttr("__iter__"); - } - - public bool CanDecode(PyType objectType, Type targetType) - { - return IsIterable(objectType) && IsIterable(targetType); - } - - public bool TryDecode(PyObject pyObj, out T value) - { - //first see if T is a plan IEnumerable - if (typeof(T) == typeof(System.Collections.IEnumerable)) - { - object enumerable = new CollectionWrappers.IterableWrapper(pyObj); - value = (T)enumerable; - return true; - } - - var elementType = typeof(T).GetGenericArguments()[0]; - var collectionType = typeof(CollectionWrappers.IterableWrapper<>).MakeGenericType(elementType); - - var instance = Activator.CreateInstance(collectionType, new[] { pyObj }); - value = (T)instance; - return true; - } - - public static IterableDecoder Instance { get; } = new IterableDecoder(); - - public static void Register() - { - PyObjectConversions.RegisterDecoder(Instance); - } - } -} +using System; +using System.Collections.Generic; + +namespace Python.Runtime.Codecs +{ + public class IterableDecoder : IPyObjectDecoder + { + internal static bool IsIterable(Type targetType) + { + //if it is a plain IEnumerable, we can decode it using sequence protocol. + if (targetType == typeof(System.Collections.IEnumerable)) + return true; + + if (!targetType.IsGenericType) + return false; + + return targetType.GetGenericTypeDefinition() == typeof(IEnumerable<>); + } + + internal static bool IsIterable(PyType objectType) + { + return objectType.HasAttr("__iter__"); + } + + public bool CanDecode(PyType objectType, Type targetType) + { + return IsIterable(objectType) && IsIterable(targetType); + } + + public bool TryDecode(PyObject pyObj, out T value) + { + //first see if T is a plan IEnumerable + if (typeof(T) == typeof(System.Collections.IEnumerable)) + { + object enumerable = new CollectionWrappers.IterableWrapper(pyObj); + value = (T)enumerable; + return true; + } + + var elementType = typeof(T).GetGenericArguments()[0]; + var collectionType = typeof(CollectionWrappers.IterableWrapper<>).MakeGenericType(elementType); + + var instance = Activator.CreateInstance(collectionType, new[] { pyObj }); + value = (T)instance; + return true; + } + + public static IterableDecoder Instance { get; } = new IterableDecoder(); + + public static void Register() + { + PyObjectConversions.RegisterDecoder(Instance); + } + } +} diff --git a/src/runtime/Codecs/ListDecoder.cs b/src/runtime/Codecs/ListDecoder.cs index 70ff33aaa..5da82851f 100644 --- a/src/runtime/Codecs/ListDecoder.cs +++ b/src/runtime/Codecs/ListDecoder.cs @@ -1,50 +1,50 @@ -using System; -using System.Collections.Generic; - -namespace Python.Runtime.Codecs -{ - public class ListDecoder : IPyObjectDecoder - { - private static bool IsList(Type targetType) - { - if (!targetType.IsGenericType) - return false; - - return targetType.GetGenericTypeDefinition() == typeof(IList<>); - } - - private static bool IsList(PyType objectType) - { - //TODO accept any python object that implements the sequence and list protocols - //must implement sequence protocol to fully implement list protocol - //if (!SequenceDecoder.IsSequence(objectType)) return false; - - //returns wheter the type is a list. - return PythonReferenceComparer.Instance.Equals(objectType, Runtime.PyListType); - } - - public bool CanDecode(PyType objectType, Type targetType) - { - return IsList(objectType) && IsList(targetType); - } - - public bool TryDecode(PyObject pyObj, out T value) - { - if (pyObj == null) throw new ArgumentNullException(nameof(pyObj)); - - var elementType = typeof(T).GetGenericArguments()[0]; - Type collectionType = typeof(CollectionWrappers.ListWrapper<>).MakeGenericType(elementType); - - var instance = Activator.CreateInstance(collectionType, new[] { pyObj }); - value = (T)instance; - return true; - } - - public static ListDecoder Instance { get; } = new ListDecoder(); - - public static void Register() - { - PyObjectConversions.RegisterDecoder(Instance); - } - } -} +using System; +using System.Collections.Generic; + +namespace Python.Runtime.Codecs +{ + public class ListDecoder : IPyObjectDecoder + { + private static bool IsList(Type targetType) + { + if (!targetType.IsGenericType) + return false; + + return targetType.GetGenericTypeDefinition() == typeof(IList<>); + } + + private static bool IsList(PyType objectType) + { + //TODO accept any python object that implements the sequence and list protocols + //must implement sequence protocol to fully implement list protocol + //if (!SequenceDecoder.IsSequence(objectType)) return false; + + //returns wheter the type is a list. + return PythonReferenceComparer.Instance.Equals(objectType, Runtime.PyListType); + } + + public bool CanDecode(PyType objectType, Type targetType) + { + return IsList(objectType) && IsList(targetType); + } + + public bool TryDecode(PyObject pyObj, out T value) + { + if (pyObj == null) throw new ArgumentNullException(nameof(pyObj)); + + var elementType = typeof(T).GetGenericArguments()[0]; + Type collectionType = typeof(CollectionWrappers.ListWrapper<>).MakeGenericType(elementType); + + var instance = Activator.CreateInstance(collectionType, new[] { pyObj }); + value = (T)instance; + return true; + } + + public static ListDecoder Instance { get; } = new ListDecoder(); + + public static void Register() + { + PyObjectConversions.RegisterDecoder(Instance); + } + } +} diff --git a/src/runtime/Codecs/SequenceDecoder.cs b/src/runtime/Codecs/SequenceDecoder.cs index a539297cd..c5ded4958 100644 --- a/src/runtime/Codecs/SequenceDecoder.cs +++ b/src/runtime/Codecs/SequenceDecoder.cs @@ -1,52 +1,52 @@ -using System; -using System.Collections.Generic; - -namespace Python.Runtime.Codecs -{ - public class SequenceDecoder : IPyObjectDecoder - { - internal static bool IsSequence(Type targetType) - { - if (!targetType.IsGenericType) - return false; - - return targetType.GetGenericTypeDefinition() == typeof(ICollection<>); - } - - internal static bool IsSequence(PyType objectType) - { - //must implement iterable protocol to fully implement sequence protocol - if (!IterableDecoder.IsIterable(objectType)) return false; - +using System; +using System.Collections.Generic; + +namespace Python.Runtime.Codecs +{ + public class SequenceDecoder : IPyObjectDecoder + { + internal static bool IsSequence(Type targetType) + { + if (!targetType.IsGenericType) + return false; + + return targetType.GetGenericTypeDefinition() == typeof(ICollection<>); + } + + internal static bool IsSequence(PyType objectType) + { + //must implement iterable protocol to fully implement sequence protocol + if (!IterableDecoder.IsIterable(objectType)) return false; + //returns wheter it implements the sequence protocol //according to python doc this needs to exclude dict subclasses //but I don't know how to look for that given the objectType //rather than the instance. - return objectType.HasAttr("__getitem__"); - } - - public bool CanDecode(PyType objectType, Type targetType) - { - return IsSequence(objectType) && IsSequence(targetType); - } - - public bool TryDecode(PyObject pyObj, out T value) - { - if (pyObj == null) throw new ArgumentNullException(nameof(pyObj)); - - var elementType = typeof(T).GetGenericArguments()[0]; - Type collectionType = typeof(CollectionWrappers.SequenceWrapper<>).MakeGenericType(elementType); - - var instance = Activator.CreateInstance(collectionType, new[] { pyObj }); - value = (T)instance; - return true; - } - - public static SequenceDecoder Instance { get; } = new SequenceDecoder(); - - public static void Register() - { - PyObjectConversions.RegisterDecoder(Instance); - } - } -} + return objectType.HasAttr("__getitem__"); + } + + public bool CanDecode(PyType objectType, Type targetType) + { + return IsSequence(objectType) && IsSequence(targetType); + } + + public bool TryDecode(PyObject pyObj, out T value) + { + if (pyObj == null) throw new ArgumentNullException(nameof(pyObj)); + + var elementType = typeof(T).GetGenericArguments()[0]; + Type collectionType = typeof(CollectionWrappers.SequenceWrapper<>).MakeGenericType(elementType); + + var instance = Activator.CreateInstance(collectionType, new[] { pyObj }); + value = (T)instance; + return true; + } + + public static SequenceDecoder Instance { get; } = new SequenceDecoder(); + + public static void Register() + { + PyObjectConversions.RegisterDecoder(Instance); + } + } +} diff --git a/src/runtime/Types/ManagedTypes.cd b/src/runtime/Types/ManagedTypes.cd index 9a3e3de16..e6759265f 100644 --- a/src/runtime/Types/ManagedTypes.cd +++ b/src/runtime/Types/ManagedTypes.cd @@ -1,4 +1,4 @@ - + diff --git a/tests/domain_tests/App.config b/tests/domain_tests/App.config index 56efbc7b5..20939707c 100644 --- a/tests/domain_tests/App.config +++ b/tests/domain_tests/App.config @@ -1,4 +1,4 @@ - + diff --git a/tests/test_mp_length.py b/tests/test_mp_length.py index e86fff288..8b6e56b7c 100644 --- a/tests/test_mp_length.py +++ b/tests/test_mp_length.py @@ -1,63 +1,63 @@ -# -*- coding: utf-8 -*- - -"""Test __len__ for .NET classes implementing ICollection/ICollection.""" - -import System -import pytest -from Python.Test import MpLengthCollectionTest, MpLengthExplicitCollectionTest, MpLengthGenericCollectionTest, MpLengthExplicitGenericCollectionTest - -def test_simple___len__(): - """Test __len__ for simple ICollection implementers""" - import System - import System.Collections.Generic - l = System.Collections.Generic.List[int]() - assert len(l) == 0 - l.Add(5) - l.Add(6) - assert len(l) == 2 - - d = System.Collections.Generic.Dictionary[int, int]() - assert len(d) == 0 - d.Add(4, 5) - assert len(d) == 1 - - a = System.Array[int]([0,1,2,3]) - assert len(a) == 4 - -def test_custom_collection___len__(): - """Test __len__ for custom collection class""" - s = MpLengthCollectionTest() - assert len(s) == 3 - -def test_custom_collection_explicit___len__(): - """Test __len__ for custom collection class that explicitly implements ICollection""" - s = MpLengthExplicitCollectionTest() - assert len(s) == 2 - -def test_custom_generic_collection___len__(): - """Test __len__ for custom generic collection class""" - s = MpLengthGenericCollectionTest[int]() - s.Add(1) - s.Add(2) - assert len(s) == 2 - -def test_custom_generic_collection_explicit___len__(): - """Test __len__ for custom generic collection that explicity implements ICollection""" - s = MpLengthExplicitGenericCollectionTest[int]() - s.Add(1) - s.Add(10) - assert len(s) == 2 - -def test_len_through_interface_generic(): - """Test __len__ for ICollection""" - import System.Collections.Generic - l = System.Collections.Generic.List[int]() - coll = System.Collections.Generic.ICollection[int](l) - assert len(coll) == 0 - -def test_len_through_interface(): - """Test __len__ for ICollection""" - import System.Collections - l = System.Collections.ArrayList() - coll = System.Collections.ICollection(l) - assert len(coll) == 0 +# -*- coding: utf-8 -*- + +"""Test __len__ for .NET classes implementing ICollection/ICollection.""" + +import System +import pytest +from Python.Test import MpLengthCollectionTest, MpLengthExplicitCollectionTest, MpLengthGenericCollectionTest, MpLengthExplicitGenericCollectionTest + +def test_simple___len__(): + """Test __len__ for simple ICollection implementers""" + import System + import System.Collections.Generic + l = System.Collections.Generic.List[int]() + assert len(l) == 0 + l.Add(5) + l.Add(6) + assert len(l) == 2 + + d = System.Collections.Generic.Dictionary[int, int]() + assert len(d) == 0 + d.Add(4, 5) + assert len(d) == 1 + + a = System.Array[int]([0,1,2,3]) + assert len(a) == 4 + +def test_custom_collection___len__(): + """Test __len__ for custom collection class""" + s = MpLengthCollectionTest() + assert len(s) == 3 + +def test_custom_collection_explicit___len__(): + """Test __len__ for custom collection class that explicitly implements ICollection""" + s = MpLengthExplicitCollectionTest() + assert len(s) == 2 + +def test_custom_generic_collection___len__(): + """Test __len__ for custom generic collection class""" + s = MpLengthGenericCollectionTest[int]() + s.Add(1) + s.Add(2) + assert len(s) == 2 + +def test_custom_generic_collection_explicit___len__(): + """Test __len__ for custom generic collection that explicity implements ICollection""" + s = MpLengthExplicitGenericCollectionTest[int]() + s.Add(1) + s.Add(10) + assert len(s) == 2 + +def test_len_through_interface_generic(): + """Test __len__ for ICollection""" + import System.Collections.Generic + l = System.Collections.Generic.List[int]() + coll = System.Collections.Generic.ICollection[int](l) + assert len(coll) == 0 + +def test_len_through_interface(): + """Test __len__ for ICollection""" + import System.Collections + l = System.Collections.ArrayList() + coll = System.Collections.ICollection(l) + assert len(coll) == 0 From 08e1b3743f0f816733bf7d26dd7b24b1caa69d73 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 9 Dec 2025 08:03:16 +0100 Subject: [PATCH 77/77] Add previous commit to ignore file --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..d7b97b3a7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Line endings normalization +fd7c7e1cbd8e1d7bdb2ec2423c3cf9f95a3abed1