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