From 5806f58bcf8e848533ff27c3e97dbbff58f1e21f Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Wed, 11 Nov 2020 16:45:52 -0800 Subject: [PATCH 1/6] drop legacy collections from MethodBinder implementation --- src/runtime/classobject.cs | 2 +- src/runtime/constructorbinding.cs | 2 +- src/runtime/indexer.cs | 6 +-- src/runtime/methodbinder.cs | 66 ++++++++++--------------------- src/runtime/methodobject.cs | 2 +- 5 files changed, 26 insertions(+), 52 deletions(-) diff --git a/src/runtime/classobject.cs b/src/runtime/classobject.cs index 18816781f..066d5c1b7 100644 --- a/src/runtime/classobject.cs +++ b/src/runtime/classobject.cs @@ -32,7 +32,7 @@ internal ClassObject(Type tp) : base(tp) /// internal IntPtr GetDocString() { - MethodBase[] methods = binder.GetMethods(); + var methods = binder.GetMethods(); var str = ""; foreach (MethodBase t in methods) { diff --git a/src/runtime/constructorbinding.cs b/src/runtime/constructorbinding.cs index 0c81c0a93..02649863b 100644 --- a/src/runtime/constructorbinding.cs +++ b/src/runtime/constructorbinding.cs @@ -121,7 +121,7 @@ public static IntPtr tp_repr(IntPtr ob) Runtime.XIncref(self.repr); return self.repr; } - MethodBase[] methods = self.ctorBinder.GetMethods(); + var methods = self.ctorBinder.GetMethods(); string name = self.type.FullName; var doc = ""; foreach (MethodBase t in methods) diff --git a/src/runtime/indexer.cs b/src/runtime/indexer.cs index 0772b57c6..0c684edc1 100644 --- a/src/runtime/indexer.cs +++ b/src/runtime/indexer.cs @@ -58,8 +58,8 @@ internal void SetItem(IntPtr inst, IntPtr args) internal bool NeedsDefaultArgs(IntPtr args) { var pynargs = Runtime.PyTuple_Size(args); - MethodBase[] methods = SetterBinder.GetMethods(); - if (methods.Length == 0) + var methods = SetterBinder.GetMethods(); + if (methods.Count == 0) { return false; } @@ -99,7 +99,7 @@ internal IntPtr GetDefaultArgs(IntPtr args) var pynargs = Runtime.PyTuple_Size(args); // Get the default arg tuple - MethodBase[] methods = SetterBinder.GetMethods(); + var methods = SetterBinder.GetMethods(); MethodBase mi = methods[0]; ParameterInfo[] pi = mi.GetParameters(); int clrnargs = pi.Length - 1; diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index c0cc4c75c..f2e073255 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Reflection; using System.Text; using System.Collections.Generic; @@ -16,29 +15,25 @@ namespace Python.Runtime [Serializable] internal class MethodBinder { - public ArrayList list; - public MethodBase[] methods; + readonly List methods = new List(); public bool init = false; public bool allow_threads = true; - internal MethodBinder() - { - list = new ArrayList(); - } + internal MethodBinder(){} internal MethodBinder(MethodInfo mi) { - list = new ArrayList { mi }; + this.methods.Add(mi); } public int Count { - get { return list.Count; } + get { return this.methods.Count; } } internal void AddMethod(MethodBase m) { - list.Add(m); + this.methods.Add(m); } /// @@ -158,13 +153,12 @@ internal static MethodInfo MatchSignatureAndParameters(MethodInfo[] mi, Type[] g /// is arranged in order of precedence (done lazily to avoid doing it /// at all for methods that are never called). /// - internal MethodBase[] GetMethods() + internal List GetMethods() { if (!init) { // I'm sure this could be made more efficient. - list.Sort(new MethodSorter()); - methods = (MethodBase[])list.ToArray(typeof(MethodBase)); + this.methods.Sort(new MethodSorter()); init = true; } return methods; @@ -281,9 +275,6 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info) internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, MethodInfo[] methodinfo) { - // loop to find match, return invoker w/ or /wo error - MethodBase[] _methods = null; - var kwargDict = new Dictionary(); if (kw != IntPtr.Zero) { @@ -301,15 +292,9 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth var pynargs = (int)Runtime.PyTuple_Size(args); var isGeneric = false; - if (info != null) - { - _methods = new MethodBase[1]; - _methods.SetValue(info, 0); - } - else - { - _methods = GetMethods(); - } + + // loop to find match, return invoker w/ or /wo error + List _methods = info != null ? new List { info } : GetMethods(); // TODO: Clean up foreach (MethodBase mi in _methods) @@ -319,16 +304,15 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth isGeneric = true; } ParameterInfo[] pi = mi.GetParameters(); - ArrayList defaultArgList; bool paramsArray; - if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList)) + if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out var defaultArgList)) { continue; } var outs = 0; var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, - needsResolution: _methods.Length > 1, + needsResolution: _methods.Count > 1, outs: out outs); if (margs == null) @@ -417,7 +401,7 @@ static IntPtr HandleParamsArray(IntPtr args, int arrayStart, int pyArgCount, out static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, IntPtr args, int pyArgCount, Dictionary kwargDict, - ArrayList defaultArgList, + List defaultArgList, bool needsResolution, out int outs) { @@ -575,7 +559,7 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, Dictionary kwargDict, out bool paramsArray, - out ArrayList defaultArgList) + out List defaultArgList) { defaultArgList = null; var match = false; @@ -590,7 +574,7 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa // every parameter past 'positionalArgumentCount' must have either // a corresponding keyword argument or a default parameter match = true; - defaultArgList = new ArrayList(); + defaultArgList = new List(); for (var v = positionalArgumentCount; v < parameters.Length; v++) { if (kwargDict.ContainsKey(parameters[v].Name)) @@ -770,12 +754,10 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i /// /// Utility class to sort method info by parameter type precedence. /// - internal class MethodSorter : IComparer + internal class MethodSorter : IComparer { - int IComparer.Compare(object m1, object m2) + int IComparer.Compare(MethodBase me1, MethodBase me2) { - var me1 = (MethodBase)m1; - var me2 = (MethodBase)m2; if (me1.DeclaringType != me2.DeclaringType) { // m2's type derives from m1's type, favor m2 @@ -787,17 +769,9 @@ int IComparer.Compare(object m1, object m2) return -1; } - int p1 = MethodBinder.GetPrecedence((MethodBase)m1); - int p2 = MethodBinder.GetPrecedence((MethodBase)m2); - if (p1 < p2) - { - return -1; - } - if (p1 > p2) - { - return 1; - } - return 0; + int p1 = MethodBinder.GetPrecedence(me1); + int p2 = MethodBinder.GetPrecedence(me2); + return p1.CompareTo(p2); } } diff --git a/src/runtime/methodobject.cs b/src/runtime/methodobject.cs index dc23e3ce5..a766350f0 100644 --- a/src/runtime/methodobject.cs +++ b/src/runtime/methodobject.cs @@ -70,7 +70,7 @@ internal IntPtr GetDocString() } var str = ""; Type marker = typeof(DocStringAttribute); - MethodBase[] methods = binder.GetMethods(); + var methods = binder.GetMethods(); foreach (MethodBase method in methods) { if (str.Length > 0) From ebb579dfcac0e531b71889f37dd7b5a1594d9072 Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Wed, 11 Nov 2020 21:19:53 -0800 Subject: [PATCH 2/6] added BorrowedReference.Null and DangerousGetAddressOrNull --- src/runtime/BorrowedReference.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/runtime/BorrowedReference.cs b/src/runtime/BorrowedReference.cs index 8ae382e77..479942b06 100644 --- a/src/runtime/BorrowedReference.cs +++ b/src/runtime/BorrowedReference.cs @@ -11,9 +11,11 @@ readonly ref struct BorrowedReference public bool IsNull => this.pointer == IntPtr.Zero; - /// Gets a raw pointer to the Python object + /// Gets a raw pointer to the Python object. Performs null check public IntPtr DangerousGetAddress() => this.IsNull ? throw new NullReferenceException() : this.pointer; + /// Gets a raw pointer to the Python object + public IntPtr DangerousGetAddressOrNull() => this.pointer; /// /// Creates new instance of from raw pointer. Unsafe. @@ -22,5 +24,7 @@ public BorrowedReference(IntPtr pointer) { this.pointer = pointer; } + + public static BorrowedReference Null => default; } } From f4ea89c293ec87ba3522969988054393b02cf58f Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Wed, 11 Nov 2020 21:21:10 -0800 Subject: [PATCH 3/6] added PyEval_GetFrame and PyFrame_GetLineNumber + a few fancy overloads with *Reference types --- src/runtime/Native/PyFrameObjectReference.cs | 11 ++++++ src/runtime/Util.cs | 2 ++ src/runtime/converter.cs | 7 ++++ src/runtime/managedtype.cs | 3 ++ src/runtime/runtime.cs | 35 ++++++++++++++++++-- 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/runtime/Native/PyFrameObjectReference.cs diff --git a/src/runtime/Native/PyFrameObjectReference.cs b/src/runtime/Native/PyFrameObjectReference.cs new file mode 100644 index 000000000..eae6a943f --- /dev/null +++ b/src/runtime/Native/PyFrameObjectReference.cs @@ -0,0 +1,11 @@ +using System; + +namespace Python.Runtime.Native +{ + readonly ref struct PyFrameObjectReference + { + readonly IntPtr pointer; + + public bool IsNull => this.pointer == IntPtr.Zero; + } +} diff --git a/src/runtime/Util.cs b/src/runtime/Util.cs index eb21cddbb..353758dae 100644 --- a/src/runtime/Util.cs +++ b/src/runtime/Util.cs @@ -7,6 +7,8 @@ internal static class Util { internal const string UnstableApiMessage = "This API is unstable, and might be changed or removed in the next minor release"; + internal const string UseReferenceOverloadMessage = + "This is a legacy overload. Please use an overload, that works with *Reference types"; internal static Int64 ReadCLong(IntPtr tp, int offset) { diff --git a/src/runtime/converter.cs b/src/runtime/converter.cs index 98fe99141..62b1a6fb5 100644 --- a/src/runtime/converter.cs +++ b/src/runtime/converter.cs @@ -298,11 +298,18 @@ internal static IntPtr ToPythonImplicit(object value) return ToPython(value, objectType); } + /// + /// Return a managed object for the given Python object, taking funny + /// byref types into account. + /// + internal static bool ToManaged(BorrowedReference value, Type type, out object result, bool setError) + => ToManaged(value.DangerousGetAddress(), type, out result, setError); /// /// Return a managed object for the given Python object, taking funny /// byref types into account. /// + [Obsolete(Util.UseReferenceOverloadMessage)] internal static bool ToManaged(IntPtr value, Type type, out object result, bool setError) { diff --git a/src/runtime/managedtype.cs b/src/runtime/managedtype.cs index bc2805d80..c3d237fcf 100644 --- a/src/runtime/managedtype.cs +++ b/src/runtime/managedtype.cs @@ -75,6 +75,9 @@ internal void FreeGCHandle() } } + internal static ManagedType GetManagedObject(BorrowedReference ob) + => ob.IsNull ? null : GetManagedObject(ob.DangerousGetAddress()); + [Obsolete(Util.UseReferenceOverloadMessage)] /// /// Given a Python object, return the associated managed object or null. /// diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index b9f471339..2fe4d7737 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using Python.Runtime.Platform; using System.Linq; +using Python.Runtime.Native; namespace Python.Runtime { @@ -898,12 +899,18 @@ public static extern int Py_Main( [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr PyEval_GetBuiltins(); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern PyFrameObjectReference PyEval_GetFrame(); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr PyEval_GetGlobals(); [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr PyEval_GetLocals(); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern int PyFrame_GetLineNumber(PyFrameObjectReference frame); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr Py_GetProgramName(); @@ -1605,7 +1612,7 @@ internal static IntPtr PyUnicode_FromString(string s) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern int PyUnicode_Compare(IntPtr left, IntPtr right); - internal static string GetManagedString(in BorrowedReference borrowedReference) + internal static string GetManagedString(BorrowedReference borrowedReference) => GetManagedString(borrowedReference.DangerousGetAddress()); /// /// Function to access the internal PyUnicode/PyString object and @@ -1691,9 +1698,15 @@ internal static bool PyDict_Check(IntPtr ob) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern int PyMapping_HasKey(IntPtr pointer, IntPtr key); + [Obsolete(Util.UseReferenceOverloadMessage)] [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr PyDict_Keys(IntPtr pointer); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern NewReference PyDict_Keys(BorrowedReference pointer); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern NewReference PyDict_Values(BorrowedReference pointer); + [Obsolete(Util.UseReferenceOverloadMessage)] [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr PyDict_Values(IntPtr pointer); @@ -1709,6 +1722,8 @@ internal static bool PyDict_Check(IntPtr ob) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern void PyDict_Clear(IntPtr pointer); + internal static long PyDict_Size(BorrowedReference dict) => PyDict_Size(dict.DangerousGetAddress()); + [Obsolete(Util.UseReferenceOverloadMessage)] internal static long PyDict_Size(IntPtr pointer) { return (long)_PyDict_Size(pointer); @@ -1827,6 +1842,9 @@ internal static IntPtr PyTuple_New(long size) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr PyTuple_New(IntPtr size); + internal static BorrowedReference PyTuple_GetItem(BorrowedReference pointer, long index) + => new BorrowedReference(PyTuple_GetItem(pointer.DangerousGetAddress(), new IntPtr(index))); + [Obsolete(Util.UseReferenceOverloadMessage)] internal static IntPtr PyTuple_GetItem(IntPtr pointer, long index) { return PyTuple_GetItem(pointer, new IntPtr(index)); @@ -1835,6 +1853,11 @@ internal static IntPtr PyTuple_GetItem(IntPtr pointer, long index) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr PyTuple_GetItem(IntPtr pointer, IntPtr index); + internal static int PyTuple_SetItem(BorrowedReference tuple, long index, IntPtr value) + { + return PyTuple_SetItem(tuple.DangerousGetAddress(), new IntPtr(index), value); + } + [Obsolete(Util.UseReferenceOverloadMessage)] internal static int PyTuple_SetItem(IntPtr pointer, long index, IntPtr value) { return PyTuple_SetItem(pointer, new IntPtr(index), value); @@ -1851,13 +1874,19 @@ internal static IntPtr PyTuple_GetSlice(IntPtr pointer, long start, long end) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr PyTuple_GetSlice(IntPtr pointer, IntPtr start, IntPtr end); + internal static long PyTuple_Size(BorrowedReference tuple) + { + return (long)_PyTuple_Size(tuple); + } + + [Obsolete(Util.UseReferenceOverloadMessage)] internal static long PyTuple_Size(IntPtr pointer) { - return (long)_PyTuple_Size(pointer); + return (long)_PyTuple_Size(new BorrowedReference(pointer)); } [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "PyTuple_Size")] - private static extern IntPtr _PyTuple_Size(IntPtr pointer); + private static extern IntPtr _PyTuple_Size(BorrowedReference pointer); //==================================================================== From 7cb7cfa1aacde710c1c97ad295ca766e562ab59b Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Thu, 12 Nov 2020 10:57:47 -0800 Subject: [PATCH 4/6] fixed TestDomainReload to not call static method using object instance --- src/embed_tests/TestDomainReload.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/embed_tests/TestDomainReload.cs b/src/embed_tests/TestDomainReload.cs index 4c9de1461..ff814b023 100644 --- a/src/embed_tests/TestDomainReload.cs +++ b/src/embed_tests/TestDomainReload.cs @@ -94,7 +94,7 @@ import clr from Python.EmbeddingTest.Domain import MyClass obj = MyClass() obj.Method() -obj.StaticMethod() +MyClass.StaticMethod() obj.Property = 1 obj.Field = 10 ", Assembly.GetExecutingAssembly().FullName); @@ -137,16 +137,18 @@ public override ValueType Execute(ValueType arg) Assert.That(tp_clear, Is.Not.Null); using (PyObject obj = new PyObject(handle)) + using (PyObject myClass = new PyObject(new BorrowedReference(tp))) { obj.InvokeMethod("Method"); - obj.InvokeMethod("StaticMethod"); + myClass.InvokeMethod("StaticMethod"); using (var scope = Py.CreateScope()) { scope.Set("obj", obj); + scope.Set("myClass", myClass); scope.Exec(@" obj.Method() -obj.StaticMethod() +myClass.StaticMethod() obj.Property += 1 obj.Field += 10 "); @@ -191,7 +193,7 @@ from Python.EmbeddingTest.Domain import MyClass def test_obj_call(): obj = MyClass() obj.Method() - obj.StaticMethod() + MyClass.StaticMethod() obj.Property = 1 obj.Field = 10 From 5dd74f9b4df7ff2cfb2f9d5f37f68a2c238c8ce2 Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Thu, 12 Nov 2020 11:07:05 -0800 Subject: [PATCH 5/6] a few more *Reference overloads --- src/runtime/runtime.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index 2fe4d7737..1884f8ed4 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -1031,6 +1031,15 @@ internal static IntPtr PyObject_Type(IntPtr op) return tp; } + /// + /// Managed version of the standard Python C API PyObject_Type call. + /// This version avoids a managed <-> unmanaged transition. + /// + internal static BorrowedReference PyObject_Type(BorrowedReference op) + { + return new BorrowedReference(PyObject_TYPE(op.DangerousGetAddress())); + } + internal static string PyObject_GetTypeName(IntPtr op) { IntPtr pyType = Marshal.ReadIntPtr(op, ObjectOffset.ob_type); @@ -1150,6 +1159,9 @@ internal static long PyObject_Size(IntPtr pointer) [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "PyObject_Str")] internal static extern IntPtr PyObject_Unicode(IntPtr pointer); + [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "PyObject_Str")] + internal static extern NewReference PyObject_Unicode(BorrowedReference pointer); [DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr PyObject_Dir(IntPtr pointer); From 97b82b1c5b149f0ed25165cb12d9f9d5d8210eab Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Thu, 12 Nov 2020 11:22:23 -0800 Subject: [PATCH 6/6] trivial DLR-based instance method binding --- src/runtime/Python.Runtime.15.csproj | 25 +-- src/runtime/Python.Runtime.csproj | 1 + src/runtime/callsitebinder.cs | 60 ++++++ src/runtime/classbase.cs | 44 ++--- src/runtime/classderived.cs | 2 +- src/runtime/classobject.cs | 5 +- src/runtime/constructorbinder.cs | 6 +- src/runtime/constructorbinding.cs | 2 +- src/runtime/delegateobject.cs | 7 +- src/runtime/indexer.cs | 8 +- src/runtime/methodbinder.cs | 267 ++++++++++++++++++++++++--- src/runtime/methodbinding.cs | 2 +- src/runtime/methodobject.cs | 4 +- src/runtime/modulefunctionobject.cs | 5 +- src/runtime/typemethod.cs | 14 +- src/testing/Python.Test.15.csproj | 2 +- 16 files changed, 362 insertions(+), 92 deletions(-) create mode 100644 src/runtime/callsitebinder.cs diff --git a/src/runtime/Python.Runtime.15.csproj b/src/runtime/Python.Runtime.15.csproj index 4ca8140e9..1d15741cb 100644 --- a/src/runtime/Python.Runtime.15.csproj +++ b/src/runtime/Python.Runtime.15.csproj @@ -122,6 +122,9 @@ + + 4.7.0 + @@ -143,20 +146,20 @@ - - - TextTemplatingFileGenerator - intern_.cs - + + + TextTemplatingFileGenerator + intern_.cs + - - - True - True - intern_.tt - + + + True + True + intern_.tt + diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index a11a4b852..29e97abae 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -92,6 +92,7 @@ + diff --git a/src/runtime/callsitebinder.cs b/src/runtime/callsitebinder.cs new file mode 100644 index 000000000..3ebfee4cd --- /dev/null +++ b/src/runtime/callsitebinder.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +using Microsoft.CSharp.RuntimeBinder; + +using CSharpBinder = Microsoft.CSharp.RuntimeBinder.Binder; + +namespace Python.Runtime +{ + public class PythonNetCallSiteBinder : CallSiteBinder + { + readonly CallSiteBinder voidBinder; + readonly CallSiteBinder resultBinder; + + PythonNetCallSiteBinder(string methodName, + IEnumerable typeArguments, + Type context, + IEnumerable argumentInfo) + { + this.voidBinder = CSharpBinder.InvokeMember(CSharpBinderFlags.ResultDiscarded, + methodName, + typeArguments: typeArguments, + context: context, + argumentInfo + ); + this.resultBinder = CSharpBinder.InvokeMember(CSharpBinderFlags.None, + methodName, + typeArguments: typeArguments, + context: context, + argumentInfo + ); + } + + public override Expression Bind(object[] args, + ReadOnlyCollection parameters, + LabelTarget returnLabel) + { + var result = this.resultBinder.Bind(args, parameters, returnLabel); + if (result.Type == typeof(void)) + { + var voidExpr = this.voidBinder.Bind(args, parameters, returnLabel); + return Expression.Block( + voidExpr, + Expression.Return(returnLabel, Expression.Constant(null)) + ); + } + + return result; + } + + public static PythonNetCallSiteBinder InvokeMember(string methodName, + IEnumerable typeArguments, + Type context, + IEnumerable argumentInfo) + => new PythonNetCallSiteBinder(methodName, typeArguments, context, argumentInfo); + } +} diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index a62e76050..9c983af35 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -347,9 +347,10 @@ protected override void OnLoad(InterDomainContext context) /// /// Implements __getitem__ for reflected classes and value types. /// - public static IntPtr mp_subscript(IntPtr ob, IntPtr idx) + public static IntPtr mp_subscript(IntPtr obRaw, IntPtr idx) { - IntPtr tp = Runtime.PyObject_TYPE(ob); + var ob = new BorrowedReference(obRaw); + var tp = Runtime.PyObject_Type(ob); var cls = (ClassBase)GetManagedObject(tp); if (cls.indexer == null || !cls.indexer.CanGet) @@ -361,40 +362,35 @@ public static IntPtr mp_subscript(IntPtr ob, IntPtr idx) // Arg may be a tuple in the case of an indexer with multiple // parameters. If so, use it directly, else make a new tuple // with the index arg (method binders expect arg tuples). - IntPtr args = idx; - var free = false; - - if (!Runtime.PyTuple_Check(idx)) - { - args = Runtime.PyTuple_New(1); - Runtime.XIncref(idx); - Runtime.PyTuple_SetItem(args, 0, idx); - free = true; - } - - IntPtr value; - try + if (Runtime.PyTuple_Check(idx)) { - value = cls.indexer.GetItem(ob, args); + return cls.indexer.GetItem(ob, new BorrowedReference(idx)); } - finally + else { - if (free) + var args = NewReference.DangerousFromPointer(Runtime.PyTuple_New(1)); + try { - Runtime.XDecref(args); + Runtime.XIncref(idx); + Runtime.PyTuple_SetItem(args, 0, idx); + return cls.indexer.GetItem(ob, args); + } + finally + { + args.Dispose(); } } - return value; } /// /// Implements __setitem__ for reflected classes and value types. /// - public static int mp_ass_subscript(IntPtr ob, IntPtr idx, IntPtr v) + public static int mp_ass_subscript(IntPtr obRaw, IntPtr idx, IntPtr v) { - IntPtr tp = Runtime.PyObject_TYPE(ob); + var ob = new BorrowedReference(obRaw); + BorrowedReference tp = Runtime.PyObject_Type(ob); var cls = (ClassBase)GetManagedObject(tp); if (cls.indexer == null || !cls.indexer.CanSet) @@ -422,7 +418,7 @@ public static int mp_ass_subscript(IntPtr ob, IntPtr idx, IntPtr v) IntPtr defaultArgs = cls.indexer.GetDefaultArgs(args); var numOfDefaultArgs = Runtime.PyTuple_Size(defaultArgs); var temp = i + numOfDefaultArgs; - IntPtr real = Runtime.PyTuple_New(temp + 1); + var real = NewReference.DangerousFromPointer(Runtime.PyTuple_New(temp + 1)); for (var n = 0; n < i; n++) { IntPtr item = Runtime.PyTuple_GetItem(args, n); @@ -451,7 +447,7 @@ public static int mp_ass_subscript(IntPtr ob, IntPtr idx, IntPtr v) } finally { - Runtime.XDecref(real); + real.Dispose(); if (free) { diff --git a/src/runtime/classderived.cs b/src/runtime/classderived.cs index e55e89240..63523410a 100644 --- a/src/runtime/classderived.cs +++ b/src/runtime/classderived.cs @@ -52,7 +52,7 @@ internal ClassDerivedObject(Type tp) : base(tp) var cls = GetManagedObject(tp) as ClassDerivedObject; // call the managed constructor - object obj = cls.binder.InvokeRaw(IntPtr.Zero, args, kw); + object obj = cls.binder.InvokeRaw(IntPtr.Zero, new BorrowedReference(args), kw); if (obj == null) { return IntPtr.Zero; diff --git a/src/runtime/classobject.cs b/src/runtime/classobject.cs index 066d5c1b7..3a4ce8133 100644 --- a/src/runtime/classobject.cs +++ b/src/runtime/classobject.cs @@ -49,8 +49,9 @@ internal IntPtr GetDocString() /// /// Implements __new__ for reflected classes and value types. /// - public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw) + public static IntPtr tp_new(IntPtr tp, IntPtr rawArgs, IntPtr kw) { + var args = new BorrowedReference(rawArgs); var self = GetManagedObject(tp) as ClassObject; // Sanity check: this ensures a graceful error if someone does @@ -74,7 +75,7 @@ public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw) return IntPtr.Zero; } - IntPtr op = Runtime.PyTuple_GetItem(args, 0); + BorrowedReference op = Runtime.PyTuple_GetItem(args, 0); object result; if (!Converter.ToManaged(op, type, out result, true)) diff --git a/src/runtime/constructorbinder.cs b/src/runtime/constructorbinder.cs index 0cda3a3d9..15b69b7ba 100644 --- a/src/runtime/constructorbinder.cs +++ b/src/runtime/constructorbinder.cs @@ -29,7 +29,7 @@ internal ConstructorBinder(Type containingType) /// object - the reason is that only the caller knows the correct /// Python type to use when wrapping the result (may be a subclass). /// - internal object InvokeRaw(IntPtr inst, IntPtr args, IntPtr kw) + internal object InvokeRaw(IntPtr inst, BorrowedReference args, IntPtr kw) { return InvokeRaw(inst, args, kw, null); } @@ -49,7 +49,7 @@ internal object InvokeRaw(IntPtr inst, IntPtr args, IntPtr kw) /// Binding binding = this.Bind(inst, args, kw, info); /// to take advantage of Bind()'s ability to use a single MethodBase (CI or MI). /// - internal object InvokeRaw(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info) + internal object InvokeRaw(IntPtr inst, BorrowedReference args, IntPtr kw, MethodBase info) { object result; @@ -78,7 +78,7 @@ internal object InvokeRaw(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info) return result; } - Binding binding = Bind(inst, args, kw, info); + Binding binding = Bind(inst, args.DangerousGetAddress(), kw, info); if (binding == null) { diff --git a/src/runtime/constructorbinding.cs b/src/runtime/constructorbinding.cs index 02649863b..93ccb462d 100644 --- a/src/runtime/constructorbinding.cs +++ b/src/runtime/constructorbinding.cs @@ -211,7 +211,7 @@ public static IntPtr tp_call(IntPtr op, IntPtr args, IntPtr kw) }*/ // Bind using ConstructorBinder.Bind and invoke the ctor providing a null instancePtr // which will fire self.ctorInfo using ConstructorInfo.Invoke(). - object obj = self.ctorBinder.InvokeRaw(IntPtr.Zero, args, kw, self.ctorInfo); + object obj = self.ctorBinder.InvokeRaw(IntPtr.Zero, new BorrowedReference(args), kw, self.ctorInfo); if (obj == null) { // XXX set an error diff --git a/src/runtime/delegateobject.cs b/src/runtime/delegateobject.cs index c5078740f..6a8efb45f 100644 --- a/src/runtime/delegateobject.cs +++ b/src/runtime/delegateobject.cs @@ -72,10 +72,11 @@ public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw) /// /// Implements __call__ for reflected delegate types. /// - public static IntPtr tp_call(IntPtr ob, IntPtr args, IntPtr kw) + public static IntPtr tp_call(IntPtr obRaw, IntPtr args, IntPtr kw) { + var ob = new BorrowedReference(obRaw); // TODO: add fast type check! - IntPtr pytype = Runtime.PyObject_TYPE(ob); + BorrowedReference pytype = Runtime.PyObject_Type(ob); var self = (DelegateObject)GetManagedObject(pytype); var o = GetManagedObject(ob) as CLRObject; @@ -90,7 +91,7 @@ public static IntPtr tp_call(IntPtr ob, IntPtr args, IntPtr kw) { return Exceptions.RaiseTypeError("invalid argument"); } - return self.binder.Invoke(ob, args, kw); + return self.binder.Invoke(ob, new BorrowedReference(args), new BorrowedReference(kw)); } diff --git a/src/runtime/indexer.cs b/src/runtime/indexer.cs index 0c684edc1..22c122369 100644 --- a/src/runtime/indexer.cs +++ b/src/runtime/indexer.cs @@ -44,15 +44,15 @@ public void AddProperty(PropertyInfo pi) } } - internal IntPtr GetItem(IntPtr inst, IntPtr args) + internal IntPtr GetItem(BorrowedReference inst, BorrowedReference args) { - return GetterBinder.Invoke(inst, args, IntPtr.Zero); + return GetterBinder.Invoke(inst, args, BorrowedReference.Null); } - internal void SetItem(IntPtr inst, IntPtr args) + internal void SetItem(BorrowedReference inst, BorrowedReference args) { - SetterBinder.Invoke(inst, args, IntPtr.Zero); + SetterBinder.Invoke(inst, args, BorrowedReference.Null); } internal bool NeedsDefaultArgs(IntPtr args) diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index f2e073255..3c376495e 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -3,6 +3,10 @@ using System.Text; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +using Microsoft.CSharp.RuntimeBinder; namespace Python.Runtime { @@ -13,15 +17,19 @@ namespace Python.Runtime /// ConstructorBinder, a minor variation used to invoke constructors. /// [Serializable] - internal class MethodBinder + internal class MethodBinder: System.Runtime.Serialization.IDeserializationCallback { readonly List methods = new List(); + [NonSerialized] + Lazy> callSiteCache; public bool init = false; public bool allow_threads = true; - internal MethodBinder(){} + internal MethodBinder() { + this.InitializeCallSiteCache(); + } - internal MethodBinder(MethodInfo mi) + internal MethodBinder(MethodInfo mi):this() { this.methods.Add(mi); } @@ -609,41 +617,49 @@ static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] pa return match; } - internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw) + internal virtual IntPtr Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) { return Invoke(inst, args, kw, null, null); } - internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info) + internal virtual IntPtr Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) { return Invoke(inst, args, kw, info, null); } - protected static void AppendArgumentTypes(StringBuilder to, IntPtr args) + private static string GetName(IEnumerable methodInfo) + => methodInfo.FirstOrDefault()?.Name; + + private static void SetOverloadNotFoundError(BorrowedReference args, string methodName) + { + var value = new StringBuilder("No method matches given arguments"); + if (methodName != null) + { + value.Append($" for {methodName}"); + } + + value.Append(": "); + AppendArgumentTypes(to: value, args); + Exceptions.SetError(Exceptions.TypeError, value.ToString()); + } + protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) { long argCount = Runtime.PyTuple_Size(args); to.Append("("); for (long argIndex = 0; argIndex < argCount; argIndex++) { var arg = Runtime.PyTuple_GetItem(args, argIndex); - if (arg != IntPtr.Zero) + if (!arg.IsNull) { var type = Runtime.PyObject_Type(arg); - if (type != IntPtr.Zero) + if (!type.IsNull) { - try - { - var description = Runtime.PyObject_Unicode(type); - if (description != IntPtr.Zero) - { - to.Append(Runtime.GetManagedString(description)); - Runtime.XDecref(description); - } - } - finally + var description = Runtime.PyObject_Unicode(type); + if (!description.IsNull()) { - Runtime.XDecref(type); + to.Append(Runtime.GetManagedString(description)); } + description.Dispose(); } } @@ -653,23 +669,147 @@ protected static void AppendArgumentTypes(StringBuilder to, IntPtr args) to.Append(')'); } - internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, MethodInfo[] methodinfo) + static List GetArguments(BorrowedReference args, BorrowedReference kwArgs) + { + int argCount = (int)Runtime.PyTuple_Size(args); + + var arguments = new List() { + // the instance, on which the call is made (null for static methods) + CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), + }; + for (int argN = 0; argN < argCount; argN++) + { + arguments.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)); + } + + if (!kwArgs.IsNull) + { + int kwCount = (int)Runtime.PyDict_Size(kwArgs); + NewReference keys = Runtime.PyDict_Keys(kwArgs); + NewReference items = Runtime.PyDict_Values(kwArgs); + for (int kwN = 0; kwN < kwCount; kwN++) + { + string argName = Runtime.GetManagedString(Runtime.PyList_GetItem(keys, kwN)); + arguments.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.NamedArgument, argName)); + } + keys.Dispose(); + items.Dispose(); + } + + return arguments; + } + + static object[] GetParameters( + BorrowedReference args, BorrowedReference kwArgs, + out int argCount, out int kwCount) + { + argCount = (int)Runtime.PyTuple_Size(args); + kwCount = kwArgs.IsNull ? 0 : (int)Runtime.PyDict_Size(kwArgs); + var result = new object[argCount + kwCount]; + for (int argN = 0; argN < argCount; argN++) + { + var arg = Runtime.PyTuple_GetItem(args, argN); + if (!Converter.ToManaged(arg, typeof(object), out result[argN], setError: true)) + return null; + } + + if (!kwArgs.IsNull) + { + NewReference items = Runtime.PyDict_Values(kwArgs); + for (int kwN = 0; kwN < kwCount; kwN++) + { + var kwArg = Runtime.PyList_GetItem(items, kwN); + if (!Converter.ToManaged(kwArg, typeof(object), out result[kwN + argCount], setError: true)) + return null; + } + items.Dispose(); + } + + return result; + } + + internal virtual IntPtr Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) { - Binding binding = Bind(inst, args, kw, info, methodinfo); + var clrInstance = ManagedType.GetManagedObject(inst) as CLRObject; + if (clrInstance is null) + // static method binding is not yet implemented with DLR + return this.StaticInvoke(IntPtr.Zero, args, kw, info, methodinfo); + + object[] parameters = GetParameters(args, kw, out int argCount, out int kwCount); + if (parameters is null) return IntPtr.Zero; + + var method = info ?? methodinfo?.FirstOrDefault() ?? this.GetMethods()[0]; + var callSite = this.GetOrCreateCallSite(args, kw, parameters.Length, argCount, kwCount, method); + object result; IntPtr ts = IntPtr.Zero; - if (binding == null) + if (allow_threads) { - var value = new StringBuilder("No method matches given arguments"); - if (methodinfo != null && methodinfo.Length > 0) + ts = PythonEngine.BeginAllowThreads(); + } + + try + { + var invokeParameters = new object[parameters.Length + 2]; + invokeParameters[0] = callSite; + invokeParameters[1] = clrInstance?.inst; + Array.Copy(parameters, sourceIndex: 0, invokeParameters, destinationIndex: 2, parameters.Length); + result = ((dynamic)callSite).Target.DynamicInvoke(invokeParameters); + } catch (TargetInvocationException e) + when (e.InnerException is RuntimeBinderException bindError) + { + if (allow_threads) { - value.Append($" for {methodinfo[0].Name}"); + PythonEngine.EndAllowThreads(ts); } + // TODO: propagate bindError + SetOverloadNotFoundError(args, GetName(methodinfo ?? GetMethods().ToArray())); + return IntPtr.Zero; + } + catch (Exception e) + { + if (e.InnerException != null) + { + e = e.InnerException; + } + if (allow_threads) + { + PythonEngine.EndAllowThreads(ts); + } + Exceptions.SetError(e); + return IntPtr.Zero; + } + + if (allow_threads) + { + PythonEngine.EndAllowThreads(ts); + } - value.Append(": "); - AppendArgumentTypes(to: value, args); - Exceptions.SetError(Exceptions.TypeError, value.ToString()); + // If there are out parameters, we return a tuple containing + // the result followed by the out parameters. If there is only + // one out parameter and the return type of the method is void, + // we return the out parameter as the result to Python (for + // code compatibility with ironpython). + +#warning INCOMPLETE + + return Converter.ToPython(result, typeof(object) /*must be actual return type here*/); + } + + internal virtual IntPtr StaticInvoke(IntPtr inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) + { + Binding binding = Bind(inst, + args.DangerousGetAddress(), + kw.DangerousGetAddressOrNull(), + info, methodinfo); + + object result; + IntPtr ts = IntPtr.Zero; + + if (binding == null) + { + SetOverloadNotFoundError(args, GetName(methodinfo)); return IntPtr.Zero; } @@ -748,6 +888,77 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i return Converter.ToPython(result, mi.ReturnType); } + + private CallSite GetOrCreateCallSite(BorrowedReference args, BorrowedReference kw, int parameterCount, int argCount, int kwCount, MethodBase method) + { + var frame = Runtime.PyEval_GetFrame(); + var cacheKey = new CallSiteCacheKey + { + Line = frame.IsNull ? -1 : Runtime.PyFrame_GetLineNumber(frame), + ArgCount = argCount, + KwArgCount = kwCount, + }; + lock (this.callSiteCache.Value) + { + if (!this.callSiteCache.Value.TryGetValue(cacheKey, out var callSite)) + { + var arguments = GetArguments(args, kw); + var binder = PythonNetCallSiteBinder.InvokeMember( + method.Name, + // TODO: forward type arguments + typeArguments: null, + context: method.DeclaringType, + arguments.ToArray() + ); + + var callSiteTypeArgs = new List + { + typeof(CallSite), + typeof(object), + }; + for (int i = 0; i < parameterCount; i++) + callSiteTypeArgs.Add(typeof(object)); + // return type + callSiteTypeArgs.Add(typeof(object)); + var delegateType = Expression.GetFuncType(callSiteTypeArgs.ToArray()); + + callSite = CallSite.Create(delegateType, binder); + this.callSiteCache.Value[cacheKey] = callSite; + } + + return callSite; + } + } + + void System.Runtime.Serialization.IDeserializationCallback.OnDeserialization(object sender) + { + this.InitializeCallSiteCache(); + } + + void InitializeCallSiteCache() + { + this.callSiteCache = new Lazy>(() => new Dictionary()); + } + + struct CallSiteCacheKey : IEquatable + { + public int Line { get; set; } + public int ArgCount { get; set; } +#warning has to be replaced with the full ordered list of kwarg names + public int KwArgCount { get; set; } + + public override bool Equals(object obj) => obj is CallSiteCacheKey key && this.Equals(key); + public bool Equals(CallSiteCacheKey other) => this.Line == other.Line && this.ArgCount == other.ArgCount && this.KwArgCount == other.KwArgCount; + + public override int GetHashCode() + { + int hashCode = -1650922183; + hashCode = hashCode * -1521134295 + this.Line.GetHashCode(); + hashCode = hashCode * -1521134295 + this.ArgCount.GetHashCode(); + hashCode = hashCode * -1521134295 + this.KwArgCount.GetHashCode(); + return hashCode; + } + } } diff --git a/src/runtime/methodbinding.cs b/src/runtime/methodbinding.cs index 7a10fcdef..8018277d0 100644 --- a/src/runtime/methodbinding.cs +++ b/src/runtime/methodbinding.cs @@ -185,7 +185,7 @@ public static IntPtr tp_call(IntPtr ob, IntPtr args, IntPtr kw) } } - return self.m.Invoke(target, args, kw, self.info); + return self.m.Invoke(new BorrowedReference(target), new BorrowedReference(args), new BorrowedReference(kw), self.info); } finally { diff --git a/src/runtime/methodobject.cs b/src/runtime/methodobject.cs index a766350f0..2dc731ff2 100644 --- a/src/runtime/methodobject.cs +++ b/src/runtime/methodobject.cs @@ -49,12 +49,12 @@ private void _MethodObject(Type type, string name, MethodInfo[] info) } } - public virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw) + public virtual IntPtr Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) { return Invoke(inst, args, kw, null); } - public virtual IntPtr Invoke(IntPtr target, IntPtr args, IntPtr kw, MethodBase info) + public virtual IntPtr Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase info) { return binder.Invoke(target, args, kw, info, this.info); } diff --git a/src/runtime/modulefunctionobject.cs b/src/runtime/modulefunctionobject.cs index e7a2c515a..88cfee1e2 100644 --- a/src/runtime/modulefunctionobject.cs +++ b/src/runtime/modulefunctionobject.cs @@ -22,10 +22,11 @@ public ModuleFunctionObject(Type type, string name, MethodInfo[] info, bool allo /// /// __call__ implementation. /// - public static IntPtr tp_call(IntPtr ob, IntPtr args, IntPtr kw) + public static IntPtr tp_call(IntPtr obRaw, IntPtr args, IntPtr kw) { + var ob = new BorrowedReference(obRaw); var self = (ModuleFunctionObject)GetManagedObject(ob); - return self.Invoke(ob, args, kw); + return self.Invoke(ob, new BorrowedReference(args), new BorrowedReference(kw)); } /// diff --git a/src/runtime/typemethod.cs b/src/runtime/typemethod.cs index 4da92613c..9a4c87bfb 100644 --- a/src/runtime/typemethod.cs +++ b/src/runtime/typemethod.cs @@ -18,21 +18,17 @@ public TypeMethod(Type type, string name, MethodInfo[] info, bool allow_threads) { } - public override IntPtr Invoke(IntPtr ob, IntPtr args, IntPtr kw) + public override IntPtr Invoke(BorrowedReference ob, BorrowedReference args, BorrowedReference kw) { MethodInfo mi = info[0]; var arglist = new object[3]; - arglist[0] = ob; - arglist[1] = args; - arglist[2] = kw; + arglist[0] = ob.DangerousGetAddressOrNull(); + arglist[1] = args.DangerousGetAddress(); + arglist[2] = kw.DangerousGetAddressOrNull(); try { - object inst = null; - if (ob != IntPtr.Zero) - { - inst = GetManagedObject(ob); - } + object inst = ob.IsNull ? null : GetManagedObject(ob); return (IntPtr)mi.Invoke(inst, BindingFlags.Default, null, arglist, null); } catch (Exception e) diff --git a/src/testing/Python.Test.15.csproj b/src/testing/Python.Test.15.csproj index 0e19adf91..f7ec4ebd5 100644 --- a/src/testing/Python.Test.15.csproj +++ b/src/testing/Python.Test.15.csproj @@ -69,7 +69,7 @@ - +