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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy