diff --git a/README.rst b/README.rst index c381228a..bebf53fb 100644 --- a/README.rst +++ b/README.rst @@ -5,5 +5,5 @@ We will try to keep this up-to-date with pythonnet and upstream changes that mig Changes relative to pythonnet: * Revert of `#1240 `_. -* Enum REPR `#2239 `_ is included in this release of version 3.0.2, but is unreleased in pythonnet * Opt-into explicit interface wrapping, `#19 `_. This opts into the behavior that became the default in #1240 if ToPythonAs is explicitly used +* Option to bind explicit interface implementations, `#23 `_. This provides a runtime option to expose C# explicit interface implementations to Python. diff --git a/src/runtime/BindingOptions.cs b/src/runtime/BindingOptions.cs new file mode 100644 index 00000000..474128e8 --- /dev/null +++ b/src/runtime/BindingOptions.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Python.Runtime +{ + public class BindingOptions + { + private bool _SuppressDocs = false; + private bool _SuppressOverloads = false; + private bool _AllowExplicitInterfaceImplementation = false; + + //[ModuleProperty] + public bool SuppressDocs + { + get { return _SuppressDocs; } + set { _SuppressDocs = value; } + } + + //[ModuleProperty] + public bool SuppressOverloads + { + get { return _SuppressOverloads; } + set { _SuppressOverloads = value; } + } + + public bool AllowExplicitInterfaceImplementation + { + get { return _AllowExplicitInterfaceImplementation; } + set { _AllowExplicitInterfaceImplementation = value; } + } + } + + public class BindingManager + { + static IDictionary _typeOverrides = new Dictionary(); + static IDictionary _assemblyOverrides = new Dictionary(); + static BindingOptions _defaultBindingOptions = new BindingOptions(); + + public static BindingOptions GetBindingOptions(Type type) + { + if (_typeOverrides.ContainsKey(type)) + { + return _typeOverrides[type]; + } + + if (_assemblyOverrides.ContainsKey(type.Assembly)) + { + return _assemblyOverrides[type.Assembly]; + } + return _defaultBindingOptions; + } + + public static BindingOptions DefaultBindingOptions => _defaultBindingOptions; + + public static void SetBindingOptions(Type type, BindingOptions options) + { + _typeOverrides[type] = options; + } + + public static void SetBindingOptions(Assembly assembly, BindingOptions options) + { + _assemblyOverrides[assembly] = options; + } + + public static void Clear() + { + _typeOverrides.Clear(); + _assemblyOverrides.Clear(); + } + } +} diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index d743bc00..59624664 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -210,7 +210,9 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p // information, including generating the member descriptors // that we'll be putting in the Python class __dict__. - ClassInfo info = GetClassInfo(type, impl); + var bindingOptions = BindingManager.GetBindingOptions(type); + + ClassInfo info = GetClassInfo(type, impl, bindingOptions); impl.indexer = info.indexer; impl.richcompare.Clear(); @@ -254,7 +256,7 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p if (co.NumCtors > 0 && !co.HasCustomNew()) { // Implement Overloads on the class object - if (!CLRModule._SuppressOverloads) + if (!bindingOptions.SuppressOverloads) { // HACK: __init__ points to instance constructors. // When unbound they fully instantiate object, so we get overloads for free from MethodBinding. @@ -265,7 +267,7 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p } // don't generate the docstring if one was already set from a DocStringAttribute. - if (!CLRModule._SuppressDocs && doc.IsNull()) + if (!bindingOptions.SuppressDocs && doc.IsNull()) { doc = co.GetDocString(); Runtime.PyDict_SetItem(dict, PyIdentifier.__doc__, doc.Borrow()); @@ -288,10 +290,20 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p Runtime.PyType_Modified(pyType.Reference); } - internal static bool ShouldBindMethod(MethodBase mb) + internal static bool ShouldBindMethod(MethodBase mb, BindingOptions bindingOptions) { if (mb is null) throw new ArgumentNullException(nameof(mb)); - return (mb.IsPublic || mb.IsFamily || mb.IsFamilyOrAssembly); + + bool rejectByVisibility = false; + if (!mb.IsPublic) + { + if (bindingOptions.AllowExplicitInterfaceImplementation) + // detect explicit interface implementation + rejectByVisibility = !mb.IsVirtual; + else + rejectByVisibility = true; + } + return (!rejectByVisibility || mb.IsFamily || mb.IsFamilyOrAssembly); } internal static bool ShouldBindField(FieldInfo fi) @@ -300,7 +312,7 @@ internal static bool ShouldBindField(FieldInfo fi) return (fi.IsPublic || fi.IsFamily || fi.IsFamilyOrAssembly); } - internal static bool ShouldBindProperty(PropertyInfo pi) + internal static bool ShouldBindProperty(PropertyInfo pi, BindingOptions bindingOptions) { MethodInfo? mm; try @@ -323,16 +335,27 @@ internal static bool ShouldBindProperty(PropertyInfo pi) return false; } - return ShouldBindMethod(mm); + return ShouldBindMethod(mm, bindingOptions); } - internal static bool ShouldBindEvent(EventInfo ei) + internal static bool ShouldBindEvent(EventInfo ei, BindingOptions bindingOptions) { - return ei.GetAddMethod(true) is { } add && ShouldBindMethod(add); + return ei.GetAddMethod(true) is { } add && ShouldBindMethod(add, bindingOptions); } - private static ClassInfo GetClassInfo(Type type, ClassBase impl) + private static string SanitizeName(string name) { + // For explicit interface implementation, the name of the attribute will be: + // `Class.Interface.Attribute` + // In that case, only use the last token for the real attribute name to bind. + if (name.Contains(".")) + name = name.Substring(name.LastIndexOf(".")+1); + return name; + } + + private static ClassInfo GetClassInfo(Type type, ClassBase impl, BindingOptions bindingOptions) + { + var typeName = type.Name; var ci = new ClassInfo(); var methods = new Dictionary>(); MethodInfo meth; @@ -436,11 +459,12 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) { case MemberTypes.Method: meth = (MethodInfo)mi; - if (!ShouldBindMethod(meth)) + if (!ShouldBindMethod(meth, bindingOptions)) { continue; } - name = meth.Name; + + name = SanitizeName(meth.Name); //TODO mangle? if (name == "__init__" && !impl.HasCustomNew()) @@ -471,7 +495,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) case MemberTypes.Property: var pi = (PropertyInfo)mi; - if(!ShouldBindProperty(pi)) + if(!ShouldBindProperty(pi, bindingOptions)) { continue; } @@ -490,8 +514,9 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) continue; } + var propertyName = SanitizeName(pi.Name); ob = new PropertyObject(pi); - ci.members[pi.Name] = ob.AllocObject(); + ci.members[propertyName] = ob.AllocObject(); continue; case MemberTypes.Field: @@ -506,7 +531,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) case MemberTypes.Event: var ei = (EventInfo)mi; - if (!ShouldBindEvent(ei)) + if (!ShouldBindEvent(ei, bindingOptions)) { continue; } diff --git a/src/runtime/StateSerialization/MaybeMemberInfo.cs b/src/runtime/StateSerialization/MaybeMemberInfo.cs index b734bb07..728f580b 100644 --- a/src/runtime/StateSerialization/MaybeMemberInfo.cs +++ b/src/runtime/StateSerialization/MaybeMemberInfo.cs @@ -64,9 +64,10 @@ internal MaybeMemberInfo(SerializationInfo serializationInfo, StreamingContext c var tp = Type.GetType(serializationInfo.GetString(SerializationType)); if (tp != null) { + var bindingOptions = BindingManager.GetBindingOptions(tp); var memberName = serializationInfo.GetString(SerializationMemberName); MemberInfo? mi = Get(tp, memberName, ClassManager.BindingFlags); - if (mi != null && ShouldBindMember(mi)) + if (mi != null && ShouldBindMember(mi, bindingOptions)) { info = mi; } @@ -92,11 +93,11 @@ internal MaybeMemberInfo(SerializationInfo serializationInfo, StreamingContext c // based on it's setter/getter (which is a method // info) visibility and events based on their // AddMethod visibility. - static bool ShouldBindMember(MemberInfo mi) + static bool ShouldBindMember(MemberInfo mi, BindingOptions bindingOptions) { if (mi is PropertyInfo pi) { - return ClassManager.ShouldBindProperty(pi); + return ClassManager.ShouldBindProperty(pi, bindingOptions); } else if (mi is FieldInfo fi) { @@ -104,7 +105,7 @@ static bool ShouldBindMember(MemberInfo mi) } else if (mi is EventInfo ei) { - return ClassManager.ShouldBindEvent(ei); + return ClassManager.ShouldBindEvent(ei, bindingOptions); } return false; diff --git a/src/runtime/StateSerialization/MaybeMethodBase.cs b/src/runtime/StateSerialization/MaybeMethodBase.cs index d196d5e8..f1283c5f 100644 --- a/src/runtime/StateSerialization/MaybeMethodBase.cs +++ b/src/runtime/StateSerialization/MaybeMethodBase.cs @@ -117,9 +117,11 @@ static MethodBase ScanForMethod(Type declaringType, string name, int genericCoun var visibility = flags & MaybeMethodFlags.Visibility; + var bindingOptions = BindingManager.GetBindingOptions(declaringType); + var result = alternatives.Cast().FirstOrDefault(m => MatchesGenericCount(m, genericCount) && MatchesSignature(m, parameters) - && (Visibility(m) == visibility || ClassManager.ShouldBindMethod(m))); + && (Visibility(m) == visibility || ClassManager.ShouldBindMethod(m, bindingOptions))); if (result is null) throw new MissingMethodException($"Matching overload not found for {declaringType}.{name}"); diff --git a/src/runtime/Types/ClrModule.cs b/src/runtime/Types/ClrModule.cs index b7d10260..c9915278 100644 --- a/src/runtime/Types/ClrModule.cs +++ b/src/runtime/Types/ClrModule.cs @@ -16,7 +16,6 @@ internal class CLRModule : ModuleObject protected static bool interactive_preload = true; internal static bool preload; // XXX Test performance of new features // - internal static bool _SuppressDocs = false; internal static bool _SuppressOverloads = false; static CLRModule() @@ -39,10 +38,6 @@ public static void Reset() { interactive_preload = true; preload = false; - - // XXX Test performance of new features // - _SuppressDocs = false; - _SuppressOverloads = false; } /// @@ -82,15 +77,15 @@ public static void setPreload(bool preloadFlag) //[ModuleProperty] public static bool SuppressDocs { - get { return _SuppressDocs; } - set { _SuppressDocs = value; } + get { return BindingManager.DefaultBindingOptions.SuppressDocs; } + set { BindingManager.DefaultBindingOptions.SuppressDocs = value; } } //[ModuleProperty] public static bool SuppressOverloads { - get { return _SuppressOverloads; } - set { _SuppressOverloads = value; } + get { return BindingManager.DefaultBindingOptions.SuppressOverloads; } + set { BindingManager.DefaultBindingOptions.SuppressOverloads = value; } } [ModuleFunction] diff --git a/src/testing/propertytest.cs b/src/testing/propertytest.cs index f54fd8bb..cdd94780 100644 --- a/src/testing/propertytest.cs +++ b/src/testing/propertytest.cs @@ -1,5 +1,9 @@ namespace Python.Test { + public interface IInherited { + int InheritedProperty { get; } + } + /// /// Supports units tests for property access. /// @@ -80,5 +84,17 @@ public ShortEnum EnumProperty get { return _enum_property; } set { _enum_property = value; } } + + public int InheritedProperty => 1; + } + + public class PropertyTest2 : IInherited + { + int IInherited.InheritedProperty => 2; + } + + public class PropertyTest3 : IInherited + { + int IInherited.InheritedProperty => 3; } } diff --git a/tests/conftest.py b/tests/conftest.py index 1ac20e1d..7226dd98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -87,7 +87,17 @@ def pytest_configure(config): import clr sys.path.append(str(bin_path)) - clr.AddReference("Python.Test") + python_test_module = clr.AddReference("Python.Test") + configure_custom_binding_options(python_test_module) + + +def configure_custom_binding_options(python_test_module): + from Python.Runtime import BindingManager, BindingOptions + binding_options = BindingOptions() + binding_options.AllowExplicitInterfaceImplementation = True + prop_test_3_type = [t for t in python_test_module.GetTypes() if "PropertyTest3" == t.Name][0] + + BindingManager.SetBindingOptions(prop_test_3_type, binding_options) def pytest_unconfigure(config): diff --git a/tests/test_property.py b/tests/test_property.py index 4dc8ea11..58e3664d 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -3,8 +3,8 @@ """Test CLR property support.""" import pytest -from Python.Test import PropertyTest +from Python.Test import PropertyTest, PropertyTest2, PropertyTest3 def test_public_instance_property(): """Test public instance properties.""" @@ -93,6 +93,15 @@ def test_private_property(): with pytest.raises(AttributeError): _ = PropertyTest.PrivateStaticProperty +def test_inherited_property(): + """Test inherited properties.""" + + assert PropertyTest().InheritedProperty == 1 + with pytest.raises(AttributeError): + _ = PropertyTest2().InheritedProperty + + assert PropertyTest3().InheritedProperty == 3 + def test_property_descriptor_get_set(): """Test property descriptor get / set."""