diff --git a/src/python_tests_runner/PythonTestRunner.cs b/src/python_tests_runner/PythonTestRunner.cs index 05298997b..dcc76e9d9 100644 --- a/src/python_tests_runner/PythonTestRunner.cs +++ b/src/python_tests_runner/PythonTestRunner.cs @@ -17,6 +17,8 @@ public class PythonTestRunner [OneTimeSetUp] public void SetUp() { + Python.Runtime.Runtime.PythonDLL = + "C:\\Python37.2\\python37.dll"; PythonEngine.Initialize(); } @@ -35,6 +37,13 @@ static IEnumerable PythonTestCases() // Add the test that you want to debug here. yield return new[] { "test_indexer", "test_boolean_indexer" }; yield return new[] { "test_delegate", "test_bool_delegate" }; + yield return new[] { "test_subclass", "test_virtual_generic_method" }; + yield return new[] { "test_subclass", "test_interface_and_class_impl2" }; + yield return new[] { "test_subclass", "test_class_with_attributes" }; + yield return new[] { "test_subclass", "test_class_with_advanced_attribute" }; + yield return new[] { "test_subclass", "test_more_subclasses" }; + yield return new[] { "test_subclass", "test_more_subclasses2" }; + yield return new[] { "test_subclass", "abstract_test" }; } /// diff --git a/src/runtime/PythonTypes/PyObject.cs b/src/runtime/PythonTypes/PyObject.cs index bda2d9c02..31e00a17b 100644 --- a/src/runtime/PythonTypes/PyObject.cs +++ b/src/runtime/PythonTypes/PyObject.cs @@ -25,7 +25,7 @@ public partial class PyObject : DynamicObject, IDisposable, ISerializable /// Trace stack for PyObject's construction /// public StackTrace Traceback { get; } = new StackTrace(1); -#endif +#endif protected IntPtr rawPtr = IntPtr.Zero; internal readonly int run = Runtime.GetRun(); @@ -165,7 +165,7 @@ public static PyObject FromManagedObject(object ob) { if (!Converter.ToManaged(obj, t, out var result, true)) { - throw new InvalidCastException("cannot convert object to target type", + throw new InvalidCastException($"Cannot convert object to target type '{t}'.", PythonException.FetchCurrentOrNull(out _)); } return result; @@ -235,7 +235,7 @@ public void Dispose() { GC.SuppressFinalize(this); Dispose(true); - + } internal StolenReference Steal() @@ -1325,7 +1325,7 @@ private bool TryCompare(PyObject arg, int op, out object @out) } return true; } - + public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object? result) { using var _ = Py.GIL(); diff --git a/src/runtime/PythonTypes/PythonTypeAttribute.cs b/src/runtime/PythonTypes/PythonTypeAttribute.cs new file mode 100644 index 000000000..7dbc413a9 --- /dev/null +++ b/src/runtime/PythonTypes/PythonTypeAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace Python.Runtime; + +/// +/// Marks a property type with a specific python type. Normally, properties has .NET types, but if the property has a python type, +/// that cannot be represented in the propert type info, so this attribute is used to mark the property with the corresponding python type. +/// +public class PythonTypeAttribute : Attribute +{ + /// Type name. + public string TypeName { get; } + + /// Importable module name. + public string Module { get; } + + /// + /// Creates a new instance of PythonTypeAttribute. + /// + /// + /// + public PythonTypeAttribute(string pyTypeModule, string pyTypeName) + { + TypeName = pyTypeName; + Module = pyTypeModule; + } +} diff --git a/src/runtime/Resources/clr.py b/src/runtime/Resources/clr.py index d4330a4d5..7bda24880 100644 --- a/src/runtime/Resources/clr.py +++ b/src/runtime/Resources/clr.py @@ -21,16 +21,18 @@ def test(self): string z = x.test; // calls into python and returns "x" """ - def __init__(self, type_, fget=None, fset=None): + def __init__(self, type_, fget=None, fset=None, attributes = []): self.__name__ = getattr(fget, "__name__", None) self._clr_property_type_ = type_ self.fget = fget self.fset = fset - + self._clr_attributes_ = attributes def __call__(self, fget): - return self.__class__(self._clr_property_type_, + self.__class__(self._clr_property_type_, fget=fget, - fset=self.fset) + fset=self.fset, + attributes = self._clr_attributes_) + def setter(self, fset): self.fset = fset @@ -48,7 +50,69 @@ def __set__(self, instance, value): raise AttributeError("%s is read-only" % self.__name__) return self.fset.__get__(instance, None)(value) + # TODO: I am not sure this add_attribute is actually necessary. + def add_attribute(self, *args, **kwargs): + """Adds an attribute to this class. + If the first argument is a tuple we assume it is a tuple containing everything to initialize the attribute. + Otherwise, the first arg should be a .net type implementing Attribute.""" + lst = [] + if len(args) > 0: + if isinstance(args[0], tuple): + lst = args + else: + lst = [(args[0], args[1:], kwargs)] + self._clr_attributes_.extend(lst) + return self + +class property(object): + """ + property constructor for creating properties with implicit get/set. + + It can be used as such: + e.g.:: + + class X(object): + A = clr.property(Double, 3.14)\ + .add_attribute(Browsable(False)) + """ + def __init__(self, type, default = None): + import weakref + self._clr_property_type_ = type + self.default = default + self.values = weakref.WeakKeyDictionary() + self._clr_attributes_ = [] + self.fget = 1 + self.fset = 1 + def __get__(self, instance, owner): + if self.fget != 1: + return self.fget(instance) + v = self.values.get(instance, self.default) + return v + def __set__(self, instance, value): + if self.fset != 1: + self.fset(instance,value) + return + self.values[instance] = value + + def add_attribute(self, *args, **kwargs): + """Adds an attribute to this class. + If the first argument is a tuple we assume it is a tuple containing everything to initialize the attribute. + Otherwise, the first arg should be a .net type implementing Attribute.""" + lst = [] + if len(args) > 0: + if isinstance(args[0], tuple): + lst = args + else: + lst = [(args[0], args[1:], kwargs)] + self._clr_attributes_.extend(lst) + return self + + def __call__(self, func): + self2 = self.__class__(self._clr_property_type_, None) + self2.fget = func + self2._clr_attributes_ = self._clr_attributes_ + return self2 class clrmethod(object): """ Method decorator for exposing python methods to .NET. @@ -67,18 +131,69 @@ def test(self, x): int z = x.test("hello"); // calls into python and returns len("hello") """ - def __init__(self, return_type, arg_types, clrname=None, func=None): + def __init__(self, return_type = None, arg_types = [], clrname=None, func=None, **kwargs): + if return_type == None: + import System + return_type = System.Void self.__name__ = getattr(func, "__name__", None) self._clr_return_type_ = return_type self._clr_arg_types_ = arg_types self._clr_method_name_ = clrname or self.__name__ self.__func = func + if 'attributes' in kwargs: + self._clr_attributes_ = kwargs["attributes"] + else: + self._clr_attributes_ = [] def __call__(self, func): - return self.__class__(self._clr_return_type_, + self2 = self.__class__(self._clr_return_type_, self._clr_arg_types_, clrname=self._clr_method_name_, func=func) + self2._clr_attributes_ = self._clr_attributes_ + return self2 def __get__(self, instance, owner): return self.__func.__get__(instance, owner) + + def add_attribute(self, *args, **kwargs): + """Adds an attribute to this class. + If the first argument is a tuple we assume it is a tuple containing everything to initialize the attribute. + Otherwise, the first arg should be a .net type implementing Attribute.""" + lst = [] + if len(args) > 0: + if isinstance(args[0], tuple): + lst = args + else: + lst = [(args[0], args[1:], kwargs)] + self._clr_attributes_.extend(lst) + return self + +class attribute(object): + """ + Class decorator for adding attributes to .net python classes. + + e.g.:: + @attribute(DisplayName("X Class")) + class X(object): + pass + """ + def __init__(self, *args, **kwargs): + lst = [] + if len(args) > 0: + if isinstance(args[0], tuple): + lst = args + else: + lst = [(args[0], args[1:], kwargs)] + import Python.Runtime + #todo: ensure that attributes only are pushed when @ is used. + self.attr = lst + for item in lst: + Python.Runtime.PythonDerivedType.PushAttribute(item) + + def __call__(self, x): + import Python.Runtime + for item in self.attr: + if Python.Runtime.PythonDerivedType.AssocAttribute(item, x): + pass + return x diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index beb577e45..d5558084f 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -1836,6 +1836,12 @@ internal static void SetNoSiteFlag() return *Delegates.Py_NoSiteFlag; }); } + + internal static uint PyTuple_GetSize(BorrowedReference tuple) + { + IntPtr r = Delegates.PyTuple_Size(tuple); + return (uint)r.ToInt32(); + } } internal class BadPythonDllException : MissingMethodException diff --git a/src/runtime/StateSerialization/MaybeType.cs b/src/runtime/StateSerialization/MaybeType.cs index f3c96e369..549c5f29e 100644 --- a/src/runtime/StateSerialization/MaybeType.cs +++ b/src/runtime/StateSerialization/MaybeType.cs @@ -15,7 +15,6 @@ internal struct MaybeType : ISerializable const string SerializationName = "n"; readonly string name; readonly Type type; - public string DeletedMessage { get @@ -38,6 +37,7 @@ public Type Value public string Name => name; public bool Valid => type != null; + public Type ValueOrNull => type; public override string ToString() { @@ -61,4 +61,4 @@ public void GetObjectData(SerializationInfo serializationInfo, StreamingContext serializationInfo.AddValue(SerializationName, name); } } -} \ No newline at end of file +} diff --git a/src/runtime/TypeManager.cs b/src/runtime/TypeManager.cs index e0a78ba49..8278bb4dc 100644 --- a/src/runtime/TypeManager.cs +++ b/src/runtime/TypeManager.cs @@ -374,7 +374,7 @@ static PyTuple GetBaseTypeTuple(Type clrType) return new PyTuple(bases); } - internal static NewReference CreateSubType(BorrowedReference py_name, BorrowedReference py_base_type, BorrowedReference dictRef) + internal static NewReference CreateSubType(BorrowedReference py_name, IEnumerable py_base_type, IEnumerable interfaces, BorrowedReference dictRef) { // Utility to create a subtype of a managed type with the ability for the // a python subtype able to override the managed implementation @@ -415,17 +415,12 @@ internal static NewReference CreateSubType(BorrowedReference py_name, BorrowedRe } // create the new managed type subclassing the base managed type - if (ManagedType.GetManagedObject(py_base_type) is ClassBase baseClass) - { - return ReflectedClrType.CreateSubclass(baseClass, name, - ns: (string?)namespaceStr, - assembly: (string?)assembly, - dict: dictRef); - } - else - { - return Exceptions.RaiseTypeError("invalid base class, expected CLR class type"); - } + var baseClass = py_base_type.FirstOrDefault(); + + return ReflectedClrType.CreateSubclass(baseClass, interfaces, name, + ns: (string?)namespaceStr, + assembly: (string?)assembly, + dict: dictRef); } internal static IntPtr WriteMethodDef(IntPtr mdef, IntPtr name, IntPtr func, PyMethodFlags flags, IntPtr doc) diff --git a/src/runtime/Types/ClassDerived.cs b/src/runtime/Types/ClassDerived.cs index cf6d9b16b..b5bdad47e 100644 --- a/src/runtime/Types/ClassDerived.cs +++ b/src/runtime/Types/ClassDerived.cs @@ -137,6 +137,117 @@ internal static NewReference ToPython(IPythonDerivedType obj) return result; } + static CustomAttributeBuilder AddAttribute(PyObject attr) + { + // this is a bit complicated because we want to support both unnamed and named arguments + // in C# there is a discrepancy between property setters and named argument wrt attributes + // in python there is no difference. But luckily there is rarely a conflict, so we can treat + // named arguments and named properties or fields the same way. + var tp = new PyTuple(attr); + Type attribute = (Type) tp[0].AsManagedObject(typeof(object)); + if (typeof(Attribute).IsAssignableFrom(attribute) == false) + { + throw new Exception("This type is not an attribute type."); + } + + var args = (PyObject[]) tp[1].AsManagedObject(typeof(PyObject[])); + var dict = new PyDict(tp[2]); + var dict2 = new Dictionary(); + foreach (var key in dict.Keys()) + { + var k = key.As(); + dict2[k] = dict[key]; + } + + // todo support kwargs and tupleargs + var allconstructors = attribute.GetConstructors(); + foreach (var constructor in allconstructors) + { + var parameters = constructor.GetParameters(); + List paramValues = new List(); + HashSet accountedFor = new HashSet(); + for (int i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + if (parameter.ParameterType.IsArray && null != parameter.GetCustomAttribute(typeof(ParamArrayAttribute))) + { + int cnt = args.Length - i; + var elemType = parameter.ParameterType.GetElementType(); + var values = Array.CreateInstance(elemType, cnt); + for(int j = 0; j < cnt; j++) + values.SetValue(args[i + j].AsManagedObject(typeof(object)), j); + paramValues.Add(values); + break; + } + + PyObject argvalue = null; + if (args.Length <= i && dict2.TryGetValue(parameter.Name, out argvalue)) + { + accountedFor.Add(parameter.Name); + }else if (args.Length <= i && parameter.IsOptional) + { + paramValues.Add(parameter.DefaultValue); + continue; + }else if (args.Length <= i) + { + goto next; + } + else + { + argvalue = args[i]; + } + + var argval = argvalue.AsManagedObject(parameter.ParameterType); + if (parameter.ParameterType.IsAssignableFrom(argval?.GetType() ?? typeof(object))) + paramValues.Add(argval); + else if (parameter.IsOptional) + { + paramValues.Add(parameter.DefaultValue); + } + } + + List namedProperties = new List(); + List namedPropertyValues = new List(); + List namedFields = new List(); + List namedFieldValues = new List(); + + foreach (var key in dict2.Keys.Where(x => accountedFor.Contains(x) == false)) + { + var member = attribute.GetMember(key).FirstOrDefault(); + if (member == null) + goto next; + if (member is PropertyInfo prop) + { + namedProperties.Add(prop); + var argval = dict2[key].AsManagedObject(prop.PropertyType); + if (prop.PropertyType.IsAssignableFrom(argval?.GetType() ?? typeof(object))) + namedPropertyValues.Add(argval); + else + goto next; + } + if (member is FieldInfo field) + { + namedFields.Add(field); + var argval = dict2[key].AsManagedObject(field.FieldType); + if (field.FieldType.IsAssignableFrom(argval?.GetType() ?? typeof(object))) + namedFieldValues.Add(argval); + else + goto next; + } + } + + var cb = new CustomAttributeBuilder(constructor, paramValues.ToArray(), + namedProperties.ToArray(), namedPropertyValues.ToArray(), + namedFields.ToArray(), namedFieldValues.ToArray()); + return cb; + next: ; + } + + return null; + } + + + /// /// Creates a new managed type derived from a base type with any virtual /// methods overridden to call out to python if the associated python @@ -144,6 +255,7 @@ internal static NewReference ToPython(IPythonDerivedType obj) /// internal static Type CreateDerivedType(string name, Type baseType, + IEnumerable interfaces2, BorrowedReference py_dict, string? namespaceStr, string? assemblyName, @@ -163,7 +275,10 @@ internal static Type CreateDerivedType(string name, ModuleBuilder moduleBuilder = GetModuleBuilder(assemblyName, moduleName); Type baseClass = baseType; - var interfaces = new List { typeof(IPythonDerivedType) }; + var interfaces = new HashSet { typeof(IPythonDerivedType) }; + + foreach(var interfaceType in interfaces2) + interfaces.Add(interfaceType); // if the base type is an interface then use System.Object as the base class // and add the base type to the list of interfaces this new class will implement. @@ -173,26 +288,44 @@ internal static Type CreateDerivedType(string name, baseClass = typeof(object); } + // __clr_abstract__ is used to create an abstract class. + bool isAbstract = false; + if (py_dict != null && Runtime.PyDict_Check(py_dict)) + { + using var dict = new PyDict(py_dict); + if (dict.HasKey("__clr_abstract__")) + isAbstract = true; + } + TypeBuilder typeBuilder = moduleBuilder.DefineType(name, - TypeAttributes.Public | TypeAttributes.Class, + TypeAttributes.Public | TypeAttributes.Class | (isAbstract ? TypeAttributes.Abstract : 0), baseClass, interfaces.ToArray()); - // add a field for storing the python object pointer - // FIXME: fb not used - FieldBuilder fb = typeBuilder.DefineField(PyObjName, + // add a field for storing the python object pointer, + // but only if the baseclass does not already have it. + if (baseClass.GetField(PyObjName, PyObjFlags) == null) + { + // FIXME: fb not used + FieldBuilder fb = typeBuilder.DefineField(PyObjName, #pragma warning disable CS0618 // Type or member is obsolete. OK for internal use. - typeof(UnsafeReferenceWithRun), + typeof(UnsafeReferenceWithRun), #pragma warning restore CS0618 // Type or member is obsolete - FieldAttributes.Private); + FieldAttributes.Public); + } // override any constructors - ConstructorInfo[] constructors = baseClass.GetConstructors(); + ConstructorInfo[] constructors = baseClass.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (ConstructorInfo ctor in constructors) { AddConstructor(ctor, baseType, typeBuilder); } + if (constructors.Length == 0) + { + AddConstructor(null, baseType, typeBuilder); + } + // Override any properties explicitly overridden in python var pyProperties = new HashSet(); if (py_dict != null && Runtime.PyDict_Check(py_dict)) @@ -215,12 +348,13 @@ internal static Type CreateDerivedType(string name, } // override any virtual methods not already overridden by the properties above - MethodInfo[] methods = baseType.GetMethods(); + var methods = baseType.GetMethods().Concat(interfaces.SelectMany(x => x.GetMethods())); var virtualMethods = new HashSet(); foreach (MethodInfo method in methods) { - if (!method.Attributes.HasFlag(MethodAttributes.Virtual) | - method.Attributes.HasFlag(MethodAttributes.Final)) + if (!method.Attributes.HasFlag(MethodAttributes.Virtual) + || method.Attributes.HasFlag(MethodAttributes.Final) + || method.IsGenericMethod) { continue; } @@ -243,6 +377,26 @@ internal static Type CreateDerivedType(string name, if (py_dict != null && Runtime.PyDict_Check(py_dict)) { using var dict = new PyDict(py_dict); + + // if there are any attributes, add them. + if (dict.HasKey("__clr_attributes__")) + { + using var attributes = new PyList(dict["__clr_attributes__"]); + foreach (var attr in attributes) + { + var builder = AddAttribute(attr); + if (builder == null) throw new Exception(); + typeBuilder.SetCustomAttribute(builder); + } + } + + foreach (var attr in PopAttributes()) + { + var builder = AddAttribute(attr); + if (builder == null) throw new Exception(); + typeBuilder.SetCustomAttribute(builder); + } + using var keys = dict.Keys(); foreach (PyObject pyKey in keys) { @@ -283,6 +437,7 @@ internal static Type CreateDerivedType(string name, Type type = typeBuilder.CreateType(); + // scan the assembly so the newly added class can be imported Assembly assembly = Assembly.GetAssembly(type); AssemblyManager.ScanAssembly(assembly); @@ -296,12 +451,12 @@ internal static Type CreateDerivedType(string name, /// /// Add a constructor override that calls the python ctor after calling the base type constructor. /// - /// constructor to be called before calling the python ctor + /// constructor to be called before calling the python ctor. This can be null if there is no constructor. /// Python callable object /// TypeBuilder for the new type the ctor is to be added to private static void AddConstructor(ConstructorInfo ctor, Type baseType, TypeBuilder typeBuilder) { - ParameterInfo[] parameters = ctor.GetParameters(); + ParameterInfo[] parameters = ctor?.GetParameters() ?? Array.Empty(); Type[] parameterTypes = (from param in parameters select param.ParameterType).ToArray(); // create a method for calling the original constructor @@ -320,14 +475,15 @@ private static void AddConstructor(ConstructorInfo ctor, Type baseType, TypeBuil { il.Emit(OpCodes.Ldarg, i + 1); } - il.Emit(OpCodes.Call, ctor); + if(ctor != null) + il.Emit(OpCodes.Call, ctor); il.Emit(OpCodes.Ret); // override the original method with a new one that dispatches to python ConstructorBuilder cb = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.HideBySig, - ctor.CallingConvention, + ctor?.CallingConvention ?? CallingConventions.Any, parameterTypes); il = cb.GetILGenerator(); il.DeclareLocal(typeof(object[])); @@ -370,23 +526,26 @@ private static void AddVirtualMethod(MethodInfo method, Type baseType, TypeBuild string? baseMethodName = null; if (!method.IsAbstract) { - baseMethodName = "_" + baseType.Name + "__" + method.Name; - MethodBuilder baseMethodBuilder = typeBuilder.DefineMethod(baseMethodName, - MethodAttributes.Public | - MethodAttributes.Final | - MethodAttributes.HideBySig, - method.ReturnType, - parameterTypes); - - // emit the assembly for calling the original method using call instead of callvirt - ILGenerator baseIl = baseMethodBuilder.GetILGenerator(); - baseIl.Emit(OpCodes.Ldarg_0); - for (var i = 0; i < parameters.Length; ++i) + baseMethodName = "_" + method.DeclaringType.Name + "__" + method.Name; + if (baseType.GetMethod(baseMethodName) == null) { - baseIl.Emit(OpCodes.Ldarg, i + 1); + MethodBuilder baseMethodBuilder = typeBuilder.DefineMethod(baseMethodName, + MethodAttributes.Public | + MethodAttributes.Final | + MethodAttributes.HideBySig, + method.ReturnType, + parameterTypes); + + // emit the assembly for calling the original method using call instead of callvirt + ILGenerator baseIl = baseMethodBuilder.GetILGenerator(); + baseIl.Emit(OpCodes.Ldarg_0); + for (var i = 0; i < parameters.Length; ++i) + { + baseIl.Emit(OpCodes.Ldarg, i + 1); + } + baseIl.Emit(OpCodes.Call, method); + baseIl.Emit(OpCodes.Ret); } - baseIl.Emit(OpCodes.Call, method); - baseIl.Emit(OpCodes.Ret); } // override the original method with a new one that dispatches to python @@ -471,6 +630,7 @@ private static void AddPythonMethod(string methodName, PyObject func, TypeBuilde } using var pyReturnType = func.GetAttr("_clr_return_type_"); + using var attributes = new PyList(func.GetAttr("_clr_attributes_")); using var pyArgTypes = func.GetAttr("_clr_arg_types_"); using var pyArgTypesIter = PyIter.GetIter(pyArgTypes); var returnType = pyReturnType.AsManagedObject(typeof(Type)) as Type; @@ -502,7 +662,27 @@ private static void AddPythonMethod(string methodName, PyObject func, TypeBuilde returnType, argTypes.ToArray()); - ILGenerator il = methodBuilder.GetILGenerator(); + foreach (var attr in attributes) + { + var builder = AddAttribute(attr); + if (builder == null) throw new Exception(); + methodBuilder.SetCustomAttribute(builder); + } + + if (MethodAttributeAssociations.TryGetValue(func, out var lst)) + { + foreach(var attr in lst) + { + var builder = AddAttribute(attr); + if (builder == null) throw new Exception(); + methodBuilder.SetCustomAttribute(builder); + } + + MethodAttributeAssociations.Remove(func); + + } + + ILGenerator il = methodBuilder.GetILGenerator(); il.DeclareLocal(typeof(object[])); il.DeclareLocal(typeof(RuntimeMethodHandle)); @@ -589,16 +769,62 @@ private static void AddPythonProperty(string propertyName, PyObject func, TypeBu MethodAttributes.SpecialName; using var pyPropertyType = func.GetAttr("_clr_property_type_"); - var propertyType = pyPropertyType.AsManagedObject(typeof(Type)) as Type; + var pyNativeType = new PyType(pyPropertyType); + Converter.ToManaged(pyPropertyType, typeof(Type), out var result, false); + var propertyType = result as Type; + string pyTypeName = null; + string pyTypeModule = null; + // if the property type is null, we assume that it is a python type + // and not a C# type, in this case the property is just a PyObject type instead. if (propertyType == null) { - throw new ArgumentException("_clr_property_type must be a CLR type"); + propertyType = typeof(PyObject); + pyTypeModule = pyNativeType.GetAttr("__module__").ToString(); + pyTypeName = pyNativeType.Name; } PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, null); + if (func.HasAttr("_clr_attributes_")) + { + using (var attributes = new PyList(func.GetAttr("_clr_attributes_"))) + { + foreach (var attr in attributes) + { + var builder = AddAttribute(attr); + if (builder == null) throw new Exception(); + propertyBuilder.SetCustomAttribute(builder); + } + } + } + + if (pyTypeName != null) + { + var cb = new CustomAttributeBuilder(typeof(PythonTypeAttribute).GetConstructors().First(), + new object[] {pyTypeModule, pyTypeName}); + propertyBuilder.SetCustomAttribute(cb); + } + + { + // load attribute set on functions + foreach (var fname in new[]{ "fget", "fset" }) + { + using var pyfget = func.GetAttr(fname); + if (MethodAttributeAssociations.TryGetValue(pyfget, out var lst)) + { + foreach (var attr in lst) + { + var builder = AddAttribute(attr); + if (builder == null) throw new Exception(); + propertyBuilder.SetCustomAttribute(builder); + } + + MethodAttributeAssociations.Remove(func); + } + } + } if (func.HasAttr("fget")) { @@ -694,8 +920,69 @@ private static ModuleBuilder GetModuleBuilder(string assemblyName, string module [Obsolete(Util.InternalUseOnly)] public class PythonDerivedType { + /// Tracks the attributes pushed with PushAttributes. + static List attributesStack = new (); + /// + /// This field track associations between python functions and associated attributes. + /// + internal static Dictionary> MethodAttributeAssociations = new (); + + /// + /// This pushes an attribute on the current attribute stack. + /// This happens when invoking e.g `@(attribute(Browsable(False))`. + /// + /// The pushed attribute. + /// This is should not be an attribute instance, but a tuple from which it can be created. + public static void PushAttribute(PyObject attribute) + { + using var _ = Py.GIL(); + var tp = new PyTuple(attribute); + attributesStack.Add(tp); + } + + /// + /// Associates an attribute with a function. + /// + /// + /// + /// + + public static bool AssocAttribute(PyObject attribute, PyObject func) + { + using var _ = Py.GIL(); + var tp = new PyTuple(attribute); + for (int i = 0; i < attributesStack.Count; i++) + { + if (tp.BorrowNullable() == attributesStack[i].BorrowNullable()) + { + attributesStack.RemoveAt(i); + if (!MethodAttributeAssociations.TryGetValue(func, out var lst)) + { + lst = MethodAttributeAssociations[func] = new List(); + } + lst.Add(tp); + return true; + } + } + return false; + + } + + + /// + /// Pops the current attribute stack and returns it. Any future pushed attributes will be in a new list. + /// + /// + public static IEnumerable PopAttributes() + { + if (attributesStack.Count == 0) return Array.Empty(); + var attrs = attributesStack; + attributesStack = new List(); + return attrs; + } + internal const string PyObjName = "__pyobj__"; - internal const BindingFlags PyObjFlags = BindingFlags.Instance | BindingFlags.NonPublic; + internal const BindingFlags PyObjFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy; /// /// This is the implementation of the overridden methods in the derived @@ -900,6 +1187,7 @@ public static void InvokeSetProperty(IPythonDerivedType obj, string propertyN } } + static private PyObject pin; public static void InvokeCtor(IPythonDerivedType obj, string origCtorName, object[] args) { var selfRef = GetPyObj(obj); @@ -910,8 +1198,22 @@ public static void InvokeCtor(IPythonDerivedType obj, string origCtorName, objec // In the end we decrement the python object's reference count. // This doesn't actually destroy the object, it just sets the reference to this object // to be a weak reference and it will be destroyed when the C# object is destroyed. - using var self = CLRObject.GetReference(obj, obj.GetType()); - SetPyObj(obj, self.Borrow()); + + PyType cc = ReflectedClrType.GetOrCreate(obj.GetType()); + var args2 = new PyObject[args.Length]; + for (int i = 0; i < args.Length; i++) + args2[i] = args[i].ToPython(); + // create an instance of the class and steal the reference. + using (var obj2 = cc.Invoke(args2, null)) + { + // somehow if this is not done this object never gets a reference count + // and things breaks later. + // I am not sure what it does though. + GCHandle gc = GCHandle.Alloc(obj); + // hand over the reference. + var py = obj2.NewReferenceOrNull(); + SetPyObj(obj, py.Borrow()); + } } // call the base constructor diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index 5b59f5139..d198ddd41 100644 --- a/src/runtime/Types/MetaType.cs +++ b/src/runtime/Types/MetaType.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -79,41 +82,51 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, BorrowedReference bases = Runtime.PyTuple_GetItem(args, 1); BorrowedReference dict = Runtime.PyTuple_GetItem(args, 2); - // We do not support multiple inheritance, so the bases argument - // should be a 1-item tuple containing the type we are subtyping. - // That type must itself have a managed implementation. We check - // that by making sure its metatype is the CLR metatype. + // Extract interface types and base class types. + List interfaces = new List(); + List baseType = new List(); - if (Runtime.PyTuple_Size(bases) != 1) + var cnt = Runtime.PyTuple_GetSize(bases); + + for (uint i = 0; i < cnt; i++) { - return Exceptions.RaiseTypeError("cannot use multiple inheritance with managed classes"); + var base_type2 = Runtime.PyTuple_GetItem(bases, (int)i); + var cb2 = (ClassBase) GetManagedObject(base_type2); + if (cb2 != null) + { + if (cb2.type.Valid && cb2.type.Value.IsInterface) + interfaces.Add(cb2.type.Value); + else baseType.Add(cb2); + } } - BorrowedReference base_type = Runtime.PyTuple_GetItem(bases, 0); - BorrowedReference mt = Runtime.PyObject_TYPE(base_type); + // if the base type count is 0, there might still be interfaces to implement. + if (baseType.Count == 0) + { + baseType.Add(new ClassBase(typeof(object))); + } - if (!(mt == PyCLRMetaType || mt == Runtime.PyTypeType)) + // Multiple inheritance is not supported, unless the other types are interfaces + if (baseType.Count > 1) { - return Exceptions.RaiseTypeError("invalid metatype"); + return Exceptions.RaiseTypeError("cannot use multiple inheritance with managed classes"); } // Ensure that the reflected type is appropriate for subclassing, // disallowing subclassing of delegates, enums and array types. - if (GetManagedObject(base_type) is ClassBase cb) + var cb = baseType.First(); + try { - try - { - if (!cb.CanSubclass()) - { - return Exceptions.RaiseTypeError("delegates, enums and array types cannot be subclassed"); - } - } - catch (SerializationException) + if (!cb.CanSubclass()) { - return Exceptions.RaiseTypeError($"Underlying C# Base class {cb.type} has been deleted"); + return Exceptions.RaiseTypeError("delegates, enums and array types cannot be subclassed"); } } + catch (SerializationException) + { + return Exceptions.RaiseTypeError($"Underlying C# Base class {cb.type} has been deleted"); + } BorrowedReference slots = Runtime.PyDict_GetItem(dict, PyIdentifier.__slots__); if (slots != null) @@ -121,19 +134,31 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, return Exceptions.RaiseTypeError("subclasses of managed classes do not support __slots__"); } - // If __assembly__ or __namespace__ are in the class dictionary then create + // If the base class has a parameterless constructor, or + // if __assembly__ or __namespace__ are in the class dictionary then create // a managed sub type. // This creates a new managed type that can be used from .net to call back // into python. if (null != dict) { + var btt = baseType.FirstOrDefault().type.ValueOrNull; + var ctor = btt?.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .FirstOrDefault(x => x.GetParameters().Any() == false); using var clsDict = new PyDict(dict); - if (clsDict.HasKey("__assembly__") || clsDict.HasKey("__namespace__")) + + if (clsDict.HasKey("__assembly__") || clsDict.HasKey("__namespace__") + || (ctor != null)) { - return TypeManager.CreateSubType(name, base_type, clsDict); + if (!clsDict.HasKey("__namespace__")) + { + clsDict["__namespace__"] = + (clsDict["__module__"].ToString()).ToPython(); + } + return TypeManager.CreateSubType(name, baseType, interfaces, clsDict); } } + var base_type = Runtime.PyTuple_GetItem(bases, 0); // otherwise just create a basic type without reflecting back into the managed side. IntPtr func = Util.ReadIntPtr(Runtime.PyTypeType, TypeOffset.tp_new); NewReference type = NativeCall.Call_3(func, tp, args, kw); diff --git a/src/runtime/Types/ModuleObject.cs b/src/runtime/Types/ModuleObject.cs index f641b393e..f3f9a0e67 100644 --- a/src/runtime/Types/ModuleObject.cs +++ b/src/runtime/Types/ModuleObject.cs @@ -132,6 +132,25 @@ public NewReference GetAttribute(string name, bool guess) return new NewReference(c); } + // Simplified imported attribute names + // for attributes without the Attribute suffix, create an attribute builder. + // This means that imported attributes can be invoked using just e.g 'Browsable(False)' + // and not BrowsableAttribute(False) although its still an option. + var qname2 = qname + "Attribute"; + var type2 = AssemblyManager.LookupTypes(qname2).FirstOrDefault(t => t.IsPublic); + if (type2 != null) + { + var str = "def {2}attrbuilder(*args, **kwargs):\n" + + " import {1}\n" + + " return ({0}, args, kwargs)\n"; + str = string.Format(str, qname2, _namespace, name); + PythonEngine.Exec(str); + var obj = PythonEngine.Eval(name + "attrbuilder"); + var o = obj.NewReferenceOrNull(); + this.StoreAttribute(name, o.Borrow()); + return new NewReference(o); + } + // We didn't find the name, so we may need to see if there is a // generic type with this base name. If so, we'll go ahead and // return it. Note that we store the mapping of the unmangled @@ -322,7 +341,7 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k if (attr.IsNull()) { - Exceptions.SetError(Exceptions.AttributeError, name); + Exceptions.SetError(Exceptions.AttributeError, $"name '{name}' is not defined in module '{self.moduleName}'."); return default; } diff --git a/src/runtime/Types/ReflectedClrType.cs b/src/runtime/Types/ReflectedClrType.cs index d3d89bdb8..41d568ac4 100644 --- a/src/runtime/Types/ReflectedClrType.cs +++ b/src/runtime/Types/ReflectedClrType.cs @@ -68,7 +68,7 @@ internal void Restore(ClassBase cb) TypeManager.InitializeClass(this, cb, cb.type.Value); } - internal static NewReference CreateSubclass(ClassBase baseClass, + internal static NewReference CreateSubclass(ClassBase baseClass, IEnumerable interfaces, string name, string? assembly, string? ns, BorrowedReference dict) { @@ -76,6 +76,7 @@ internal static NewReference CreateSubclass(ClassBase baseClass, { Type subType = ClassDerivedObject.CreateDerivedType(name, baseClass.type.Value, + interfaces, dict, ns, assembly); diff --git a/src/testing/generictest.cs b/src/testing/generictest.cs index 238435811..a78fd5104 100644 --- a/src/testing/generictest.cs +++ b/src/testing/generictest.cs @@ -95,6 +95,11 @@ public string Overloaded(int arg1, int arg2, string arg3) { return arg3; } + + public virtual Q VirtualOverloaded(Q arg) + { + return arg; + } } public class GenericStaticMethodTest @@ -118,10 +123,7 @@ public static Q Overloaded(Q arg) return arg; } - public static U Overloaded(Q arg1, U arg2) - { - return arg2; - } + public static string Overloaded(int arg1, int arg2, string arg3) { @@ -136,4 +138,12 @@ public static T[] EchoRange(T[] items) return items; } } + + public abstract class GenericVirtualMethodTest + { + public virtual Q VirtMethod(Q arg1) + { + return arg1; + } + } } diff --git a/src/testing/subclasstest.cs b/src/testing/subclasstest.cs index ab0b73368..9b6071c15 100644 --- a/src/testing/subclasstest.cs +++ b/src/testing/subclasstest.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Threading; + +using Python.Runtime; namespace Python.Test { @@ -124,4 +127,134 @@ public static int test_event(IInterfaceTest x, int value) return et.value; } } + + public interface ISimpleInterface + { + bool Ok(); + } + public interface ISimpleInterface2 + { + int Execute(CancellationToken token); + } + public class TestAttributeAttribute: Attribute + { + public int X { get; set; } + public int Y { get; set; } + public string Z { get; set; } + public string W { get; set; } + public TestAttributeAttribute(int x, int y, string z = "x") + { + X = x; + Y = y; + Z = z; + + } + } + + public abstract class SimpleClassBase + { + private int counter; + public virtual int IncrementThing() + { + return counter++; + } + + } + + public abstract class SimpleClass : SimpleClassBase + { + public bool Initialized; + + public SimpleClass() + { + Initialized = true; + } + + public int CallIncrementThing() + { + var x = IncrementThing(); + return x; + } + + public static void TestObject(object obj) + { + if (obj is ISimpleInterface si) + { + if (!si.Ok()) + throw new Exception(); + + }else if (obj is ISimpleInterface2 si2) + { + si2.Execute(CancellationToken.None); + + } + else + { + throw new Exception(); + } + } + public static void TestObjectProperty(object obj, string prop, double newval) + { + obj.GetType().GetProperty(prop).SetValue(obj, newval); + var val = obj.GetType().GetProperty(prop).GetValue(obj); + if (!Equals(newval, val)) + throw new Exception(); + } + + private static SimpleClass objStore; + public static void Test1(SimpleClass obj) + { + objStore = obj; + int x = obj.IncrementThing(); + } + + public static void Test2() + { + GC.Collect(); + + var threads = new Thread[20]; + for(int i = 0; i < threads.Length; i++) + threads[i] = new Thread(() => TestObjectProperty(objStore, "X", 10.0)); + for (int i = 0; i < threads.Length; i++) + threads[i].Start(); + for (int i = 0; i < threads.Length; i++) + threads[i].Join(); + } + + public static object InvokeCtor(Type t) + { + var obj = Activator.CreateInstance(t); + return obj; + } + + public object TestObj { get; set; } + + public static object TestOnType(Type t) + { + using (Py.GIL()) + { + var obj = (SimpleClass) Activator.CreateInstance(t); + //obj = obj.ToPython().As(); + obj.TestObj = new object(); + var py = obj.ToPython(); + var man = py.As(); + if (!ReferenceEquals(man, obj)) + throw new Exception("Same object expected"); + var setObj = py.GetAttr("TestObj").As(); + if (setObj == null) + throw new NullReferenceException(); + if (ReferenceEquals(setObj, obj.TestObj) == false) + throw new Exception("!!"); + + + return obj; + } + } + + public static void Pause() + { + + } + + } } diff --git a/tests/test_subclass.py b/tests/test_subclass.py index a51e89da3..6f06cb55f 100644 --- a/tests/test_subclass.py +++ b/tests/test_subclass.py @@ -7,9 +7,14 @@ """Test sub-classing managed types""" import System +from System import (Console, Attribute, Double) +from System.Diagnostics import (DebuggerDisplay, DebuggerDisplayAttribute, Debug) +from System.ComponentModel import (Browsable, BrowsableAttribute) +from System.Threading import (CancellationToken) import pytest from Python.Test import (IInterfaceTest, SubClassTest, EventArgsTest, - FunctionsTest, IGenericInterface) + FunctionsTest, GenericVirtualMethodTest, ISimpleInterface, SimpleClass, TestAttribute, TestAttributeAttribute, ISimpleInterface2, IGenericInterface) +import Python.Test from System.Collections.Generic import List @@ -275,6 +280,161 @@ class TestX(System.Object): t = TestX() assert t.q == 1 +def test_virtual_generic_method(): + class OverloadingSubclass(GenericVirtualMethodTest): + __namespace__ = "test_virtual_generic_method_cls" + class OverloadingSubclass2(OverloadingSubclass): + pass + obj = OverloadingSubclass() + assert obj.VirtMethod[int](5) == 5 + +def test_interface_and_class_impl(): + class OverloadingSubclass(GenericVirtualMethodTest): + __namespace__ = "test_virtual_generic_method_cls" + class OverloadingSubclass2(OverloadingSubclass): + pass + obj = OverloadingSubclass() + assert obj.VirtMethod[int](5) == 5 + +def test_interface_and_class_impl2(): + class DualSubClass(ISimpleInterface, SimpleClass): + def Ok(self): + return True + class DualSubClass2(ISimpleInterface): + def Ok(self): + return True + class DualSubClass3(ISimpleInterface2): + def Execute(self, cancellationToken): + return 0 + try: + class DualSubClass4(Python.Test.ISimpleInterface3): + def Execute(self, cancellationToken): + return 0 + assert False # An exception should be thrown. + except AttributeError as ae: + assert ("not defined" in str(ae)) + + obj = DualSubClass() + SimpleClass.TestObject(obj) + obj = DualSubClass2() + SimpleClass.TestObject(obj) + + obj2 = DualSubClass3(); + SimpleClass.TestObject(obj2) + #obj2.Execute(CancellationToken.None) + +def test_class_with_attributes(): + import clr + @clr.attribute(Browsable(False)) + class ClassWithAttributes(ISimpleInterface, SimpleClass): + __clr_attributes__ = [DebuggerDisplay("X: {X}")] + @clr.attribute(Browsable(True)) + def Ok(self): + return True + @clr.attribute(Browsable(True)) + @clr.clrmethod(int, [int]) + def Method1(x): + return x + + X = clr.property(Double, 1.0).add_attribute(DebuggerDisplay("Asd")) + obj = ClassWithAttributes() + tp = obj.GetType() + founddisplay = 0 + foundbrowsable = 0 + for attr in Attribute.GetCustomAttributes(tp): + if isinstance(attr, DebuggerDisplayAttribute): + founddisplay = founddisplay + 1 + if isinstance(attr, BrowsableAttribute): + foundbrowsable = foundbrowsable + 1 + SimpleClass.TestObject(obj) + found_display_on_property = 0 + for attr in Attribute.GetCustomAttributes(tp.GetProperty("X")): + if isinstance(attr, DebuggerDisplayAttribute): + found_display_on_property = found_display_on_property + 1 + found_display_on_method = 0 + for attr in Attribute.GetCustomAttributes(tp.GetMethod("Method1")): + if isinstance(attr, BrowsableAttribute): + found_display_on_method = found_display_on_method + 1 + assert founddisplay == 1 + assert found_display_on_property == 1 + assert found_display_on_method == 1 + assert foundbrowsable == 1 + assert obj.X == 1.0 + SimpleClass.TestObjectProperty(obj, "X", 10.0) +def test_class_with_advanced_attribute(): + import clr + @clr.attribute(TestAttribute(1, 2, z = "A", W = "B")) + class ClassWithAttributes2(ISimpleInterface, SimpleClass): + pass + @clr.attribute(TestAttributeAttribute, 1, 2, z = "A", W = "B") + class ClassWithAttributes3(ISimpleInterface, SimpleClass): + X = clr.property(Double, 1.0).add_attribute(TestAttributeAttribute, 1, 2) + + c = ClassWithAttributes2() + c2 = ClassWithAttributes3() + +def test_subclass_ctor(): + import clr + class SubClass0(SimpleClass): + pass + class SubClass1(SubClass0): + def __init__(self): + super().__init__() + class SubClass2(SubClass1): + __namespace__ = "TestModule" + def __init__(self): + super().__init__() + SimpleClass.TestOnType(SubClass0) + SimpleClass.TestOnType(SubClass1) + SimpleClass.TestOnType(SubClass2) + +def test_more_subclasses(): + import clr + class SubClass0(SimpleClass): + pass + class SubClass1(SubClass0): + X = clr.property(Double, 1.0) + def __init__(self): + super().__init__() + self.Y = 10.0 + SimpleClass.Pause(); + + @clr.attribute(DebuggerDisplay("X")) + + class SubClass2(SubClass1): + __namespace__ = "TestModule" + def __init__(self): + SimpleClass.Pause(); + super().__init__() + def IncrementThing(self): + super().IncrementThing() + return 6; + SimpleClass.TestOnType(SubClass0) + SimpleClass.TestOnType(SubClass1) + SimpleClass.TestOnType(SubClass2) + obj = SimpleClass.InvokeCtor(SubClass2) + + obj2 = SubClass2() + tp = obj.GetType() + obj.X = 5.0 + assert obj.Y == 10.0 + assert obj2.Y == 10.0 + assert obj.Initialized == True + assert obj2.Initialized == True + SimpleClass.Test1(obj) + obj = None + SimpleClass.Test2() + +def abstract_test(): + class abstractClass(SimpleClass): + __clr_abstract__ = True + failed = False + try: + abstractClass() + except: + failed = True + assert failed + def test_construction_from_clr(): import clr calls = [] @@ -317,6 +477,29 @@ class Derived(BaseClass): import gc gc.collect() +def test_more_subclasses2(): + import clr + class SubClass50(SimpleClass): + def __init__(self): + super().__init__() + def IncrementThing(self): + return super().IncrementThing() + + @clr.attribute(DebuggerDisplay("X")) + + class SubClass51(SubClass50): + __namespace__ = "TestModule" + def __init__(self): + super().__init__() + + def IncrementThing(self): + return super().IncrementThing() + 10 + x = SubClass51() + print(x.CallIncrementThing()) + print(x.CallIncrementThing()) + print(x.CallIncrementThing()) + + def test_generic_interface(): from System import Int32