diff --git a/CHANGELOG.md b/CHANGELOG.md index a545f335c..7a753d1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Added - Added automatic NuGet package generation in appveyor and local builds +- Added IGetAttr and ISetAttr, so that .NET classes could override __getattr__ and __setattr__ ### Changed diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index faa55fa27..d351709a4 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -89,6 +89,7 @@ + diff --git a/src/embed_tests/TestInstanceWrapping.cs b/src/embed_tests/TestInstanceWrapping.cs new file mode 100644 index 000000000..86cf2f91e --- /dev/null +++ b/src/embed_tests/TestInstanceWrapping.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using Python.Runtime; +using Python.Runtime.Slots; + +namespace Python.EmbeddingTest { + public class TestInstanceWrapping { + [Test] + public void GetAttrCanBeOverriden() { + var overloaded = new Overloaded(); + using (Py.GIL()) { + var o = overloaded.ToPython(); + dynamic getNonexistingAttr = PythonEngine.Eval("lambda o: o.non_existing_attr"); + string nonexistentAttrValue = getNonexistingAttr(o); + Assert.AreEqual(GetAttrFallbackValue, nonexistentAttrValue); + } + } + + [Test] + public void SetAttrCanBeOverriden() { + var overloaded = new Overloaded(); + using (Py.GIL()) + using (var scope = Py.CreateScope()) { + var o = overloaded.ToPython(); + scope.Set(nameof(o), o); + scope.Exec($"{nameof(o)}.non_existing_attr = 42"); + Assert.AreEqual(42, overloaded.Value); + } + } + + const string GetAttrFallbackValue = "undefined"; + + class Base {} + class Derived: Base { } + + class Overloaded: Derived, IGetAttr, ISetAttr + { + public int Value { get; private set; } + + public bool TryGetAttr(string name, out PyObject value) { + value = GetAttrFallbackValue.ToPython(); + return true; + } + + public bool TrySetAttr(string name, PyObject value) { + this.Value = value.As(); + return true; + } + } + } +} diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index ac6b59150..983a707af 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -22,11 +22,11 @@ - PYTHON2;PYTHON27;UCS4 @@ -34,7 +34,7 @@ pdbonly - PYTHON3;PYTHON37;UCS4 + PYTHON3;PYTHON37;UCS4 true pdbonly @@ -46,7 +46,7 @@ true - PYTHON3;PYTHON37;UCS4;TRACE;DEBUG + PYTHON3;PYTHON37;UCS4;TRACE;DEBUG false full @@ -56,7 +56,7 @@ pdbonly - PYTHON3;PYTHON37;UCS2 + PYTHON3;PYTHON37;UCS2 true pdbonly @@ -68,7 +68,7 @@ true - PYTHON3;PYTHON37;UCS2;TRACE;DEBUG + PYTHON3;PYTHON37;UCS2;TRACE;DEBUG false full @@ -137,11 +137,12 @@ + - - + + @@ -151,7 +152,7 @@ - + @@ -170,4 +171,4 @@ - + diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index 75f11492f..3003f33df 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -103,7 +103,7 @@ public class Runtime internal static object IsFinalizingLock = new object(); internal static bool IsFinalizing; - internal static bool Is32Bit = IntPtr.Size == 4; + internal static readonly bool Is32Bit = IntPtr.Size == 4; // .NET core: System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows) internal static bool IsWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; @@ -562,6 +562,14 @@ internal static unsafe void XIncref(IntPtr op) #endif } + /// + /// Increase Python's ref counter for the given object, and return the object back. + /// + internal static IntPtr SelfIncRef(IntPtr op) { + XIncref(op); + return op; + } + internal static unsafe void XDecref(IntPtr op) { #if PYTHON_WITH_PYDEBUG || NETSTANDARD diff --git a/src/runtime/slots.cs b/src/runtime/slots.cs new file mode 100644 index 000000000..85e35c299 --- /dev/null +++ b/src/runtime/slots.cs @@ -0,0 +1,47 @@ +using System; + +namespace Python.Runtime.Slots +{ + /// + /// Implement this interface to override Python's __getattr__ for your class + /// + public interface IGetAttr { + bool TryGetAttr(string name, out PyObject value); + } + + /// + /// Implement this interface to override Python's __setattr__ for your class + /// + public interface ISetAttr { + bool TrySetAttr(string name, PyObject value); + } + + static class SlotOverrides { + public static IntPtr tp_getattro(IntPtr ob, IntPtr key) { + IntPtr genericResult = Runtime.PyObject_GenericGetAttr(ob, key); + if (genericResult != IntPtr.Zero || !Runtime.PyString_Check(key)) { + return genericResult; + } + + Exceptions.Clear(); + + var self = (IGetAttr)((CLRObject)ManagedType.GetManagedObject(ob)).inst; + string attr = Runtime.GetManagedString(key); + return self.TryGetAttr(attr, out var value) + ? Runtime.SelfIncRef(value.Handle) + : Runtime.PyObject_GenericGetAttr(ob, key); + } + + public static int tp_setattro(IntPtr ob, IntPtr key, IntPtr val) { + if (!Runtime.PyString_Check(key)) { + return Runtime.PyObject_GenericSetAttr(ob, key, val); + } + + var self = (ISetAttr)((CLRObject)ManagedType.GetManagedObject(ob)).inst; + string attr = Runtime.GetManagedString(key); + return self.TrySetAttr(attr, new PyObject(Runtime.SelfIncRef(val))) + ? 0 + : Runtime.PyObject_GenericSetAttr(ob, key, val); + } + } +} diff --git a/src/runtime/typemanager.cs b/src/runtime/typemanager.cs index 9a98e9ebb..80032c686 100644 --- a/src/runtime/typemanager.cs +++ b/src/runtime/typemanager.cs @@ -4,10 +4,10 @@ using System.Reflection; using System.Runtime.InteropServices; using Python.Runtime.Platform; +using Python.Runtime.Slots; namespace Python.Runtime { - /// /// The TypeManager class is responsible for building binary-compatible /// Python type objects that are implemented in managed code. @@ -155,6 +155,14 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType) InitializeSlots(type, impl.GetType()); + if (typeof(IGetAttr).IsAssignableFrom(clrType)) { + InitializeSlot(type, TypeOffset.tp_getattro, typeof(SlotOverrides).GetMethod(nameof(SlotOverrides.tp_getattro))); + } + + if (typeof(ISetAttr).IsAssignableFrom(clrType)) { + InitializeSlot(type, TypeOffset.tp_setattro, typeof(SlotOverrides).GetMethod(nameof(SlotOverrides.tp_setattro))); + } + if (base_ != IntPtr.Zero) { Marshal.WriteIntPtr(type, TypeOffset.tp_base, base_); @@ -766,6 +774,11 @@ static void InitializeSlot(IntPtr type, IntPtr slot, string name) Marshal.WriteIntPtr(type, offset, slot); } + static void InitializeSlot(IntPtr type, int slotOffset, MethodInfo method) { + IntPtr thunk = Interop.GetThunk(method); + Marshal.WriteIntPtr(type, slotOffset, thunk); + } + /// /// Given a newly allocated Python type object and a managed Type that /// implements it, initialize any methods defined by the Type that need