diff --git a/AUTHORS.md b/AUTHORS.md index 6cfa216b1..4fa522336 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -29,6 +29,7 @@ - Christoph Gohlke ([@cgohlke](https://github.com/cgohlke)) - Christopher Bremner ([@chrisjbremner](https://github.com/chrisjbremner)) - Christopher Pow ([@christopherpow](https://github.com/christopherpow)) +- Colton Sellers ([@C-SELLERS](https://github.com/C-SELLERS)) - Daniel Abrahamsson ([@danabr](https://github.com/danabr)) - Daniel Fernandez ([@fdanny](https://github.com/fdanny)) - Daniel Santana ([@dgsantana](https://github.com/dgsantana)) @@ -52,6 +53,7 @@ - Luke Stratman ([@lstratman](https://github.com/lstratman)) - Konstantin Posudevskiy ([@konstantin-posudevskiy](https://github.com/konstantin-posudevskiy)) - Matthias Dittrich ([@matthid](https://github.com/matthid)) +- Martin Molinero ([@Martin-Molinero](https://github.com/Martin-Molinero)) - Meinrad Recheis ([@henon](https://github.com/henon)) - Mohamed Koubaa ([@koubaa](https://github.com/koubaa)) - Patrick Stewart ([@patstew](https://github.com/patstew)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bee653e8..305c317a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,11 +21,6 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. details about the cause of the failure - `clr.AddReference` no longer adds ".dll" implicitly - `PyIter(PyObject)` constructor replaced with static `PyIter.GetIter(PyObject)` method -- BREAKING: Return values from .NET methods that return an interface are now automatically - wrapped in that interface. This is a breaking change for users that rely on being - able to access members that are part of the implementation class, but not the - interface. Use the new __implementation__ or __raw_implementation__ properties to - if you need to "downcast" to the implementation class. - BREAKING: Parameters marked with `ParameterAttributes.Out` are no longer returned in addition to the regular method return value (unless they are passed with `ref` or `out` keyword). - BREAKING: Drop support for the long-deprecated CLR.* prefix. diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index 875adf8ef..446710e5b 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -34,6 +34,56 @@ public void Dispose() PythonEngine.Shutdown(); } + [Test] + public void ConvertListRoundTrip() + { + var list = new List { typeof(decimal), typeof(int) }; + var py = list.ToPython(); + object result; + var converted = Converter.ToManaged(py.Handle, typeof(List), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(result, list); + } + + [Test] + public void ConvertPyListToArray() + { + var array = new List { typeof(decimal), typeof(int) }; + var py = array.ToPython(); + object result; + var converted = Converter.ToManaged(py.Handle, typeof(Type[]), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(result, array); + } + + [Test] + public void ConvertTimeSpanRoundTrip() + { + var timespan = new TimeSpan(0, 1, 0, 0); + var pyTimedelta = timespan.ToPython(); + + object result; + var converted = Converter.ToManaged(pyTimedelta.Handle, typeof(TimeSpan), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(result, timespan); + } + + [Test] + public void ConvertDateTimeRoundTrip() + { + var datetime = new DateTime(2000, 1, 1); + var pyDatetime = datetime.ToPython(); + + object result; + var converted = Converter.ToManaged(pyDatetime.Handle, typeof(DateTime), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(result, datetime); + } + [Test] public void TestConvertSingleToManaged( [Values(float.PositiveInfinity, float.NegativeInfinity, float.MinValue, float.MaxValue, float.NaN, @@ -136,5 +186,17 @@ public void RawPyObjectProxy() var proxiedHandle = pyObjectProxy.GetAttr("Handle").As(); Assert.AreEqual(pyObject.Handle, proxiedHandle); } + + [Test] + public void PrimitiveIntConversion() + { + decimal value = 10; + var pyValue = value.ToPython(); + + // Try to convert python value to int + var testInt = pyValue.As(); + Assert.AreEqual(testInt , 10); + } + } } diff --git a/src/embed_tests/TestInterfaceClasses.cs b/src/embed_tests/TestInterfaceClasses.cs new file mode 100644 index 000000000..e597d2717 --- /dev/null +++ b/src/embed_tests/TestInterfaceClasses.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class TestInterfaceClasses + { + public string testCode = @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +testModule = TestInterfaceClasses.GetInstance() +print(testModule.Child.ChildBool) + +"; + + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Test] + public void TestInterfaceDerivedClassMembers() + { + // This test gets an instance of the CSharpTestModule in Python + // and then attempts to access it's member "Child"'s bool that is + // not defined in the interface. + PythonEngine.Exec(testCode); + } + + public interface IInterface + { + bool InterfaceBool { get; set; } + } + + public class Parent : IInterface + { + public bool InterfaceBool { get; set; } + public bool ParentBool { get; set; } + } + + public class Child : Parent + { + public bool ChildBool { get; set; } + } + + public class CSharpTestModule + { + public IInterface Child; + + public CSharpTestModule() + { + Child = new Child + { + ChildBool = true, + ParentBool = true, + InterfaceBool = true + }; + } + } + + public static CSharpTestModule GetInstance() + { + return new CSharpTestModule(); + } + } +} diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs new file mode 100644 index 000000000..010e3d2af --- /dev/null +++ b/src/embed_tests/TestMethodBinder.cs @@ -0,0 +1,121 @@ +using System; + +using NUnit.Framework; + +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class TestMethodBinder + { + private static dynamic module; + private static string testModule = @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class PythonModel(TestMethodBinder.CSharpModel): + def TestA(self): + return self.OnlyString(TestMethodBinder.TestImplicitConversion()) + def TestB(self): + return self.OnlyClass('input string') + def TestC(self): + return self.InvokeModel('input string') + def TestD(self): + return self.InvokeModel(TestMethodBinder.TestImplicitConversion()) + def TestE(self, array): + return array.Length == 2"; + + + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + module = PythonEngine.ModuleFromString("module", testModule).GetAttr("PythonModel").Invoke(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Test] + public void ImplicitConversionToString() + { + var data = (string)module.TestA(); + // we assert implicit conversion took place + Assert.AreEqual("OnlyString impl: implicit to string", data); + } + + [Test] + public void ImplicitConversionToClass() + { + var data = (string)module.TestB(); + // we assert implicit conversion took place + Assert.AreEqual("OnlyClass impl", data); + } + + [Test] + public void WillAvoidUsingImplicitConversionIfPossible_String() + { + var data = (string)module.TestC(); + // we assert no implicit conversion took place + Assert.AreEqual("string impl: input string", data); + } + + [Test] + public void WillAvoidUsingImplicitConversionIfPossible_Class() + { + var data = (string)module.TestD(); + // we assert no implicit conversion took place + Assert.AreEqual("TestImplicitConversion impl", data); + + } + + [Test] + public void ArrayLength() + { + var array = new[] { "pepe", "pinocho" }; + var data = (bool)module.TestE(array); + + // Assert it is true + Assert.AreEqual(true, data); + } + + public class CSharpModel + { + public virtual string OnlyClass(TestImplicitConversion data) + { + return "OnlyClass impl"; + } + + public virtual string OnlyString(string data) + { + return "OnlyString impl: " + data; + } + + public virtual string InvokeModel(string data) + { + return "string impl: " + data; + } + + public virtual string InvokeModel(TestImplicitConversion data) + { + return "TestImplicitConversion impl"; + } + } + + public class TestImplicitConversion + { + public static implicit operator string(TestImplicitConversion symbol) + { + return "implicit to string"; + } + public static implicit operator TestImplicitConversion(string symbol) + { + return new TestImplicitConversion(); + } + } + } +} diff --git a/src/embed_tests/TestOperator.cs b/src/embed_tests/TestOperator.cs index 8e9feb241..c90001267 100644 --- a/src/embed_tests/TestOperator.cs +++ b/src/embed_tests/TestOperator.cs @@ -335,6 +335,29 @@ public void SymmetricalOperatorOverloads() "); } + [Test] + public void OperatorInequality() + { + string name = string.Format("{0}.{1}", + typeof(OperableObject).DeclaringType.Name, + typeof(OperableObject).Name); + string module = MethodBase.GetCurrentMethod().DeclaringType.Namespace; + + PythonEngine.Exec($@" +from {module} import * +cls = {name} +b = cls(10) +a = cls(2) + + +c = a <= b +assert c == (a.Num <= b.Num) + +c = a >= b +assert c == (a.Num >= b.Num) +"); + } + [Test] public void OperatorOverloadMissingArgument() { diff --git a/src/runtime/arrayobject.cs b/src/runtime/arrayobject.cs index 5c97c6dbf..de4166091 100644 --- a/src/runtime/arrayobject.cs +++ b/src/runtime/arrayobject.cs @@ -137,13 +137,8 @@ static NewReference NewInstance(Type elementType, BorrowedReference arrayPyType, public new static IntPtr mp_subscript(IntPtr ob, IntPtr idx) { var obj = (CLRObject)GetManagedObject(ob); - var arrObj = (ArrayObject)GetManagedObjectType(ob); - if (!arrObj.type.Valid) - { - return Exceptions.RaiseTypeError(arrObj.type.DeletedMessage); - } var items = obj.inst as Array; - Type itemType = arrObj.type.Value.GetElementType(); + Type itemType = obj.inst.GetType().GetElementType(); int rank = items.Rank; int index; object value; diff --git a/src/runtime/assemblymanager.cs b/src/runtime/assemblymanager.cs index 0387d2dfc..165e6d53d 100644 --- a/src/runtime/assemblymanager.cs +++ b/src/runtime/assemblymanager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; namespace Python.Runtime { @@ -25,18 +26,17 @@ internal class AssemblyManager // than it can end up referring to assemblies that are already unloaded (default behavior after unload appDomain - // unless LoaderOptimization.MultiDomain is used); // So for multidomain support it is better to have the dict. recreated for each app-domain initialization - private static ConcurrentDictionary> namespaces = - new ConcurrentDictionary>(); - //private static Dictionary> generics; - private static AssemblyLoadEventHandler lhandler; - private static ResolveEventHandler rhandler; + private static ConcurrentDictionary> namespaces = + new ConcurrentDictionary>(); + private static ConcurrentDictionary assembliesNamesCache = + new ConcurrentDictionary(); + private static ConcurrentQueue assemblies = new ConcurrentQueue(); + private static int pendingAssemblies; // updated only under GIL? private static Dictionary probed = new Dictionary(32); - - // modified from event handlers below, potentially triggered from different .NET threads - private static ConcurrentQueue assemblies; - internal static List pypath; + private static List pypath = new List(16); + private static Dictionary> filesInPath = new Dictionary>(); private AssemblyManager() { @@ -49,30 +49,34 @@ private AssemblyManager() /// internal static void Initialize() { - assemblies = new ConcurrentQueue(); - pypath = new List(16); - AppDomain domain = AppDomain.CurrentDomain; - lhandler = new AssemblyLoadEventHandler(AssemblyLoadHandler); - domain.AssemblyLoad += lhandler; - - rhandler = new ResolveEventHandler(ResolveHandler); - domain.AssemblyResolve += rhandler; + domain.AssemblyLoad += AssemblyLoadHandler; + domain.AssemblyResolve += ResolveHandler; - Assembly[] items = domain.GetAssemblies(); - foreach (Assembly a in items) + foreach (var assembly in domain.GetAssemblies()) { try { - ScanAssembly(a); - assemblies.Enqueue(a); + LaunchAssemblyLoader(assembly); } catch (Exception ex) { - Debug.WriteLine("Error scanning assembly {0}. {1}", a, ex); + Debug.WriteLine("Error scanning assembly {0}. {1}", assembly, ex); } } + + var safeCount = 0; + // lets wait until all assemblies are loaded + do + { + if (safeCount++ > 200) + { + throw new TimeoutException("Timeout while waiting for assemblies to load"); + } + + Thread.Sleep(50); + } while (pendingAssemblies > 0); } @@ -82,8 +86,8 @@ internal static void Initialize() internal static void Shutdown() { AppDomain domain = AppDomain.CurrentDomain; - domain.AssemblyLoad -= lhandler; - domain.AssemblyResolve -= rhandler; + domain.AssemblyLoad -= AssemblyLoadHandler; + domain.AssemblyResolve -= ResolveHandler; } @@ -97,8 +101,35 @@ internal static void Shutdown() private static void AssemblyLoadHandler(object ob, AssemblyLoadEventArgs args) { Assembly assembly = args.LoadedAssembly; - assemblies.Enqueue(assembly); - ScanAssembly(assembly); + LaunchAssemblyLoader(assembly); + } + + /// + /// Launches a new task that will load the provided assembly + /// + private static void LaunchAssemblyLoader(Assembly assembly) + { + if (assembly != null) + { + Interlocked.Increment(ref pendingAssemblies); + Task.Factory.StartNew(() => + { + try + { + if (assembliesNamesCache.TryAdd(assembly.GetName().Name, assembly)) + { + assemblies.Enqueue(assembly); + ScanAssembly(assembly); + } + } + catch + { + // pass + } + + Interlocked.Decrement(ref pendingAssemblies); + }); + } } @@ -112,12 +143,12 @@ private static void AssemblyLoadHandler(object ob, AssemblyLoadEventArgs args) private static Assembly ResolveHandler(object ob, ResolveEventArgs args) { string name = args.Name.ToLower(); - foreach (Assembly a in assemblies) + foreach (var assembly in assemblies) { - string full = a.FullName.ToLower(); + var full = assembly.FullName.ToLower(); if (full.StartsWith(name)) { - return a; + return assembly; } } return LoadAssemblyPath(args.Name); @@ -139,19 +170,60 @@ internal static void UpdatePath() { BorrowedReference list = Runtime.PySys_GetObject("path"); var count = Runtime.PyList_Size(list); + var sep = Path.DirectorySeparatorChar; + if (count != pypath.Count) { pypath.Clear(); probed.Clear(); + for (var i = 0; i < count; i++) { BorrowedReference item = Runtime.PyList_GetItem(list, i); string path = Runtime.GetManagedString(item); if (path != null) { - pypath.Add(path); + pypath.Add(path == string.Empty ? path : path + sep); } } + + // for performance we will search for all files in each directory in the path once + Parallel.ForEach(pypath.Where(s => + { + try + { + lock (filesInPath) + { + // only search in directory if it exists and we haven't already analyzed it + return Directory.Exists(s) && !filesInPath.ContainsKey(s); + } + } + catch + { + // just in case, file operations can throw + } + return false; + }), path => + { + var container = new HashSet(); + try + { + foreach (var file in Directory.EnumerateFiles(path) + .Where(file => file.EndsWith(".dll") || file.EndsWith(".exe"))) + { + container.Add(Path.GetFileName(file)); + } + } + catch + { + // just in case, file operations can throw + } + + lock (filesInPath) + { + filesInPath[path] = container; + } + }); } } @@ -163,30 +235,18 @@ internal static void UpdatePath() /// public static string FindAssembly(string name) { - char sep = Path.DirectorySeparatorChar; - - foreach (string head in pypath) + foreach (var kvp in filesInPath) { - string path; - if (head == null || head.Length == 0) - { - path = name; - } - else - { - path = head + sep + name; - } - - string temp = path + ".dll"; - if (File.Exists(temp)) + var dll = $"{name}.dll"; + if (kvp.Value.Contains(dll)) { - return temp; + return kvp.Key + dll; } - temp = path + ".exe"; - if (File.Exists(temp)) + var executable = $"{name}.exe"; + if (kvp.Value.Contains(executable)) { - return temp; + return kvp.Key + executable; } } return null; @@ -242,14 +302,8 @@ public static Assembly LoadAssemblyFullPath(string name) /// public static Assembly FindLoadedAssembly(string name) { - foreach (Assembly a in assemblies) - { - if (a.GetName().Name == name) - { - return a; - } - } - return null; + Assembly result; + return assembliesNamesCache.TryGetValue(name, out result) ? result : null; } /// @@ -267,6 +321,7 @@ internal static void ScanAssembly(Assembly assembly) // A couple of things we want to do here: first, we want to // gather a list of all of the namespaces contributed to by // the assembly. + foreach (Type t in GetTypes(assembly)) { string ns = t.Namespace ?? ""; @@ -277,13 +332,13 @@ internal static void ScanAssembly(Assembly assembly) for (var n = 0; n < names.Length; n++) { s = n == 0 ? names[0] : s + "." + names[n]; - namespaces.TryAdd(s, new ConcurrentDictionary()); + namespaces.TryAdd(s, new ConcurrentDictionary()); } } if (ns != null) { - namespaces[ns].TryAdd(assembly, string.Empty); + namespaces[ns].TryAdd(assembly, 1); } if (ns != null && t.IsGenericTypeDefinition) diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index 1ee06e682..2de454c66 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -177,9 +177,8 @@ internal static Dictionary RestoreRuntimeData(R /// A Borrowed reference to the ClassBase object internal static ClassBase GetClass(Type type) { - ClassBase cb = null; - cache.TryGetValue(type, out cb); - if (cb != null) + ClassBase cb; + if (cache.TryGetValue(type, out cb)) { return cb; } @@ -225,6 +224,11 @@ private static ClassBase CreateClass(Type type) impl = new ArrayObject(type); } + else if (type.IsKeyValuePairEnumerable()) + { + impl = new KeyValuePairEnumerableObject(type); + } + else if (type.IsInterface) { impl = new InterfaceObject(type); diff --git a/src/runtime/classobject.cs b/src/runtime/classobject.cs index 4aa97f648..2f8da8a54 100644 --- a/src/runtime/classobject.cs +++ b/src/runtime/classobject.cs @@ -33,15 +33,15 @@ internal ClassObject(Type tp) : base(tp) /// internal NewReference GetDocString() { - MethodBase[] methods = binder.GetMethods(); + var methods = binder.GetMethods(); var str = ""; - foreach (MethodBase t in methods) + foreach (var t in methods) { if (str.Length > 0) { str += Environment.NewLine; } - str += t.ToString(); + str += t.MethodBase.ToString(); } return NewReference.DangerousFromPointer(Runtime.PyString_FromString(str)); } diff --git a/src/runtime/constructorbinding.cs b/src/runtime/constructorbinding.cs index 803823e39..b3c6b655c 100644 --- a/src/runtime/constructorbinding.cs +++ b/src/runtime/constructorbinding.cs @@ -126,7 +126,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(); if (!self.type.Valid) { @@ -134,8 +134,9 @@ public static IntPtr tp_repr(IntPtr ob) } string name = self.type.Value.FullName; var doc = ""; - foreach (MethodBase t in methods) + foreach (var methodInformation in methods) { + var t = methodInformation.MethodBase; if (doc.Length > 0) { doc += "\n"; diff --git a/src/runtime/converter.cs b/src/runtime/converter.cs index f3b378113..342687647 100644 --- a/src/runtime/converter.cs +++ b/src/runtime/converter.cs @@ -30,6 +30,9 @@ private Converter() private static Type flagsType; private static Type boolType; private static Type typeType; + private static IntPtr dateTimeCtor; + private static IntPtr timeSpanCtor; + private static IntPtr tzInfoCtor; static Converter() { @@ -45,6 +48,31 @@ static Converter() flagsType = typeof(FlagsAttribute); boolType = typeof(Boolean); typeType = typeof(Type); + + IntPtr dateTimeMod = Runtime.PyImport_ImportModule("datetime"); + if (dateTimeMod == null) throw new PythonException(); + + dateTimeCtor = Runtime.PyObject_GetAttrString(dateTimeMod, "datetime"); + if (dateTimeCtor == null) throw new PythonException(); + + timeSpanCtor = Runtime.PyObject_GetAttrString(dateTimeMod, "timedelta"); + if (timeSpanCtor == null) throw new PythonException(); + + IntPtr tzInfoMod = PythonEngine.ModuleFromString("custom_tzinfo", @" +from datetime import timedelta, tzinfo +class GMT(tzinfo): + def __init__(self, hours, minutes): + self.hours = hours + self.minutes = minutes + def utcoffset(self, dt): + return timedelta(hours=self.hours, minutes=self.minutes) + def tzname(self, dt): + return f'GMT {self.hours:00}:{self.minutes:00}' + def dst (self, dt): + return timedelta(0)").Handle; + + tzInfoCtor = Runtime.PyObject_GetAttrString(tzInfoMod, "GMT"); + if (tzInfoCtor == null) throw new PythonException(); } @@ -71,6 +99,9 @@ internal static Type GetTypeByAlias(IntPtr op) if (op == Runtime.PyBoolType) return boolType; + if (op == Runtime.PyDecimalType) + return decimalType; + return null; } @@ -97,6 +128,9 @@ internal static IntPtr GetPythonTypeByAlias(Type op) if (op == boolType) return Runtime.PyBoolType; + if (op == decimalType) + return Runtime.PyDecimalType; + return IntPtr.Zero; } @@ -173,21 +207,6 @@ internal static IntPtr ToPython(object value, Type type) } } - if (type.IsInterface) - { - var ifaceObj = (InterfaceObject)ClassManager.GetClass(type); - return ifaceObj.WrapObject(value); - } - - // We need to special case interface array handling to ensure we - // produce the correct type. Value may be an array of some concrete - // type (FooImpl[]), but we want access to go via the interface type - // (IFoo[]). - if (type.IsArray && type.GetElementType().IsInterface) - { - return CLRObject.GetInstHandle(value, type); - } - // it the type is a python subclass of a managed type then return the // underlying python object rather than construct a new wrapper object. var pyderived = value as IPythonDerivedType; @@ -208,6 +227,17 @@ internal static IntPtr ToPython(object value, Type type) switch (tc) { case TypeCode.Object: + if (value is TimeSpan) + { + var timespan = (TimeSpan)value; + + IntPtr timeSpanArgs = Runtime.PyTuple_New(1); + Runtime.PyTuple_SetItem(timeSpanArgs, 0, Runtime.PyFloat_FromDouble(timespan.TotalDays)); + var returnTimeSpan = Runtime.PyObject_CallObject(timeSpanCtor, timeSpanArgs); + // clean up + Runtime.XDecref(timeSpanArgs); + return returnTimeSpan; + } return CLRObject.GetInstHandle(value, type); case TypeCode.String: @@ -260,6 +290,41 @@ internal static IntPtr ToPython(object value, Type type) case TypeCode.UInt64: return Runtime.PyLong_FromUnsignedLongLong((ulong)value); + case TypeCode.Decimal: + // C# decimal to python decimal has a big impact on performance + // so we will use C# double and python float + return Runtime.PyFloat_FromDouble(decimal.ToDouble((decimal)value)); + + case TypeCode.DateTime: + var datetime = (DateTime)value; + + var size = datetime.Kind == DateTimeKind.Unspecified ? 7 : 8; + + IntPtr dateTimeArgs = Runtime.PyTuple_New(size); + Runtime.PyTuple_SetItem(dateTimeArgs, 0, Runtime.PyInt_FromInt32(datetime.Year)); + Runtime.PyTuple_SetItem(dateTimeArgs, 1, Runtime.PyInt_FromInt32(datetime.Month)); + Runtime.PyTuple_SetItem(dateTimeArgs, 2, Runtime.PyInt_FromInt32(datetime.Day)); + Runtime.PyTuple_SetItem(dateTimeArgs, 3, Runtime.PyInt_FromInt32(datetime.Hour)); + Runtime.PyTuple_SetItem(dateTimeArgs, 4, Runtime.PyInt_FromInt32(datetime.Minute)); + Runtime.PyTuple_SetItem(dateTimeArgs, 5, Runtime.PyInt_FromInt32(datetime.Second)); + + // datetime.datetime 6th argument represents micro seconds + var totalSeconds = datetime.TimeOfDay.TotalSeconds; + var microSeconds = Convert.ToInt32((totalSeconds - Math.Truncate(totalSeconds)) * 1000000); + if (microSeconds == 1000000) microSeconds = 999999; + Runtime.PyTuple_SetItem(dateTimeArgs, 6, Runtime.PyInt_FromInt32(microSeconds)); + + if (size == 8) + { + Runtime.PyTuple_SetItem(dateTimeArgs, 7, TzInfo(datetime.Kind)); + } + + var returnDateTime = Runtime.PyObject_CallObject(dateTimeCtor, dateTimeArgs); + // clean up + Runtime.XDecref(dateTimeArgs); + return returnDateTime; + + default: if (value is IEnumerable) { @@ -281,6 +346,17 @@ internal static IntPtr ToPython(object value, Type type) } } + private static IntPtr TzInfo(DateTimeKind kind) + { + if (kind == DateTimeKind.Unspecified) return Runtime.PyNone; + var offset = kind == DateTimeKind.Local ? DateTimeOffset.Now.Offset : TimeSpan.Zero; + IntPtr tzInfoArgs = Runtime.PyTuple_New(2); + Runtime.PyTuple_SetItem(tzInfoArgs, 0, Runtime.PyFloat_FromDouble(offset.Hours)); + Runtime.PyTuple_SetItem(tzInfoArgs, 1, Runtime.PyFloat_FromDouble(offset.Minutes)); + var returnValue = Runtime.PyObject_CallObject(tzInfoCtor, tzInfoArgs); + Runtime.XDecref(tzInfoArgs); + return returnValue; + } /// /// In a few situations, we don't have any advisory type information @@ -299,6 +375,12 @@ internal static IntPtr ToPythonImplicit(object value) } + internal static bool ToManaged(IntPtr value, Type type, + out object result, bool setError) + { + var usedImplicit = false; + return ToManaged(value, type, out result, setError, out usedImplicit); + } /// /// Return a managed object for the given Python object, taking funny /// byref types into account. @@ -309,21 +391,26 @@ internal static IntPtr ToPythonImplicit(object value) /// If true, call Exceptions.SetError with the reason for failure. /// True on success internal static bool ToManaged(IntPtr value, Type type, - out object result, bool setError) + out object result, bool setError, out bool usedImplicit) { if (type.IsByRef) { type = type.GetElementType(); } - return Converter.ToManagedValue(value, type, out result, setError); + return Converter.ToManagedValue(value, type, out result, setError, out usedImplicit); } internal static bool ToManagedValue(BorrowedReference value, Type obType, out object result, bool setError) - => ToManagedValue(value.DangerousGetAddress(), obType, out result, setError); + { + var usedImplicit = false; + return ToManagedValue(value.DangerousGetAddress(), obType, out result, setError, out usedImplicit); + } + internal static bool ToManagedValue(IntPtr value, Type obType, - out object result, bool setError) + out object result, bool setError, out bool usedImplicit) { + usedImplicit = false; if (obType == typeof(PyObject)) { Runtime.XIncref(value); // PyObject() assumes ownership @@ -331,6 +418,15 @@ internal static bool ToManagedValue(IntPtr value, Type obType, return true; } + if (obType.IsGenericType && Runtime.PyObject_TYPE(value) == Runtime.PyListType) + { + var typeDefinition = obType.GetGenericTypeDefinition(); + if (typeDefinition == typeof(List<>)) + { + return ToList(value, obType, out result, setError); + } + } + // Common case: if the Python value is a wrapped managed object // instance, just return the wrapped object. ManagedType mt = ManagedType.GetManagedObject(value); @@ -346,6 +442,18 @@ internal static bool ToManagedValue(IntPtr value, Type obType, result = tmp; return true; } + else + { + var type = tmp.GetType(); + // check implicit conversions that receive tmp type and return obType + var conversionMethod = type.GetMethod("op_Implicit", new[] { type }); + if (conversionMethod != null && conversionMethod.ReturnType == obType) + { + result = conversionMethod.Invoke(null, new[] { tmp }); + usedImplicit = true; + return true; + } + } if (setError) { string typeString = tmp is null ? "null" : tmp.GetType().ToString(); @@ -496,6 +604,12 @@ internal static bool ToManagedValue(IntPtr value, Type obType, return false; } + var underlyingType = Nullable.GetUnderlyingType(obType); + if (underlyingType != null) + { + return ToManagedValue(value, underlyingType, out result, setError, out usedImplicit); + } + TypeCode typeCode = Type.GetTypeCode(obType); if (typeCode == TypeCode.Object) { @@ -506,6 +620,20 @@ internal static bool ToManagedValue(IntPtr value, Type obType, } } + var opImplicit = obType.GetMethod("op_Implicit", new[] { obType }); + if (opImplicit != null) + { + if (ToManagedValue(value, opImplicit.ReturnType, out result, setError, out usedImplicit)) + { + opImplicit = obType.GetMethod("op_Implicit", new[] { result.GetType() }); + if (opImplicit != null) + { + result = opImplicit.Invoke(null, new[] { result }); + } + return opImplicit != null; + } + } + return ToPrimitive(value, obType, out result, setError); } @@ -522,6 +650,32 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo switch (tc) { + case TypeCode.Object: + if (obType == typeof(TimeSpan)) + { + op = Runtime.PyObject_Str(value); + TimeSpan ts; + var arr = Runtime.GetManagedString(op).Split(','); + string sts = arr.Length == 1 ? arr[0] : arr[1]; + if (!TimeSpan.TryParse(sts, out ts)) + { + goto type_error; + } + Runtime.XDecref(op); + + int days = 0; + if (arr.Length > 1) + { + if (!int.TryParse(arr[0].Split(' ')[0].Trim(), out days)) + { + goto type_error; + } + } + result = ts.Add(TimeSpan.FromDays(days)); + return true; + } + break; + case TypeCode.String: string st = Runtime.GetManagedString(value); if (st == null) @@ -534,7 +688,12 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo case TypeCode.Int32: { // Python3 always use PyLong API - nint num = Runtime.PyLong_AsSignedSize_t(value); + op = Runtime.PyNumber_Long(value); + if (op == IntPtr.Zero && Exceptions.ErrorOccurred()) + { + goto convert_error; + } + nint num = Runtime.PyLong_AsSignedSize_t(op); if (num == -1 && Exceptions.ErrorOccurred()) { goto convert_error; @@ -642,7 +801,12 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo case TypeCode.Int16: { - nint num = Runtime.PyLong_AsSignedSize_t(value); + op = Runtime.PyNumber_Long(value); + if (op == IntPtr.Zero && Exceptions.ErrorOccurred()) + { + goto convert_error; + } + nint num = Runtime.PyLong_AsSignedSize_t(op); if (num == -1 && Exceptions.ErrorOccurred()) { goto convert_error; @@ -673,7 +837,12 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo } else { - nint num = Runtime.PyLong_AsSignedSize_t(value); + op = Runtime.PyNumber_Long(value); + if (op == IntPtr.Zero && Exceptions.ErrorOccurred()) + { + goto convert_error; + } + nint num = Runtime.PyLong_AsSignedSize_t(op); if (num == -1 && Exceptions.ErrorOccurred()) { goto convert_error; @@ -685,7 +854,12 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo case TypeCode.UInt16: { - nint num = Runtime.PyLong_AsSignedSize_t(value); + op = Runtime.PyNumber_Long(value); + if (op == IntPtr.Zero && Exceptions.ErrorOccurred()) + { + goto convert_error; + } + nint num = Runtime.PyLong_AsSignedSize_t(op); if (num == -1 && Exceptions.ErrorOccurred()) { goto convert_error; @@ -700,7 +874,12 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo case TypeCode.UInt32: { - nuint num = Runtime.PyLong_AsUnsignedSize_t(value); + op = Runtime.PyNumber_Long(value); + if (op == IntPtr.Zero && Exceptions.ErrorOccurred()) + { + goto convert_error; + } + nuint num = Runtime.PyLong_AsUnsignedSize_t(op); if (num == unchecked((nuint)(-1)) && Exceptions.ErrorOccurred()) { goto convert_error; @@ -715,7 +894,12 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo case TypeCode.UInt64: { - ulong num = Runtime.PyLong_AsUnsignedLongLong(value); + op = Runtime.PyNumber_Long(value); + if (op == IntPtr.Zero && Exceptions.ErrorOccurred()) + { + goto convert_error; + } + ulong num = Runtime.PyLong_AsUnsignedLongLong(op); if (num == ulong.MaxValue && Exceptions.ErrorOccurred()) { goto convert_error; @@ -752,6 +936,28 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object result, bo result = num; return true; } + case TypeCode.Decimal: + op = Runtime.PyObject_Str(value); + decimal m; + string sm = Runtime.GetManagedString(op); + if (!Decimal.TryParse(sm, NumberStyles.Number | NumberStyles.AllowExponent, nfi, out m)) + { + goto type_error; + } + Runtime.XDecref(op); + result = m; + return true; + case TypeCode.DateTime: + op = Runtime.PyObject_Str(value); + DateTime dt; + string sdt = Runtime.GetManagedString(op); + if (!DateTime.TryParse(sdt, out dt)) + { + goto type_error; + } + Runtime.XDecref(op); + result = sdt.EndsWith("+00:00") ? dt.ToUniversalTime() : dt; + return true; default: goto type_error; } @@ -809,7 +1015,7 @@ private static bool ToArray(IntPtr value, Type obType, out object result, bool s result = null; IntPtr IterObject = Runtime.PyObject_GetIter(value); - if (IterObject == IntPtr.Zero) + if (IterObject == IntPtr.Zero || elementType.IsGenericType) { if (setError) { @@ -823,6 +1029,43 @@ private static bool ToArray(IntPtr value, Type obType, out object result, bool s return false; } + var list = MakeList(value, IterObject, obType, elementType, setError); + if (list == null) + { + return false; + } + + Array items = Array.CreateInstance(elementType, list.Count); + list.CopyTo(items, 0); + + result = items; + return true; + } + + /// + /// Convert a Python value to a correctly typed managed list instance. + /// The Python value must support the Python sequence protocol and the + /// items in the sequence must be convertible to the target list type. + /// + private static bool ToList(IntPtr value, Type obType, out object result, bool setError) + { + var elementType = obType.GetGenericArguments()[0]; + IntPtr IterObject = Runtime.PyObject_GetIter(value); + result = MakeList(value, IterObject, obType, elementType, setError); + return result != null; + } + + /// + /// Helper function for ToArray and ToList that creates a IList out of iterable objects + /// + /// + /// + /// + /// + /// + /// + private static IList MakeList(IntPtr value, IntPtr IterObject, Type obType, Type elementType, bool setError) + { IList list; try { @@ -850,19 +1093,20 @@ private static bool ToArray(IntPtr value, Type obType, out object result, bool s Exceptions.SetError(e); SetConversionError(value, obType); } - return false; + + return null; } IntPtr item; - + var usedImplicit = false; while ((item = Runtime.PyIter_Next(IterObject)) != IntPtr.Zero) { object obj; - if (!Converter.ToManaged(item, elementType, out obj, setError)) + if (!Converter.ToManaged(item, elementType, out obj, setError, out usedImplicit)) { Runtime.XDecref(item); - return false; + return null; } list.Add(obj); @@ -870,11 +1114,7 @@ private static bool ToArray(IntPtr value, Type obType, out object result, bool s } Runtime.XDecref(IterObject); - Array items = Array.CreateInstance(elementType, list.Count); - list.CopyTo(items, 0); - - result = items; - return true; + return list; } diff --git a/src/runtime/genericutil.cs b/src/runtime/genericutil.cs index 92b847e98..de00b2550 100644 --- a/src/runtime/genericutil.cs +++ b/src/runtime/genericutil.cs @@ -27,25 +27,30 @@ public static void Reset() /// A generic type definition (t.IsGenericTypeDefinition must be true) internal static void Register(Type t) { - if (null == t.Namespace || null == t.Name) + lock (mapping) { - return; - } + if (null == t.Namespace || null == t.Name) + { + return; + } - Dictionary> nsmap; - if (!mapping.TryGetValue(t.Namespace, out nsmap)) - { - nsmap = new Dictionary>(); - mapping[t.Namespace] = nsmap; - } - string basename = GetBasename(t.Name); - List gnames; - if (!nsmap.TryGetValue(basename, out gnames)) - { - gnames = new List(); - nsmap[basename] = gnames; + Dictionary> nsmap; + if (!mapping.TryGetValue(t.Namespace, out nsmap)) + { + nsmap = new Dictionary>(); + mapping[t.Namespace] = nsmap; + } + + string basename = GetBasename(t.Name); + List gnames; + if (!nsmap.TryGetValue(basename, out gnames)) + { + gnames = new List(); + nsmap[basename] = gnames; + } + + gnames.Add(t.Name); } - gnames.Add(t.Name); } /// @@ -53,17 +58,20 @@ internal static void Register(Type t) /// public static List GetGenericBaseNames(string ns) { - Dictionary> nsmap; - if (!mapping.TryGetValue(ns, out nsmap)) + lock (mapping) { - return null; - } - var names = new List(); - foreach (string key in nsmap.Keys) - { - names.Add(key); + Dictionary> nsmap; + if (!mapping.TryGetValue(ns, out nsmap)) + { + return null; + } + var names = new List(); + foreach (string key in nsmap.Keys) + { + names.Add(key); + } + return names; } - return names; } /// @@ -79,29 +87,32 @@ public static Type GenericForType(Type t, int paramCount) /// public static Type GenericByName(string ns, string basename, int paramCount) { - Dictionary> nsmap; - if (!mapping.TryGetValue(ns, out nsmap)) + lock (mapping) { - return null; - } + Dictionary> nsmap; + if (!mapping.TryGetValue(ns, out nsmap)) + { + return null; + } - List names; - if (!nsmap.TryGetValue(GetBasename(basename), out names)) - { - return null; - } + List names; + if (!nsmap.TryGetValue(GetBasename(basename), out names)) + { + return null; + } - foreach (string name in names) - { - string qname = ns + "." + name; - Type o = AssemblyManager.LookupTypes(qname).FirstOrDefault(); - if (o != null && o.GetGenericArguments().Length == paramCount) + foreach (string name in names) { - return o; + string qname = ns + "." + name; + Type o = AssemblyManager.LookupTypes(qname).FirstOrDefault(); + if (o != null && o.GetGenericArguments().Length == paramCount) + { + return o; + } } - } - return null; + return null; + } } /// @@ -109,17 +120,22 @@ public static Type GenericByName(string ns, string basename, int paramCount) /// public static string GenericNameForBaseName(string ns, string name) { - Dictionary> nsmap; - if (!mapping.TryGetValue(ns, out nsmap)) + lock (mapping) { - return null; - } - List gnames; - nsmap.TryGetValue(name, out gnames); - if (gnames?.Count > 0) - { - return gnames[0]; + Dictionary> nsmap; + if (!mapping.TryGetValue(ns, out nsmap)) + { + return null; + } + + List gnames; + nsmap.TryGetValue(name, out gnames); + if (gnames?.Count > 0) + { + return gnames[0]; + } } + return null; } diff --git a/src/runtime/indexer.cs b/src/runtime/indexer.cs index 0772b57c6..b20143067 100644 --- a/src/runtime/indexer.cs +++ b/src/runtime/indexer.cs @@ -58,13 +58,13 @@ 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; } - MethodBase mi = methods[0]; + var mi = methods[0].MethodBase; ParameterInfo[] pi = mi.GetParameters(); // need to subtract one for the value int clrnargs = pi.Length - 1; @@ -99,8 +99,8 @@ internal IntPtr GetDefaultArgs(IntPtr args) var pynargs = Runtime.PyTuple_Size(args); // Get the default arg tuple - MethodBase[] methods = SetterBinder.GetMethods(); - MethodBase mi = methods[0]; + var methods = SetterBinder.GetMethods(); + var mi = methods[0].MethodBase; ParameterInfo[] pi = mi.GetParameters(); int clrnargs = pi.Length - 1; IntPtr defaultArgs = Runtime.PyTuple_New(clrnargs - pynargs); diff --git a/src/runtime/interfaceobject.cs b/src/runtime/interfaceobject.cs index 976c09be0..30612f051 100644 --- a/src/runtime/interfaceobject.cs +++ b/src/runtime/interfaceobject.cs @@ -76,43 +76,7 @@ public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw) return IntPtr.Zero; } - return self.WrapObject(obj); - } - - /// - /// Wrap the given object in an interface object, so that only methods - /// of the interface are available. - /// - public IntPtr WrapObject(object impl) - { - var objPtr = CLRObject.GetInstHandle(impl, pyHandle); - return objPtr; - } - - /// - /// Expose the wrapped implementation through attributes in both - /// converted/encoded (__implementation__) and raw (__raw_implementation__) form. - /// - public static IntPtr tp_getattro(IntPtr ob, IntPtr key) - { - var clrObj = (CLRObject)GetManagedObject(ob); - - if (!Runtime.PyString_Check(key)) - { - return Exceptions.RaiseTypeError("string expected"); - } - - string name = Runtime.GetManagedString(key); - if (name == "__implementation__") - { - return Converter.ToPython(clrObj.inst); - } - else if (name == "__raw_implementation__") - { - return CLRObject.GetInstHandle(clrObj.inst); - } - - return Runtime.PyObject_GenericGetAttr(ob, key); + return CLRObject.GetInstHandle(obj, self.pyHandle); } } } diff --git a/src/runtime/keyvaluepairenumerableobject.cs b/src/runtime/keyvaluepairenumerableobject.cs new file mode 100644 index 000000000..c1644442c --- /dev/null +++ b/src/runtime/keyvaluepairenumerableobject.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Python.Runtime +{ + /// + /// Implements a Python type for managed KeyValuePairEnumerable (dictionaries). + /// This type is essentially the same as a ClassObject, except that it provides + /// sequence semantics to support natural dictionary usage (__contains__ and __len__) + /// from Python. + /// + internal class KeyValuePairEnumerableObject : ClassObject + { + private static Dictionary, MethodInfo> methodsByType = new Dictionary, MethodInfo>(); + private static List requiredMethods = new List { "Count", "ContainsKey" }; + + internal static bool VerifyMethodRequirements(Type type) + { + foreach (var requiredMethod in requiredMethods) + { + var method = type.GetMethod(requiredMethod); + if (method == null) + { + method = type.GetMethod($"get_{requiredMethod}"); + if (method == null) + { + return false; + } + } + + var key = Tuple.Create(type, requiredMethod); + methodsByType.Add(key, method); + } + + return true; + } + + internal KeyValuePairEnumerableObject(Type tp) : base(tp) + { + + } + + internal override bool CanSubclass() => false; + + /// + /// Implements __len__ for dictionary types. + /// + public static int mp_length(IntPtr ob) + { + var obj = (CLRObject)GetManagedObject(ob); + var self = obj.inst; + + var key = Tuple.Create(self.GetType(), "Count"); + var methodInfo = methodsByType[key]; + + return (int)methodInfo.Invoke(self, null); + } + + /// + /// Implements __contains__ for dictionary types. + /// + public static int sq_contains(IntPtr ob, IntPtr v) + { + var obj = (CLRObject)GetManagedObject(ob); + var self = obj.inst; + + var key = Tuple.Create(self.GetType(), "ContainsKey"); + var methodInfo = methodsByType[key]; + + var parameters = methodInfo.GetParameters(); + object arg; + if (!Converter.ToManaged(v, parameters[0].ParameterType, out arg, false)) + { + Exceptions.SetError(Exceptions.TypeError, + $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {parameters[0].ParameterType}"); + } + + return (bool)methodInfo.Invoke(self, new[] { arg }) ? 1 : 0; + } + } + + public static class KeyValuePairEnumerableObjectExtension + { + public static bool IsKeyValuePairEnumerable(this Type type) + { + var iEnumerableType = typeof(IEnumerable<>); + var keyValuePairType = typeof(KeyValuePair<,>); + + var interfaces = type.GetInterfaces(); + foreach (var i in interfaces) + { + if (i.IsGenericType && + i.GetGenericTypeDefinition() == iEnumerableType) + { + var arguments = i.GetGenericArguments(); + if (arguments.Length != 1) continue; + + var a = arguments[0]; + if (a.IsGenericType && + a.GetGenericTypeDefinition() == keyValuePairType && + a.GetGenericArguments().Length == 2) + { + return KeyValuePairEnumerableObject.VerifyMethodRequirements(type); + } + } + } + + return false; + } + } +} diff --git a/src/runtime/managedtype.cs b/src/runtime/managedtype.cs index 09e8a3be2..1708a2da2 100644 --- a/src/runtime/managedtype.cs +++ b/src/runtime/managedtype.cs @@ -109,25 +109,6 @@ internal static ManagedType GetManagedObject(IntPtr ob) return null; } - /// - /// Given a Python object, return the associated managed object type or null. - /// - internal static ManagedType GetManagedObjectType(IntPtr ob) - { - if (ob != IntPtr.Zero) - { - IntPtr tp = Runtime.PyObject_TYPE(ob); - var flags = Util.ReadCLong(tp, TypeOffset.tp_flags); - if ((flags & TypeFlags.Managed) != 0) - { - tp = Marshal.ReadIntPtr(tp, TypeOffset.magic()); - var gc = (GCHandle)tp; - return (ManagedType)gc.Target; - } - } - return null; - } - internal static ManagedType GetManagedObjectErr(IntPtr ob) { diff --git a/src/runtime/methodbinder.cs b/src/runtime/methodbinder.cs index 8f74e0052..02a9074ad 100644 --- a/src/runtime/methodbinder.cs +++ b/src/runtime/methodbinder.cs @@ -1,13 +1,12 @@ using System; using System.Collections; -using System.Reflection; -using System.Text; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Text; namespace Python.Runtime { - using MaybeMethodBase = MaybeMethodBase; /// /// A MethodBinder encapsulates information about a (possibly overloaded) /// managed method, and is responsible for selecting the right method given @@ -17,27 +16,19 @@ namespace Python.Runtime [Serializable] internal class MethodBinder { - /// - /// The overloads of this method - /// - public List list; - - [NonSerialized] - public MethodBase[] methods; - - [NonSerialized] - public bool init = false; + private List list; public const bool DefaultAllowThreads = true; public bool allow_threads = DefaultAllowThreads; + public bool init = false; internal MethodBinder() { - list = new List(); + list = new List(); } internal MethodBinder(MethodInfo mi) { - list = new List { new MaybeMethodBase(mi) }; + list = new List { new MethodInformation(mi, mi.GetParameters()) }; } public int Count @@ -47,7 +38,9 @@ public int Count internal void AddMethod(MethodBase m) { - list.Add(m); + // we added a new method so we have to re sort the method list + init = false; + list.Add(new MethodInformation(m, m.GetParameters())); } /// @@ -86,7 +79,6 @@ internal static MethodInfo MatchSignature(MethodInfo[] mi, Type[] tp) /// /// Given a sequence of MethodInfo and a sequence of type parameters, /// return the MethodInfo that represents the matching closed generic. - /// If unsuccessful, returns null and may set a Python error. /// internal static MethodInfo MatchParameters(MethodInfo[] mi, Type[] tp) { @@ -179,16 +171,15 @@ 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 = (from method in list where method.Valid select method.Value).ToArray(); init = true; } - return methods; + return list; } /// @@ -199,21 +190,24 @@ internal MethodBase[] GetMethods() /// Based from Jython `org.python.core.ReflectedArgs.precedence` /// See: https://github.com/jythontools/jython/blob/master/src/org/python/core/ReflectedArgs.java#L192 /// - internal static int GetPrecedence(MethodBase mi) + private static int GetPrecedence(MethodInformation methodInformation) { - if (mi == null) - { - return int.MaxValue; - } - - ParameterInfo[] pi = mi.GetParameters(); + ParameterInfo[] pi = methodInformation.ParameterInfo; + var mi = methodInformation.MethodBase; int val = mi.IsStatic ? 3000 : 0; int num = pi.Length; val += mi.IsGenericMethod ? 1 : 0; for (var i = 0; i < num; i++) { - val += ArgPrecedence(pi[i].ParameterType); + val += ArgPrecedence(pi[i].ParameterType, methodInformation); + } + + var info = mi as MethodInfo; + if (info != null) + { + val += ArgPrecedence(info.ReturnType, methodInformation); + val += mi.DeclaringType == mi.ReflectedType ? 0 : 3000; } return val; @@ -222,7 +216,7 @@ internal static int GetPrecedence(MethodBase mi) /// /// Return a precedence value for a particular Type object. /// - internal static int ArgPrecedence(Type t) + internal static int ArgPrecedence(Type t, MethodInformation mi) { Type objectType = typeof(object); if (t == objectType) @@ -230,14 +224,9 @@ internal static int ArgPrecedence(Type t) return 3000; } - if (t.IsArray) + if (t.IsAssignableFrom(typeof(PyObject)) && !OperatorMethod.IsOperatorMethod(mi.MethodBase)) { - Type e = t.GetElementType(); - if (e == objectType) - { - return 2500; - } - return 100 + ArgPrecedence(e); + return -1; } TypeCode tc = Type.GetTypeCode(t); @@ -287,129 +276,58 @@ internal static int ArgPrecedence(Type t) return 40; } + if (t.IsArray) + { + Type e = t.GetElementType(); + if (e == objectType) + { + return 2500; + } + return 100 + ArgPrecedence(e, mi); + } + return 2000; } /// /// Bind the given Python instance and arguments to a particular method - /// overload in and return a structure that contains the converted Python + /// overload and return a structure that contains the converted Python /// instance, converted arguments and the correct method to call. - /// If unsuccessful, may set a Python error. /// - /// The Python target of the method invocation. - /// The Python arguments. - /// The Python keyword arguments. - /// A Binding if successful. Otherwise null. internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw) { return Bind(inst, args, kw, null, null); } - /// - /// Bind the given Python instance and arguments to a particular method - /// overload in and return a structure that contains the converted Python - /// instance, converted arguments and the correct method to call. - /// If unsuccessful, may set a Python error. - /// - /// The Python target of the method invocation. - /// The Python arguments. - /// The Python keyword arguments. - /// If not null, only bind to that method. - /// A Binding if successful. Otherwise null. internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info) { return Bind(inst, args, kw, info, null); } - private readonly struct MatchedMethod - { - public MatchedMethod(int kwargsMatched, int defaultsNeeded, object[] margs, int outs, MethodBase mb) - { - KwargsMatched = kwargsMatched; - DefaultsNeeded = defaultsNeeded; - ManagedArgs = margs; - Outs = outs; - Method = mb; - } - - public int KwargsMatched { get; } - public int DefaultsNeeded { get; } - public object[] ManagedArgs { get; } - public int Outs { get; } - public MethodBase Method { get; } - } - - private readonly struct MismatchedMethod - { - public MismatchedMethod(PythonException exception, MethodBase mb) - { - Exception = exception; - Method = mb; - } - - public PythonException Exception { get; } - public MethodBase Method { get; } - } - - /// - /// Bind the given Python instance and arguments to a particular method - /// overload in and return a structure that contains the converted Python - /// instance, converted arguments and the correct method to call. - /// If unsuccessful, may set a Python error. - /// - /// The Python target of the method invocation. - /// The Python arguments. - /// 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. - /// A Binding if successful. Otherwise null. internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, MethodInfo[] methodinfo) { - // loop to find match, return invoker w/ or w/o error - MethodBase[] _methods = null; - - var kwargDict = new Dictionary(); - if (kw != IntPtr.Zero) - { - var pynkwargs = (int)Runtime.PyDict_Size(kw); - IntPtr keylist = Runtime.PyDict_Keys(kw); - IntPtr valueList = Runtime.PyDict_Values(kw); - for (int i = 0; i < pynkwargs; ++i) - { - var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(new BorrowedReference(keylist), i)); - kwargDict[keyStr] = Runtime.PyList_GetItem(new BorrowedReference(valueList), i).DangerousGetAddress(); - } - Runtime.XDecref(keylist); - Runtime.XDecref(valueList); - } - + // loop to find match, return invoker w/ or /wo error var pynargs = (int)Runtime.PyTuple_Size(args); + object arg; var isGeneric = false; - if (info != null) - { - _methods = new MethodBase[1]; - _methods.SetValue(info, 0); - } - else - { - _methods = GetMethods(); - } + ArrayList defaultArgList; + Type clrtype; + Binding bindingUsingImplicitConversion = null; - var argMatchedMethods = new List(_methods.Length); - var mismatchedMethods = new List(); + var methods = info == null ? GetMethods() + : new List(1) { new MethodInformation(info, info.GetParameters()) }; // TODO: Clean up - foreach (MethodBase mi in _methods) + foreach (var methodInformation in methods) { + var mi = methodInformation.MethodBase; + var pi = methodInformation.ParameterInfo; + if (mi.IsGenericMethod) { isGeneric = true; } - ParameterInfo[] pi = mi.GetParameters(); - ArrayList defaultArgList; - bool paramsArray; - int kwargsMatched; - int defaultsNeeded; + bool isOperator = OperatorMethod.IsOperatorMethod(mi); // 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. @@ -417,436 +335,289 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // 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 paramsArray, out defaultArgList, out kwargsMatched, out defaultsNeeded) && !isOperator) - { - 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(); } - int outs; - var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList, - needsResolution: _methods.Length > 1, // If there's more than one possible match. - outs: out outs); - if (margs == null) - { - mismatchedMethods.Add(new MismatchedMethod(new PythonException(), mi)); - Exceptions.Clear(); - continue; - } - if (isOperator) + + int clrnargs = pi.Length; + int arrayStart; + + if (CheckMethodArgumentsMatch(clrnargs, + pynargs, + pi, + out arrayStart, + out defaultArgList)) { - if (inst != IntPtr.Zero) + var outs = 0; + var margs = new object[clrnargs]; + var usedImplicitConversion = false; + + for (int n = 0; n < clrnargs; n++) { - if (ManagedType.GetManagedObject(inst) is CLRObject co) + IntPtr op; + if (n < pynargs) { - bool isUnary = pynargs == 0; - // Postprocessing to extend margs. - var margsTemp = isUnary ? new object[1] : new object[2]; - // If reverse, the bound instance is the right operand. - int boundOperandIndex = isReverse ? 1 : 0; - // If reverse, the passed instance is the left operand. - int passedOperandIndex = isReverse ? 0 : 1; - margsTemp[boundOperandIndex] = co.inst; - if (!isUnary) + if (arrayStart == n) { - margsTemp[passedOperandIndex] = margs[0]; + // map remaining Python arguments to a tuple since + // the managed function accepts it - hopefully :] + op = Runtime.PyTuple_GetSlice(args, arrayStart, pynargs); + } + else + { + op = Runtime.PyTuple_GetItem(args, n); } - margs = margsTemp; - } - else continue; - } - } + // this logic below handles cases when multiple overloading methods + // are ambiguous, hence comparison between Python and CLR types + // is necessary + clrtype = null; + IntPtr pyoptype; + if (methods.Count > 1) + { + pyoptype = IntPtr.Zero; + pyoptype = Runtime.PyObject_Type(op); + Exceptions.Clear(); + if (pyoptype != IntPtr.Zero) + { + clrtype = Converter.GetTypeByAlias(pyoptype); + } + Runtime.XDecref(pyoptype); + } - var matchedMethod = new MatchedMethod(kwargsMatched, defaultsNeeded, margs, outs, mi); - argMatchedMethods.Add(matchedMethod); - } - if (argMatchedMethods.Count > 0) - { - var bestKwargMatchCount = argMatchedMethods.Max(x => x.KwargsMatched); - var fewestDefaultsRequired = argMatchedMethods.Where(x => x.KwargsMatched == bestKwargMatchCount).Min(x => x.DefaultsNeeded); - int bestCount = 0; - int bestMatchIndex = -1; + if (clrtype != null) + { + var typematch = false; + if ((pi[n].ParameterType != typeof(object)) && (pi[n].ParameterType != clrtype)) + { + IntPtr pytype = Converter.GetPythonTypeByAlias(pi[n].ParameterType); + pyoptype = Runtime.PyObject_Type(op); + Exceptions.Clear(); + if (pyoptype != IntPtr.Zero) + { + if (pytype != pyoptype) + { + typematch = false; + } + else + { + typematch = true; + clrtype = pi[n].ParameterType; + } + } + if (!typematch) + { + // this takes care of nullables + var underlyingType = Nullable.GetUnderlyingType(pi[n].ParameterType); + if (underlyingType == null) + { + underlyingType = pi[n].ParameterType; + } + // this takes care of enum values + TypeCode argtypecode = Type.GetTypeCode(underlyingType); + TypeCode paramtypecode = Type.GetTypeCode(clrtype); + if (argtypecode == paramtypecode) + { + typematch = true; + clrtype = pi[n].ParameterType; + } + // accepts non-decimal numbers in decimal parameters + if (underlyingType == typeof(decimal)) + { + clrtype = pi[n].ParameterType; + typematch = Converter.ToManaged(op, clrtype, out arg, false); + } + // this takes care of implicit conversions + var opImplicit = pi[n].ParameterType.GetMethod("op_Implicit", new[] { clrtype }); + if (opImplicit != null) + { + usedImplicitConversion = typematch = opImplicit.ReturnType == pi[n].ParameterType; + clrtype = pi[n].ParameterType; + } + } + Runtime.XDecref(pyoptype); + if (!typematch) + { + margs = null; + break; + } + } + else + { + typematch = true; + clrtype = pi[n].ParameterType; + } + } + else + { + clrtype = pi[n].ParameterType; + } - for (int index = 0; index < argMatchedMethods.Count; index++) - { - var testMatch = argMatchedMethods[index]; - if (testMatch.DefaultsNeeded == fewestDefaultsRequired && testMatch.KwargsMatched == bestKwargMatchCount) - { - bestCount++; - if (bestMatchIndex == -1) - bestMatchIndex = index; + if (pi[n].IsOut || clrtype.IsByRef) + { + outs++; + } + + if (!Converter.ToManaged(op, clrtype, out arg, false)) + { + Exceptions.Clear(); + margs = null; + break; + } + if (arrayStart == n) + { + // GetSlice() creates a new reference but GetItem() + // returns only a borrow reference. + Runtime.XDecref(op); + } + margs[n] = arg; + } + else + { + if (defaultArgList != null) + { + margs[n] = defaultArgList[n - pynargs]; + } + } } - } - if (bestCount > 1 && fewestDefaultsRequired > 0) - { - // Best effort for determining method to match on gives multiple possible - // matches and we need at least one default argument - bail from this point - StringBuilder stringBuilder = new StringBuilder("Not enough arguments provided to disambiguate the method. Found:"); - foreach (var matchedMethod in argMatchedMethods) + if (margs == null) { - stringBuilder.AppendLine(); - stringBuilder.Append(matchedMethod.Method.ToString()); + continue; } - Exceptions.SetError(Exceptions.TypeError, stringBuilder.ToString()); - return null; - } - // If we're here either: - // (a) There is only one best match - // (b) There are multiple best matches but none of them require - // default arguments - // in the case of (a) we're done by default. For (b) regardless of which - // method we choose, all arguments are specified _and_ can be converted - // from python to C# so picking any will suffice - MatchedMethod bestMatch = argMatchedMethods[bestMatchIndex]; - var margs = bestMatch.ManagedArgs; - var outs = bestMatch.Outs; - var mi = bestMatch.Method; - - object target = null; - if (!mi.IsStatic && inst != IntPtr.Zero) - { - //CLRObject co = (CLRObject)ManagedType.GetManagedObject(inst); - // InvalidCastException: Unable to cast object of type - // 'Python.Runtime.ClassObject' to type 'Python.Runtime.CLRObject' - var co = ManagedType.GetManagedObject(inst) as CLRObject; - - // Sanity check: this ensures a graceful exit if someone does - // something intentionally wrong like call a non-static method - // on the class rather than on an instance of the class. - // XXX maybe better to do this before all the other rigmarole. - if (co == null) + if (isOperator) { - Exceptions.SetError(Exceptions.TypeError, "Invoked a non-static method with an invalid instance"); - return null; + if (inst != IntPtr.Zero) + { + if (ManagedType.GetManagedObject(inst) is CLRObject co) + { + bool isUnary = pynargs == 0; + // Postprocessing to extend margs. + var margsTemp = isUnary ? new object[1] : new object[2]; + // If reverse, the bound instance is the right operand. + int boundOperandIndex = isReverse ? 1 : 0; + // If reverse, the passed instance is the left operand. + int passedOperandIndex = isReverse ? 0 : 1; + margsTemp[boundOperandIndex] = co.inst; + if (!isUnary) + { + margsTemp[passedOperandIndex] = margs[0]; + } + margs = margsTemp; + } + else continue; + } } - target = co.inst; - } - - return new Binding(mi, target, margs, outs); - } - else if (isGeneric && info == null && methodinfo != null) - { - // We weren't able to find a matching method but at least one - // is a generic method and info is null. That happens when a generic - // method was not called using the [] syntax. Let's introspect the - // type of the arguments and use it to construct the correct method. - Type[] types = Runtime.PythonArgsToTypeArray(args, true); - MethodInfo mi = MatchParameters(methodinfo, types); - if (mi != null) - { - return Bind(inst, args, kw, mi, null); - } - } - if (mismatchedMethods.Count > 0) - { - var aggregateException = GetAggregateException(mismatchedMethods); - Exceptions.SetError(aggregateException); - } - return null; - } - static AggregateException GetAggregateException(IEnumerable mismatchedMethods) - { - return new AggregateException(mismatchedMethods.Select(m => new ArgumentException($"{m.Exception.Message} in method {m.Method}", m.Exception))); - } - - static IntPtr HandleParamsArray(IntPtr args, int arrayStart, int pyArgCount, out bool isNewReference) - { - isNewReference = false; - IntPtr op; - // for a params method, we may have a sequence or single/multiple items - // here we look to see if the item at the paramIndex is there or not - // and then if it is a sequence itself. - if ((pyArgCount - arrayStart) == 1) - { - // we only have one argument left, so we need to check it - // to see if it is a sequence or a single item - IntPtr item = Runtime.PyTuple_GetItem(args, arrayStart); - if (!Runtime.PyString_Check(item) && Runtime.PySequence_Check(item)) - { - // it's a sequence (and not a string), so we use it as the op - op = item; - } - else - { - isNewReference = true; - op = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - } - } - else - { - isNewReference = true; - op = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - } - return op; - } - - /// - /// Attempts to convert Python positional argument tuple and keyword argument table - /// into an array of managed objects, that can be passed to a method. - /// If unsuccessful, returns null and may set a Python error. - /// - /// Information about expected parameters - /// true, if the last parameter is a params array. - /// A pointer to the Python argument tuple - /// Number of arguments, passed by Python - /// Dictionary of keyword argument name to python object pointer - /// A list of default values for omitted parameters - /// true, if overloading resolution is required - /// Returns number of output parameters - /// If successful, an array of .NET arguments that can be passed to the method. Otherwise null. - static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray, - IntPtr args, int pyArgCount, - Dictionary kwargDict, - ArrayList defaultArgList, - bool needsResolution, - out int outs) - { - outs = 0; - var margs = new object[pi.Length]; - int arrayStart = paramsArray ? pi.Length - 1 : -1; - - for (int paramIndex = 0; paramIndex < pi.Length; paramIndex++) - { - var parameter = pi[paramIndex]; - bool hasNamedParam = parameter.Name != null ? kwargDict.ContainsKey(parameter.Name) : false; - bool isNewReference = false; - - if (paramIndex >= pyArgCount && !(hasNamedParam || (paramsArray && paramIndex == arrayStart))) - { - if (defaultArgList != null) + object target = null; + if (!mi.IsStatic && inst != IntPtr.Zero) { - margs[paramIndex] = defaultArgList[paramIndex - pyArgCount]; + //CLRObject co = (CLRObject)ManagedType.GetManagedObject(inst); + // InvalidCastException: Unable to cast object of type + // 'Python.Runtime.ClassObject' to type 'Python.Runtime.CLRObject' + var co = ManagedType.GetManagedObject(inst) as CLRObject; + + // Sanity check: this ensures a graceful exit if someone does + // something intentionally wrong like call a non-static method + // on the class rather than on an instance of the class. + // XXX maybe better to do this before all the other rigmarole. + if (co == null) + { + return null; + } + target = co.inst; } - continue; - } - - IntPtr op; - if (hasNamedParam) - { - op = kwargDict[parameter.Name]; - } - else - { - if(arrayStart == paramIndex) + var binding = new Binding(mi, target, margs, outs); + if (usedImplicitConversion) { - op = HandleParamsArray(args, arrayStart, pyArgCount, out isNewReference); + // lets just keep the first binding using implicit conversion + // this is to respect method order/precedence + if (bindingUsingImplicitConversion == null) + { + // in this case we will not return the binding yet in case there is a match + // which does not use implicit conversions, which will return directly + bindingUsingImplicitConversion = binding; + } } else { - op = Runtime.PyTuple_GetItem(args, paramIndex); + return binding; } } - - bool isOut; - if (!TryConvertArgument(op, parameter.ParameterType, needsResolution, out margs[paramIndex], out isOut)) - { - return null; - } - - if (isNewReference) - { - // TODO: is this a bug? Should this happen even if the conversion fails? - // GetSlice() creates a new reference but GetItem() - // returns only a borrow reference. - Runtime.XDecref(op); - } - - if (isOut) - { - outs++; - } } - return margs; - } - - /// - /// Try to convert a Python argument object to a managed CLR type. - /// If unsuccessful, may set a Python error. - /// - /// Pointer to the Python argument object. - /// That parameter's managed type. - /// If true, there are multiple overloading methods that need resolution. - /// Converted argument. - /// Whether the CLR type is passed by reference. - /// true on success - static bool TryConvertArgument(IntPtr op, Type parameterType, bool needsResolution, - out object arg, out bool isOut) - { - arg = null; - isOut = false; - var clrtype = TryComputeClrArgumentType(parameterType, op, needsResolution: needsResolution); - if (clrtype == null) + // if we generated a binding using implicit conversion return it + if (bindingUsingImplicitConversion != null) { - return false; + return bindingUsingImplicitConversion; } - if (!Converter.ToManaged(op, clrtype, out arg, true)) + // We weren't able to find a matching method but at least one + // is a generic method and info is null. That happens when a generic + // method was not called using the [] syntax. Let's introspect the + // type of the arguments and use it to construct the correct method. + if (isGeneric && info == null && methodinfo != null) { - return false; + Type[] types = Runtime.PythonArgsToTypeArray(args, true); + MethodInfo mi = MatchParameters(methodinfo, types); + return Bind(inst, args, kw, mi, null); } - - isOut = clrtype.IsByRef; - return true; + return null; } /// - /// Determine the managed type that a Python argument object needs to be converted into. + /// This helper method will perform an initial check to determine if we found a matching + /// method based on its parameters count and type /// - /// The parameter's managed type. - /// Pointer to the Python argument object. - /// If true, there are multiple overloading methods that need resolution. - /// null if conversion is not possible - static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool needsResolution) - { - // this logic below handles cases when multiple overloading methods - // are ambiguous, hence comparison between Python and CLR types - // is necessary - Type clrtype = null; - IntPtr pyoptype; - if (needsResolution) - { - // HACK: each overload should be weighted in some way instead - pyoptype = Runtime.PyObject_Type(argument); - if (pyoptype != IntPtr.Zero) - { - clrtype = Converter.GetTypeByAlias(pyoptype); - } - Runtime.XDecref(pyoptype); - } - - if (clrtype != null) - { - if ((parameterType != typeof(object)) && (parameterType != clrtype)) - { - IntPtr pytype = Converter.GetPythonTypeByAlias(parameterType); - pyoptype = Runtime.PyObject_Type(argument); - var typematch = false; - if (pyoptype != IntPtr.Zero) - { - if (pytype != pyoptype) - { - typematch = false; - } - else - { - typematch = true; - clrtype = parameterType; - } - } - if (!typematch) - { - // this takes care of enum values - TypeCode parameterTypeCode = Type.GetTypeCode(parameterType); - TypeCode clrTypeCode = Type.GetTypeCode(clrtype); - if (parameterTypeCode == clrTypeCode) - { - typematch = true; - clrtype = parameterType; - } - else - { - Exceptions.RaiseTypeError($"Expected {parameterTypeCode}, got {clrTypeCode}"); - } - } - Runtime.XDecref(pyoptype); - if (!typematch) - { - return null; - } - } - else - { - clrtype = parameterType; - } - } - else - { - clrtype = parameterType; - } - - return clrtype; - } - /// - /// Check whether the number of Python and .NET arguments match, and compute additional arg information. - /// - /// Number of positional args passed from Python. - /// Parameters of the specified .NET method. - /// Keyword args passed from Python. - /// True if the final param of the .NET method is an array (`params` keyword). - /// List of default values for arguments. - /// Number of kwargs from Python that are also present in the .NET method. - /// Number of non-null defaultsArgs. - /// - static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters, - Dictionary kwargDict, - out bool paramsArray, - out ArrayList defaultArgList, - out int kwargsMatched, - out int defaultsNeeded) + private bool CheckMethodArgumentsMatch(int clrnargs, + nint pynargs, + ParameterInfo[] parameterInfo, + out int arrayStart, + out ArrayList defaultArgList) { + arrayStart = -1; defaultArgList = null; + var match = false; - paramsArray = parameters.Length > 0 ? Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute)) : false; - kwargsMatched = 0; - defaultsNeeded = 0; - if (positionalArgumentCount == parameters.Length && kwargDict.Count == 0) + if (pynargs == clrnargs) { match = true; } - else if (positionalArgumentCount < parameters.Length && (!paramsArray || positionalArgumentCount == parameters.Length - 1)) + else if (pynargs < clrnargs) { match = true; - // every parameter past 'positionalArgumentCount' must have either - // a corresponding keyword arg or a default param, unless the method - // method accepts a params array (which cannot have a default value) defaultArgList = new ArrayList(); - for (var v = positionalArgumentCount; v < parameters.Length; v++) + for (var v = pynargs; v < clrnargs && match; v++) { - if (kwargDict.ContainsKey(parameters[v].Name)) + if (parameterInfo[v].DefaultValue == DBNull.Value) { - // we have a keyword argument for this parameter, - // no need to check for a default parameter, but put a null - // placeholder in defaultArgList - defaultArgList.Add(null); - kwargsMatched++; - } - else if (parameters[v].IsOptional) - { - // IsOptional will be true if the parameter has a default value, - // or if the parameter has the [Optional] attribute specified. - // The GetDefaultValue() extension method will return the value - // to be passed in as the parameter value - defaultArgList.Add(parameters[v].GetDefaultValue()); - defaultsNeeded++; + match = false; } - else if (!paramsArray) + else { - match = false; + defaultArgList.Add(parameterInfo[v].DefaultValue); } } } - else if (positionalArgumentCount > parameters.Length && parameters.Length > 0 && - Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute))) + else if (pynargs > clrnargs && clrnargs > 0 && + Attribute.IsDefined(parameterInfo[clrnargs - 1], typeof(ParamArrayAttribute))) { // This is a `foo(params object[] bar)` style method match = true; - paramsArray = true; + arrayStart = clrnargs - 1; } return match; @@ -862,53 +633,8 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i return Invoke(inst, args, kw, info, null); } - protected static void AppendArgumentTypes(StringBuilder to, IntPtr 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) - { - var type = Runtime.PyObject_Type(arg); - if (type != IntPtr.Zero) - { - try - { - var description = Runtime.PyObject_Unicode(type); - if (description != IntPtr.Zero) - { - to.Append(Runtime.GetManagedString(description)); - Runtime.XDecref(description); - } - } - finally - { - Runtime.XDecref(type); - } - } - } - - if (argIndex + 1 < argCount) - to.Append(", "); - } - to.Append(')'); - } - internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, MethodInfo[] methodinfo) { - // No valid methods, nothing to bind. - if (GetMethods().Length == 0) - { - var msg = new StringBuilder("The underlying C# method(s) have been deleted"); - if (list.Count > 0 && list[0].Name != null) - { - msg.Append($": {list[0]}"); - } - return Exceptions.RaiseTypeError(msg.ToString()); - } - Binding binding = Bind(inst, args, kw, info, methodinfo); object result; IntPtr ts = IntPtr.Zero; @@ -920,9 +646,9 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i { value.Append($" for {methodinfo[0].Name}"); } - else if (list.Count > 0 && list[0].Valid) + else if (list.Count > 0) { - value.Append($" for {list[0].Value.Name}"); + value.Append($" for {list[0].MethodBase.Name}"); } value.Append(": "); @@ -960,7 +686,7 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i } // If there are out parameters, we return a tuple containing - // the result, if any, followed by the out parameters. If there is only + // 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). @@ -973,22 +699,17 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i int c = pi.Length; var n = 0; - bool isVoid = mi.ReturnType == typeof(void); - int tupleSize = binding.outs + (isVoid ? 0 : 1); - IntPtr t = Runtime.PyTuple_New(tupleSize); - if (!isVoid) - { - IntPtr v = Converter.ToPython(result, mi.ReturnType); - Runtime.PyTuple_SetItem(t, n, v); - n++; - } + IntPtr t = Runtime.PyTuple_New(binding.outs + 1); + IntPtr v = Converter.ToPython(result, mi.ReturnType); + Runtime.PyTuple_SetItem(t, n, v); + n++; for (var i = 0; i < c; i++) { Type pt = pi[i].ParameterType; - if (pt.IsByRef) + if (pi[i].IsOut || pt.IsByRef) { - IntPtr v = Converter.ToPython(binding.args[i], pt.GetElementType()); + v = Converter.ToPython(binding.args[i], pt); Runtime.PyTuple_SetItem(t, n, v); n++; } @@ -996,7 +717,7 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i if (binding.outs == 1 && mi.ReturnType == typeof(void)) { - IntPtr v = Runtime.PyTuple_GetItem(t, 0); + v = Runtime.PyTuple_GetItem(t, 1); Runtime.XIncref(v); Runtime.XDecref(t); return v; @@ -1007,53 +728,81 @@ internal virtual IntPtr Invoke(IntPtr inst, IntPtr args, IntPtr kw, MethodBase i return Converter.ToPython(result, mi.ReturnType); } - } - - /// - /// Utility class to sort method info by parameter type precedence. - /// - internal class MethodSorter : IComparer - { - int IComparer.Compare(MaybeMethodBase m1, MaybeMethodBase m2) + /// + /// Utility class to store the information about a + /// + [Serializable] + internal class MethodInformation { - MethodBase me1 = m1.UnsafeValue; - MethodBase me2 = m2.UnsafeValue; - if (me1 == null && me2 == null) - { - return 0; - } - else if (me1 == null) - { - return -1; - } - else if (me2 == null) + public MethodBase MethodBase { get; } + + public ParameterInfo[] ParameterInfo { get; } + + public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo) { - return 1; + MethodBase = methodBase; + ParameterInfo = parameterInfo; } - if (me1.DeclaringType != me2.DeclaringType) + public override string ToString() { - // m2's type derives from m1's type, favor m2 - if (me1.DeclaringType.IsAssignableFrom(me2.DeclaringType)) - return 1; - - // m1's type derives from m2's type, favor m1 - if (me2.DeclaringType.IsAssignableFrom(me1.DeclaringType)) - return -1; + return MethodBase.ToString(); } + } - int p1 = MethodBinder.GetPrecedence(me1); - int p2 = MethodBinder.GetPrecedence(me2); - if (p1 < p2) + /// + /// Utility class to sort method info by parameter type precedence. + /// + private class MethodSorter : IComparer + { + public int Compare(MethodInformation x, MethodInformation y) { - return -1; + int p1 = GetPrecedence(x); + int p2 = GetPrecedence(y); + if (p1 < p2) + { + return -1; + } + if (p1 > p2) + { + return 1; + } + return 0; } - if (p1 > p2) + } + protected static void AppendArgumentTypes(StringBuilder to, IntPtr args) + { + long argCount = Runtime.PyTuple_Size(args); + to.Append("("); + for (long argIndex = 0; argIndex < argCount; argIndex++) { - return 1; + var arg = Runtime.PyTuple_GetItem(args, argIndex); + if (arg != IntPtr.Zero) + { + var type = Runtime.PyObject_Type(arg); + if (type != IntPtr.Zero) + { + try + { + var description = Runtime.PyObject_Unicode(type); + if (description != IntPtr.Zero) + { + to.Append(Runtime.GetManagedString(description)); + Runtime.XDecref(description); + } + } + finally + { + Runtime.XDecref(type); + } + } + } + + if (argIndex + 1 < argCount) + to.Append(", "); } - return 0; + to.Append(')'); } } @@ -1078,33 +827,4 @@ internal Binding(MethodBase info, object inst, object[] args, int outs) this.outs = outs; } } - - - static internal class ParameterInfoExtensions - { - public static object GetDefaultValue(this ParameterInfo parameterInfo) - { - // parameterInfo.HasDefaultValue is preferable but doesn't exist in .NET 4.0 - bool hasDefaultValue = (parameterInfo.Attributes & ParameterAttributes.HasDefault) == - ParameterAttributes.HasDefault; - - if (hasDefaultValue) - { - return parameterInfo.DefaultValue; - } - else - { - // [OptionalAttribute] was specified for the parameter. - // See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value - // for rules on determining the value to pass to the parameter - var type = parameterInfo.ParameterType; - if (type == typeof(object)) - return Type.Missing; - else if (type.IsValueType) - return Activator.CreateInstance(type); - else - return null; - } - } - } } diff --git a/src/runtime/methodobject.cs b/src/runtime/methodobject.cs index 37c01f5c5..66ee65293 100644 --- a/src/runtime/methodobject.cs +++ b/src/runtime/methodobject.cs @@ -79,14 +79,14 @@ internal IntPtr GetDocString() } var str = ""; Type marker = typeof(DocStringAttribute); - MethodBase[] methods = binder.GetMethods(); - foreach (MethodBase method in methods) + var methods = binder.GetMethods(); + foreach (var method in methods) { if (str.Length > 0) { str += Environment.NewLine; } - var attrs = (Attribute[])method.GetCustomAttributes(marker, false); + var attrs = (Attribute[])method.MethodBase.GetCustomAttributes(marker, false); if (attrs.Length == 0) { str += method.ToString(); diff --git a/src/runtime/pythonengine.cs b/src/runtime/pythonengine.cs index 35ea3f6d2..c08ff863e 100644 --- a/src/runtime/pythonengine.cs +++ b/src/runtime/pythonengine.cs @@ -182,6 +182,7 @@ public static void Initialize(IEnumerable args, bool setSysArgv = true, // during an initial "import clr", and the world ends shortly thereafter. // This is probably masking some bad mojo happening somewhere in Runtime.Initialize(). delegateManager = new DelegateManager(); + Console.WriteLine("PythonEngine.Initialize(): Runtime.Initialize()..."); Runtime.Initialize(initSigs, mode); initialized = true; Exceptions.Clear(); @@ -199,6 +200,7 @@ public static void Initialize(IEnumerable args, bool setSysArgv = true, } // Load the clr.py resource into the clr module + Console.WriteLine("PythonEngine.Initialize(): GetCLRModule()..."); NewReference clr = Python.Runtime.ImportHook.GetCLRModule(); BorrowedReference clr_dict = Runtime.PyModule_GetDict(clr); @@ -210,6 +212,7 @@ public static void Initialize(IEnumerable args, bool setSysArgv = true, BorrowedReference builtins = Runtime.PyEval_GetBuiltins(); Runtime.PyDict_SetItemString(module_globals, "__builtins__", builtins); + Console.WriteLine("PythonEngine.Initialize(): clr GetManifestResourceStream..."); Assembly assembly = Assembly.GetExecutingAssembly(); using (Stream stream = assembly.GetManifestResourceStream("clr.py")) using (var reader = new StreamReader(stream)) diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index ec7f5e446..068fb1c6a 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Collections.Generic; +using System.IO; using Python.Runtime.Native; using Python.Runtime.Platform; using System.Linq; @@ -116,9 +117,11 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd if (Py_IsInitialized() == 0) { + Console.WriteLine("Runtime.Initialize(): Py_Initialize..."); Py_InitializeEx(initSigs ? 1 : 0); if (PyEval_ThreadsInitialized() == 0) { + Console.WriteLine("Runtime.Initialize(): PyEval_InitThreads..."); PyEval_InitThreads(); } // XXX: Reload mode may reduct to Soft mode, @@ -143,7 +146,9 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd IsFinalizing = false; InternString.Initialize(); + Console.WriteLine("Runtime.Initialize(): Initialize types..."); InitPyMembers(); + Console.WriteLine("Runtime.Initialize(): Initialize types end."); ABI.Initialize(PyVersion, pyType: new BorrowedReference(PyTypeType)); @@ -155,6 +160,7 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd TypeManager.Initialize(); // Initialize modules that depend on the runtime class. + Console.WriteLine("Runtime.Initialize(): AssemblyManager.Initialize()..."); AssemblyManager.Initialize(); OperatorMethod.Initialize(); if (mode == ShutdownMode.Reload && RuntimeData.HasStashData()) @@ -170,15 +176,28 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd // Need to add the runtime directory to sys.path so that we // can find built-in assemblies like System.Data, et. al. - string rtdir = RuntimeEnvironment.GetRuntimeDirectory(); + AddToPyPath(RuntimeEnvironment.GetRuntimeDirectory()); + AddToPyPath(Directory.GetCurrentDirectory()); + + Console.WriteLine("Runtime.Initialize(): AssemblyManager.UpdatePath()..."); + AssemblyManager.UpdatePath(); + } + + private static void AddToPyPath(string directory) + { + if (!Directory.Exists(directory)) + { + return; + } + IntPtr path = PySys_GetObject("path").DangerousGetAddress(); - IntPtr item = PyString_FromString(rtdir); + IntPtr item = PyString_FromString(directory); if (PySequence_Contains(path, item) == 0) { PyList_Append(new BorrowedReference(path), item); } + XDecref(item); - AssemblyManager.UpdatePath(); } private static void InitPyMembers() @@ -271,6 +290,14 @@ private static void InitPyMembers() () => PyFloatType = IntPtr.Zero); XDecref(op); + IntPtr decimalMod = PyImport_ImportModule("_pydecimal"); + IntPtr decimalCtor = PyObject_GetAttrString(decimalMod, "Decimal"); + op = PyObject_CallObject(decimalCtor, IntPtr.Zero); + PyDecimalType = PyObject_Type(op); + XDecref(op); + XDecref(decimalMod); + XDecref(decimalCtor); + PyClassType = IntPtr.Zero; PyInstanceType = IntPtr.Zero; @@ -551,6 +578,7 @@ private static void MoveClrInstancesOnwershipToPython() internal static IntPtr PyBoolType; internal static IntPtr PyNoneType; internal static IntPtr PyTypeType; + internal static IntPtr PyDecimalType; internal static IntPtr Py_NoSiteFlag; diff --git a/src/runtime/typemanager.cs b/src/runtime/typemanager.cs index e0564b243..7f5351ed6 100644 --- a/src/runtime/typemanager.cs +++ b/src/runtime/typemanager.cs @@ -251,8 +251,10 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType) InitializeSlot(type, TypeOffset.mp_length, mp_length_slot.Method, slotsHolder); } - if (!typeof(IEnumerable).IsAssignableFrom(clrType) && - !typeof(IEnumerator).IsAssignableFrom(clrType)) + // we want to do this after the slot stuff above in case the class itself implements a slot method + InitializeSlots(type, impl.GetType()); + + if (!clrType.GetInterfaces().Any(ifc => ifc == typeof(IEnumerable) || ifc == typeof(IEnumerator))) { // The tp_iter slot should only be set for enumerable types. Marshal.WriteIntPtr(type, TypeOffset.tp_iter, IntPtr.Zero); diff --git a/src/testing/conversiontest.cs b/src/testing/conversiontest.cs index 1f9d64e1b..58ee61a20 100644 --- a/src/testing/conversiontest.cs +++ b/src/testing/conversiontest.cs @@ -1,3 +1,5 @@ +using System; + namespace Python.Test { using System.Collections.Generic; @@ -31,6 +33,8 @@ public ConversionTest() public ShortEnum EnumField; public object ObjectField = null; public ISpam SpamField; + public DateTime DateTimeField; + public TimeSpan TimeSpanField; public byte[] ByteArrayField; public sbyte[] SByteArrayField; diff --git a/src/testing/dictionarytest.cs b/src/testing/dictionarytest.cs new file mode 100644 index 000000000..a7fa3497d --- /dev/null +++ b/src/testing/dictionarytest.cs @@ -0,0 +1,106 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Python.Test +{ + /// + /// Supports units tests for dictionary __contains__ and __len__ + /// + public class PublicDictionaryTest + { + public IDictionary items; + + public PublicDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + + public class ProtectedDictionaryTest + { + protected IDictionary items; + + public ProtectedDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + + public class InternalDictionaryTest + { + internal IDictionary items; + + public InternalDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + + public class PrivateDictionaryTest + { + private IDictionary items; + + public PrivateDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + public class InheritedDictionaryTest : IDictionary + { + private readonly IDictionary items; + + public InheritedDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + + public int this[string key] + { + get { return items[key]; } + set { items[key] = value; } + } + + public ICollection Keys => items.Keys; + + public ICollection Values => items.Values; + + public int Count => items.Count; + + public bool IsReadOnly => false; + + public void Add(string key, int value) => items.Add(key, value); + + public void Add(KeyValuePair item) => items.Add(item); + + public void Clear() => items.Clear(); + + public bool Contains(KeyValuePair item) => items.Contains(item); + + public bool ContainsKey(string key) => items.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + items.CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() => items.GetEnumerator(); + + public bool Remove(string key) => items.Remove(key); + + public bool Remove(KeyValuePair item) => items.Remove(item); + + public bool TryGetValue(string key, out int value) => items.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/testing/interfacetest.cs b/src/testing/interfacetest.cs index 0158d64da..2c24596bc 100644 --- a/src/testing/interfacetest.cs +++ b/src/testing/interfacetest.cs @@ -11,6 +11,7 @@ internal interface IInternalInterface { } + public interface ISayHello1 { string SayHello(); @@ -42,27 +43,6 @@ string ISayHello2.SayHello() return "hello 2"; } - public ISayHello1 GetISayHello1() - { - return this; - } - - public void GetISayHello2(out ISayHello2 hello2) - { - hello2 = this; - } - - public ISayHello1 GetNoSayHello(out ISayHello2 hello2) - { - hello2 = null; - return null; - } - - public ISayHello1 [] GetISayHello1Array() - { - return new[] { this }; - } - public interface IPublic { } diff --git a/src/testing/subclasstest.cs b/src/testing/subclasstest.cs index ab0b73368..9817d865e 100644 --- a/src/testing/subclasstest.cs +++ b/src/testing/subclasstest.cs @@ -89,24 +89,13 @@ public static string test_bar(IInterfaceTest x, string s, int i) } // test instances can be constructed in managed code - public static SubClassTest create_instance(Type t) - { - return (SubClassTest)t.GetConstructor(new Type[] { }).Invoke(new object[] { }); - } - - public static IInterfaceTest create_instance_interface(Type t) + public static IInterfaceTest create_instance(Type t) { return (IInterfaceTest)t.GetConstructor(new Type[] { }).Invoke(new object[] { }); } - // test instances pass through managed code unchanged ... - public static SubClassTest pass_through(SubClassTest s) - { - return s; - } - - // ... but the return type is an interface type, objects get wrapped - public static IInterfaceTest pass_through_interface(IInterfaceTest s) + // test instances pass through managed code unchanged + public static IInterfaceTest pass_through(IInterfaceTest s) { return s; } diff --git a/src/tests/test_dictionary.py b/src/tests/test_dictionary.py new file mode 100644 index 000000000..1532c9b15 --- /dev/null +++ b/src/tests/test_dictionary.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +"""Test support for managed dictionaries.""" + +import Python.Test as Test +import System +import pytest + + +def test_public_dict(): + """Test public dict.""" + ob = Test.PublicDictionaryTest() + items = ob.items + + assert len(items) == 5 + + assert items['0'] == 0 + assert items['4'] == 4 + + items['0'] = 8 + assert items['0'] == 8 + + items['4'] = 9 + assert items['4'] == 9 + + items['-4'] = 0 + assert items['-4'] == 0 + + items['-1'] = 4 + assert items['-1'] == 4 + +def test_protected_dict(): + """Test protected dict.""" + ob = Test.ProtectedDictionaryTest() + items = ob.items + + assert len(items) == 5 + + assert items['0'] == 0 + assert items['4'] == 4 + + items['0'] = 8 + assert items['0'] == 8 + + items['4'] = 9 + assert items['4'] == 9 + + items['-4'] = 0 + assert items['-4'] == 0 + + items['-1'] = 4 + assert items['-1'] == 4 + +def test_internal_dict(): + """Test internal dict.""" + + with pytest.raises(AttributeError): + ob = Test.InternalDictionaryTest() + _ = ob.items + +def test_private_dict(): + """Test private dict.""" + + with pytest.raises(AttributeError): + ob = Test.PrivateDictionaryTest() + _ = ob.items + +def test_dict_contains(): + """Test dict support for __contains__.""" + + ob = Test.PublicDictionaryTest() + keys = ob.items.Keys + + assert '0' in keys + assert '1' in keys + assert '2' in keys + assert '3' in keys + assert '4' in keys + + assert not ('5' in keys) + assert not ('-1' in keys) + +def test_dict_abuse(): + """Test dict abuse.""" + _class = Test.PublicDictionaryTest + ob = Test.PublicDictionaryTest() + + with pytest.raises(AttributeError): + del _class.__getitem__ + + with pytest.raises(AttributeError): + del ob.__getitem__ + + with pytest.raises(AttributeError): + del _class.__setitem__ + + with pytest.raises(AttributeError): + del ob.__setitem__ + + with pytest.raises(TypeError): + Test.PublicArrayTest.__getitem__(0, 0) + +def test_InheritedDictionary(): + """Test class that inherited from IDictionary.""" + items = Test.InheritedDictionaryTest() + + assert len(items) == 5 + + assert items['0'] == 0 + assert items['4'] == 4 + + items['0'] = 8 + assert items['0'] == 8 + + items['4'] = 9 + assert items['4'] == 9 + + items['-4'] = 0 + assert items['-4'] == 0 + + items['-1'] = 4 + assert items['-1'] == 4 + +def test_InheritedDictionary_contains(): + """Test dict support for __contains__ in class that inherited from IDictionary""" + items = Test.InheritedDictionaryTest() + + assert '0' in items + assert '1' in items + assert '2' in items + assert '3' in items + assert '4' in items + + assert not ('5' in items) + assert not ('-1' in items) diff --git a/tests/test_array.py b/tests/test_array.py index 2b1a289ad..cc154394b 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -1334,10 +1334,9 @@ def test_special_array_creation(): assert value[1].__class__ == inst.__class__ assert value.Length == 2 - iface_class = ISayHello1(inst).__class__ value = Array[ISayHello1]([inst, inst]) - assert value[0].__class__ == iface_class - assert value[1].__class__ == iface_class + assert value[0].__class__ == inst.__class__ + assert value[1].__class__ == inst.__class__ assert value.Length == 2 inst = System.Exception("badness") diff --git a/tests/test_conversion.py b/tests/test_conversion.py index aea95e164..1980e49c1 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -475,9 +475,6 @@ def test_decimal_conversion(): """Test decimal conversion.""" from System import Decimal - max_d = Decimal.Parse("79228162514264337593543950335") - min_d = Decimal.Parse("-79228162514264337593543950335") - assert Decimal.ToInt64(Decimal(10)) == 10 ob = ConversionTest() @@ -492,21 +489,45 @@ def test_decimal_conversion(): ob.DecimalField = Decimal.Zero assert ob.DecimalField == Decimal.Zero - ob.DecimalField = max_d - assert ob.DecimalField == max_d - - ob.DecimalField = min_d - assert ob.DecimalField == min_d - with pytest.raises(TypeError): ConversionTest().DecimalField = None with pytest.raises(TypeError): ConversionTest().DecimalField = "spam" +def test_timedelta_conversion(): + import datetime + + ob = ConversionTest() + assert type(ob.TimeSpanField) is type(datetime.timedelta(0)) + assert ob.TimeSpanField.days == 0 + + ob.TimeSpanField = datetime.timedelta(days=1) + assert ob.TimeSpanField.days == 1 + + with pytest.raises(TypeError): + ConversionTest().TimeSpanField = None + with pytest.raises(TypeError): - ConversionTest().DecimalField = 1 + ConversionTest().TimeSpanField = "spam" +def test_datetime_conversion(): + from datetime import datetime + + ob = ConversionTest() + assert type(ob.DateTimeField) is type(datetime(1,1,1)) + assert ob.DateTimeField.day == 1 + + ob.DateTimeField = datetime(2000,1,2) + assert ob.DateTimeField.day == 2 + assert ob.DateTimeField.month == 1 + assert ob.DateTimeField.year == 2000 + + with pytest.raises(TypeError): + ConversionTest().DateTimeField = None + + with pytest.raises(TypeError): + ConversionTest().DateTimeField = "spam" def test_string_conversion(): """Test string / unicode conversion.""" @@ -716,3 +737,5 @@ def CanEncode(self, clr_type): l = ob.ListField l.Add(42) assert ob.ListField.Count == 1 + + diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 7fafeebcb..fc631a2eb 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -120,14 +120,12 @@ def test_raise_instance_exception_with_args(): assert isinstance(exc, NullReferenceException) assert exc.Message == 'Aiiieee!' - def test_managed_exception_propagation(): """Test propagation of exceptions raised in managed code.""" - from System import Decimal, OverflowException - - with pytest.raises(OverflowException): - Decimal.ToInt64(Decimal.MaxValue) + from System import Decimal, DivideByZeroException + with pytest.raises(DivideByZeroException): + Decimal.Divide(1, 0) def test_managed_exception_conversion(): """Test conversion of managed exceptions.""" diff --git a/tests/test_generic.py b/tests/test_generic.py index 248303179..9c2e298e7 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -318,6 +318,7 @@ def test_generic_method_type_handling(): assert_generic_method_by_type(ShortEnum, ShortEnum.Zero) assert_generic_method_by_type(System.Object, InterfaceTest()) assert_generic_method_by_type(InterfaceTest, InterfaceTest(), 1) + assert_generic_method_by_type(ISayHello1, InterfaceTest(), 1) def test_correct_overload_selection(): @@ -546,11 +547,10 @@ def test_method_overload_selection_with_generic_types(): value = MethodTest.Overloaded.__overloads__[vtype](input_) assert value.value.__class__ == inst.__class__ - iface_class = ISayHello1(inst).__class__ vtype = GenericWrapper[ISayHello1] input_ = vtype(inst) value = MethodTest.Overloaded.__overloads__[vtype](input_) - assert value.value.__class__ == iface_class + assert value.value.__class__ == inst.__class__ vtype = System.Array[GenericWrapper[int]] input_ = vtype([GenericWrapper[int](0), GenericWrapper[int](1)]) @@ -725,12 +725,11 @@ def test_overload_selection_with_arrays_of_generic_types(): assert value[0].value.__class__ == inst.__class__ assert value.Length == 2 - iface_class = ISayHello1(inst).__class__ gtype = GenericWrapper[ISayHello1] vtype = System.Array[gtype] input_ = vtype([gtype(inst), gtype(inst)]) value = MethodTest.Overloaded.__overloads__[vtype](input_) - assert value[0].value.__class__ == iface_class + assert value[0].value.__class__ == inst.__class__ assert value.Length == 2 diff --git a/tests/test_indexer.py b/tests/test_indexer.py index 7992f76b0..535048ac3 100644 --- a/tests/test_indexer.py +++ b/tests/test_indexer.py @@ -333,31 +333,6 @@ def test_double_indexer(): ob["wrong"] = "wrong" -def test_decimal_indexer(): - """Test Decimal indexers.""" - ob = Test.DecimalIndexerTest() - - from System import Decimal - max_d = Decimal.Parse("79228162514264337593543950335") - min_d = Decimal.Parse("-79228162514264337593543950335") - - assert ob[max_d] is None - - ob[max_d] = "max_" - assert ob[max_d] == "max_" - - ob[min_d] = "min_" - assert ob[min_d] == "min_" - - with pytest.raises(TypeError): - ob = Test.DecimalIndexerTest() - ob["wrong"] - - with pytest.raises(TypeError): - ob = Test.DecimalIndexerTest() - ob["wrong"] = "wrong" - - def test_string_indexer(): """Test String indexers.""" ob = Test.StringIndexerTest() diff --git a/tests/test_interface.py b/tests/test_interface.py index 130bd71c1..7e50aaac5 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -61,8 +61,6 @@ def test_explicit_cast_to_interface(): assert hasattr(i1, 'SayHello') assert i1.SayHello() == 'hello 1' assert not hasattr(i1, 'HelloProperty') - assert i1.__implementation__ == ob - assert i1.__raw_implementation__ == ob i2 = Test.ISayHello2(ob) assert type(i2).__name__ == 'ISayHello2' diff --git a/tests/test_method.py b/tests/test_method.py index 9bdb571c0..0b374208d 100644 --- a/tests/test_method.py +++ b/tests/test_method.py @@ -564,10 +564,8 @@ def test_explicit_overload_selection(): value = MethodTest.Overloaded.__overloads__[InterfaceTest](inst) assert value.__class__ == inst.__class__ - iface_class = ISayHello1(InterfaceTest()).__class__ value = MethodTest.Overloaded.__overloads__[ISayHello1](inst) - assert value.__class__ != inst.__class__ - assert value.__class__ == iface_class + assert value.__class__ == inst.__class__ atype = Array[System.Object] value = MethodTest.Overloaded.__overloads__[str, int, atype]( @@ -720,12 +718,11 @@ def test_overload_selection_with_array_types(): assert value[0].__class__ == inst.__class__ assert value[1].__class__ == inst.__class__ - iface_class = ISayHello1(inst).__class__ vtype = Array[ISayHello1] input_ = vtype([inst, inst]) value = MethodTest.Overloaded.__overloads__[vtype](input_) - assert value[0].__class__ == iface_class - assert value[1].__class__ == iface_class + assert value[0].__class__ == inst.__class__ + assert value[1].__class__ == inst.__class__ def test_explicit_overload_selection_failure(): diff --git a/tests/test_subclass.py b/tests/test_subclass.py index 4f3180480..bcb2e7cbe 100644 --- a/tests/test_subclass.py +++ b/tests/test_subclass.py @@ -112,10 +112,8 @@ def test_interface(): assert ob.bar("bar", 2) == "bar/bar" assert FunctionsTest.test_bar(ob, "bar", 2) == "bar/bar" - # pass_through will convert from InterfaceTestClass -> IInterfaceTest, - # causing a new wrapper object to be created. Hence id will differ. - x = FunctionsTest.pass_through_interface(ob) - assert id(x) != id(ob) + x = FunctionsTest.pass_through(ob) + assert id(x) == id(ob) def test_derived_class(): @@ -188,14 +186,14 @@ def test_create_instance(): assert id(x) == id(ob) InterfaceTestClass = interface_test_class_fixture(test_create_instance.__name__) - ob2 = FunctionsTest.create_instance_interface(InterfaceTestClass) + ob2 = FunctionsTest.create_instance(InterfaceTestClass) assert ob2.foo() == "InterfaceTestClass" assert FunctionsTest.test_foo(ob2) == "InterfaceTestClass" assert ob2.bar("bar", 2) == "bar/bar" assert FunctionsTest.test_bar(ob2, "bar", 2) == "bar/bar" - y = FunctionsTest.pass_through_interface(ob2) - assert id(y) != id(ob2) + y = FunctionsTest.pass_through(ob2) + assert id(y) == id(ob2) def test_events():