From 46de53e22c90fa461e6b14705d62372a61d53b6d Mon Sep 17 00:00:00 2001 From: Rolf Madsen Date: Tue, 29 Nov 2022 15:06:55 +0100 Subject: [PATCH 1/3] 1783 Implement Interface And Inherit Class Added support for multiple inheritance when inheriting from one base class and/or multiple interfaces. Added a unit test verifying that it works with a simple class and interface. This unit test would previously have failed since multiple types are in the class super class list. --- CHANGELOG.md | 2 + src/python_tests_runner/PythonTestRunner.cs | 1 + src/runtime/Runtime.cs | 6 ++ src/runtime/StateSerialization/MaybeType.cs | 4 +- src/runtime/TypeManager.cs | 19 +++--- src/runtime/Types/ClassDerived.cs | 10 +++- src/runtime/Types/MetaType.cs | 64 ++++++++++++--------- src/runtime/Types/ReflectedClrType.cs | 3 +- src/testing/classtest.cs | 12 ++++ tests/test_subclass.py | 9 ++- 10 files changed, 84 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a85e094..818d90c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. - Fixed error occuring when inheriting a class containing a virtual generic method. - Make a second call to `pythonnet.load` a no-op, as it was intended. +- Added support for multiple inheritance when inheriting from a class and/or multiple interfaces. + ## [3.0.1](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.1) - 2022-11-03 ### Added diff --git a/src/python_tests_runner/PythonTestRunner.cs b/src/python_tests_runner/PythonTestRunner.cs index 05298997b..f97cc5aec 100644 --- a/src/python_tests_runner/PythonTestRunner.cs +++ b/src/python_tests_runner/PythonTestRunner.cs @@ -35,6 +35,7 @@ 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_implement_interface_and_class" }; } /// 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..884b7edb0 100644 --- a/src/runtime/StateSerialization/MaybeType.cs +++ b/src/runtime/StateSerialization/MaybeType.cs @@ -15,7 +15,7 @@ internal struct MaybeType : ISerializable const string SerializationName = "n"; readonly string name; readonly Type type; - + public string DeletedMessage { get @@ -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..a23973919 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, IList py_base_type, IList 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 61c602783..592eefd55 100644 --- a/src/runtime/Types/ClassDerived.cs +++ b/src/runtime/Types/ClassDerived.cs @@ -144,6 +144,7 @@ internal static NewReference ToPython(IPythonDerivedType obj) /// internal static Type CreateDerivedType(string name, Type baseType, + IList typeInterfaces, BorrowedReference py_dict, string? namespaceStr, string? assemblyName, @@ -163,7 +164,9 @@ 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 typeInterfaces) + 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. @@ -214,8 +217,9 @@ internal static Type CreateDerivedType(string name, } } - // override any virtual methods not already overridden by the properties above - MethodInfo[] methods = baseType.GetMethods(); + // override any virtual not already overridden by the properties above + // also override any interface method. + var methods = baseType.GetMethods().Concat(interfaces.SelectMany(x => x.GetMethods())); var virtualMethods = new HashSet(); foreach (MethodInfo method in methods) { diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index 5b59f5139..9ff30ed33 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,47 @@ 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. + var interfaces = new List(); + var 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 (!(mt == PyCLRMetaType || mt == Runtime.PyTypeType)) + // if the base type count is 0, there might still be interfaces to implement. + if (baseType.Count == 0) { - return Exceptions.RaiseTypeError("invalid metatype"); + baseType.Add(new ClassBase(typeof(object))); } - // Ensure that the reflected type is appropriate for subclassing, - // disallowing subclassing of delegates, enums and array types. + // Multiple inheritance is not supported, unless the other types are interfaces + if (baseType.Count > 1) + { + return Exceptions.RaiseTypeError("cannot use multiple inheritance with managed classes"); + } - if (GetManagedObject(base_type) is ClassBase cb) + var cb = baseType[0]; + 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,7 +130,8 @@ 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. @@ -130,10 +140,12 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, using var clsDict = new PyDict(dict); if (clsDict.HasKey("__assembly__") || clsDict.HasKey("__namespace__")) { - return TypeManager.CreateSubType(name, base_type, clsDict); + 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/ReflectedClrType.cs b/src/runtime/Types/ReflectedClrType.cs index d3d89bdb8..3d0aa7e99 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, IList 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/classtest.cs b/src/testing/classtest.cs index 68c0d8c55..993afdfc9 100644 --- a/src/testing/classtest.cs +++ b/src/testing/classtest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; namespace Python.Test @@ -59,4 +60,15 @@ public ClassCtorTest2(string v) internal class InternalClass { } + + public class SimpleClass + { + public static void TestObject(object obj) + { + if ((!(obj is ISayHello1 && obj is SimpleClass))) + { + throw new Exception("Expected ISayHello and SimpleClass instance"); + } + } + } } diff --git a/tests/test_subclass.py b/tests/test_subclass.py index 504b82548..c6ab7650f 100644 --- a/tests/test_subclass.py +++ b/tests/test_subclass.py @@ -9,7 +9,7 @@ import System import pytest from Python.Test import (IInterfaceTest, SubClassTest, EventArgsTest, - FunctionsTest, IGenericInterface, GenericVirtualMethodTest) + FunctionsTest, IGenericInterface, GenericVirtualMethodTest, SimpleClass, ISayHello1) from System.Collections.Generic import List @@ -338,4 +338,9 @@ class OverloadingSubclass2(OverloadingSubclass): obj = OverloadingSubclass2() assert obj.VirtMethod[int](5) == 5 - +def test_implement_interface_and_class(): + class DualSubClass0(ISayHello1, SimpleClass): + __namespace__ = "Test" + def SayHello(self): + return "hello" + obj = DualSubClass0() From f92a26b734bb090b98065eb40cf1046bfeaa07b3 Mon Sep 17 00:00:00 2001 From: romadsen-ks Date: Wed, 30 Nov 2022 08:52:44 +0100 Subject: [PATCH 2/3] Added a few cleanups based on feedback. --- src/runtime/Runtime.cs | 6 ------ src/runtime/TypeManager.cs | 6 ++---- src/runtime/Types/MetaType.cs | 31 +++++++++++++++++-------------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index d5558084f..beb577e45 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -1836,12 +1836,6 @@ 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/TypeManager.cs b/src/runtime/TypeManager.cs index a23973919..559d5148e 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, IList py_base_type, IList interfaces, BorrowedReference dictRef) + internal static NewReference CreateSubType(BorrowedReference py_name, ClassBase py_base_type, IList 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,9 +415,7 @@ internal static NewReference CreateSubType(BorrowedReference py_name, IList(); - var baseType = new List(); - var cnt = Runtime.PyTuple_GetSize(bases); + // More than one base type case be declared, but an exception will be thrown + // if more than one is a class/not an interface. + var baseTypes = new List(); - for (uint i = 0; i < cnt; i++) + var baseClassCount = Runtime.PyTuple_Size(bases); + + for (nint i = 0; i < baseClassCount; i++) { - var base_type2 = Runtime.PyTuple_GetItem(bases, (int)i); - var cb2 = (ClassBase) GetManagedObject(base_type2); - if (cb2 != null) + var baseTypeIt = Runtime.PyTuple_GetItem(bases, (int)i); + + if (GetManagedObject(baseTypeIt) is ClassBase classBaseIt) { - if (cb2.type.Valid && cb2.type.Value.IsInterface) - interfaces.Add(cb2.type.Value); - else baseType.Add(cb2); + if (classBaseIt.type.Valid && classBaseIt.type.Value.IsInterface) + interfaces.Add(classBaseIt.type.Value); + else baseTypes.Add(classBaseIt); } } // if the base type count is 0, there might still be interfaces to implement. - if (baseType.Count == 0) + if (baseTypes.Count == 0) { - baseType.Add(new ClassBase(typeof(object))); + baseTypes.Add(new ClassBase(typeof(object))); } // Multiple inheritance is not supported, unless the other types are interfaces - if (baseType.Count > 1) + if (baseTypes.Count > 1) { return Exceptions.RaiseTypeError("cannot use multiple inheritance with managed classes"); } - var cb = baseType[0]; + var cb = baseTypes[0]; try { if (!cb.CanSubclass()) @@ -140,7 +143,7 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, using var clsDict = new PyDict(dict); if (clsDict.HasKey("__assembly__") || clsDict.HasKey("__namespace__")) { - return TypeManager.CreateSubType(name, baseType, interfaces, clsDict); + return TypeManager.CreateSubType(name, baseTypes[0], interfaces, clsDict); } } From 7acbb229e89c16983b60b761dc01e4c25bd17807 Mon Sep 17 00:00:00 2001 From: Rolf Madsen Date: Thu, 2 Feb 2023 21:41:28 +0100 Subject: [PATCH 3/3] 1783 Implement Interface And Inherit Class: Cleanup - Added exceptions during unintended use of superclasses. - Removed invalid comment. - Improved exceptions from MetaType slightly. --- src/runtime/Types/MetaType.cs | 39 ++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index 2e61b6808..57fcaa232 100644 --- a/src/runtime/Types/MetaType.cs +++ b/src/runtime/Types/MetaType.cs @@ -90,6 +90,10 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, var baseTypes = new List(); var baseClassCount = Runtime.PyTuple_Size(bases); + if (baseClassCount == 0) + { + return Exceptions.RaiseTypeError("zero base classes "); + } for (nint i = 0; i < baseClassCount; i++) { @@ -97,9 +101,22 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, if (GetManagedObject(baseTypeIt) is ClassBase classBaseIt) { - if (classBaseIt.type.Valid && classBaseIt.type.Value.IsInterface) + if (!classBaseIt.type.Valid) + { + return Exceptions.RaiseTypeError("Invalid type used as a super type."); + } + if (classBaseIt.type.Value.IsInterface) + { interfaces.Add(classBaseIt.type.Value); - else baseTypes.Add(classBaseIt); + } + else + { + baseTypes.Add(classBaseIt); + } + } + else + { + return Exceptions.RaiseTypeError("Non .NET type used as super class for meta type. This is not supported."); } } // if the base type count is 0, there might still be interfaces to implement. @@ -111,7 +128,20 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, // Multiple inheritance is not supported, unless the other types are interfaces if (baseTypes.Count > 1) { - return Exceptions.RaiseTypeError("cannot use multiple inheritance with managed classes"); + var types = string.Join(", ", baseTypes.Select(baseType => baseType.type.Value)); + return Exceptions.RaiseTypeError($"Multiple inheritance with managed classes cannot be used. Types: {types} "); + } + + // check if the list of interfaces contains no duplicates. + if (interfaces.Distinct().Count() != interfaces.Count) + { + // generate a string containing the problematic types. + var duplicateTypes = interfaces.GroupBy(type => type) + .Where(typeGroup => typeGroup.Count() > 1) + .Select(typeGroup => typeGroup.Key); + var duplicateTypesString = string.Join(", ", duplicateTypes); + + return Exceptions.RaiseTypeError($"An interface can only be implemented once. Duplicate types: {duplicateTypesString}"); } var cb = baseTypes[0]; @@ -133,8 +163,7 @@ public static NewReference tp_new(BorrowedReference tp, BorrowedReference args, return Exceptions.RaiseTypeError("subclasses of managed classes do not support __slots__"); } - // If the base class has a parameterless constructor, or - // if __assembly__ or __namespace__ are in the class dictionary then create + // 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.