From 1b177434f9f5818d590b5067435126bde3dfee76 Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Tue, 25 Jun 2019 19:07:28 -0700 Subject: [PATCH 1/2] allow .NET classes to override __getattr__ and __setattr__ --- CHANGELOG.md | 1 + src/embed_tests/Python.EmbeddingTest.csproj | 1 + src/embed_tests/TestInstanceWrapping.cs | 60 +++++++++++++++++++++ src/runtime/Python.Runtime.csproj | 27 +++++----- src/runtime/runtime.cs | 8 +++ src/runtime/slots.cs | 47 ++++++++++++++++ src/runtime/typemanager.cs | 15 +++++- 7 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 src/embed_tests/TestInstanceWrapping.cs create mode 100644 src/runtime/slots.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 941045aef..72ab7f3fa 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..4bcea8770 --- /dev/null +++ b/src/embed_tests/TestInstanceWrapping.cs @@ -0,0 +1,60 @@ +using NUnit.Framework; +using Python.Runtime; +using Python.Runtime.Slots; + +namespace Python.EmbeddingTest { + public class TestInstanceWrapping { + [OneTimeSetUp] + public void SetUp() { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() { + PythonEngine.Shutdown(); + } + + [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..b9063c576 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -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 From 5e276d973b6a57f947336d2634f8c069e2868328 Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Fri, 23 Aug 2019 13:26:43 -0700 Subject: [PATCH 2/2] removed initialization from TestInstanceWrapping, as it partaken in tests crashing under x86 (opened bug #946) --- src/embed_tests/TestInstanceWrapping.cs | 10 ---------- src/runtime/runtime.cs | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/embed_tests/TestInstanceWrapping.cs b/src/embed_tests/TestInstanceWrapping.cs index 4bcea8770..86cf2f91e 100644 --- a/src/embed_tests/TestInstanceWrapping.cs +++ b/src/embed_tests/TestInstanceWrapping.cs @@ -4,16 +4,6 @@ namespace Python.EmbeddingTest { public class TestInstanceWrapping { - [OneTimeSetUp] - public void SetUp() { - PythonEngine.Initialize(); - } - - [OneTimeTearDown] - public void Dispose() { - PythonEngine.Shutdown(); - } - [Test] public void GetAttrCanBeOverriden() { var overloaded = new Overloaded(); diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index b9063c576..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;