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/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..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, BorrowedReference py_base_type, 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,17 +415,10 @@ 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"); - } + return ReflectedClrType.CreateSubclass(py_base_type, 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..57fcaa232 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,80 @@ 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(); - if (Runtime.PyTuple_Size(bases) != 1) - { - return Exceptions.RaiseTypeError("cannot use multiple inheritance with managed classes"); - } + // 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(); - BorrowedReference base_type = Runtime.PyTuple_GetItem(bases, 0); - BorrowedReference mt = Runtime.PyObject_TYPE(base_type); - - if (!(mt == PyCLRMetaType || mt == Runtime.PyTypeType)) + var baseClassCount = Runtime.PyTuple_Size(bases); + if (baseClassCount == 0) { - return Exceptions.RaiseTypeError("invalid metatype"); + return Exceptions.RaiseTypeError("zero base 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) + for (nint i = 0; i < baseClassCount; i++) { - try + var baseTypeIt = Runtime.PyTuple_GetItem(bases, (int)i); + + if (GetManagedObject(baseTypeIt) is ClassBase classBaseIt) { - if (!cb.CanSubclass()) + if (!classBaseIt.type.Valid) + { + return Exceptions.RaiseTypeError("Invalid type used as a super type."); + } + if (classBaseIt.type.Value.IsInterface) { - return Exceptions.RaiseTypeError("delegates, enums and array types cannot be subclassed"); + interfaces.Add(classBaseIt.type.Value); } + else + { + baseTypes.Add(classBaseIt); + } + } + else + { + return Exceptions.RaiseTypeError("Non .NET type used as super class for meta type. This is not supported."); } - catch (SerializationException) + } + // if the base type count is 0, there might still be interfaces to implement. + if (baseTypes.Count == 0) + { + baseTypes.Add(new ClassBase(typeof(object))); + } + + // Multiple inheritance is not supported, unless the other types are interfaces + if (baseTypes.Count > 1) + { + 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]; + try + { + 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) @@ -130,10 +172,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, baseTypes[0], 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()