Skip to content

Commit 41dbcb7

Browse files
committed
allow .NET classes to override __getattr__ and __setattr__
1 parent df0574d commit 41dbcb7

File tree

7 files changed

+145
-14
lines changed

7 files changed

+145
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1010
### Added
1111

1212
- Added automatic NuGet package generation in appveyor and local builds
13+
- Added IGetAttr and ISetAttr, so that .NET classes could override __getattr__ and __setattr__
1314

1415
### Changed
1516

src/embed_tests/Python.EmbeddingTest.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<Compile Include="TestDomainReload.cs" />
9090
<Compile Include="TestExample.cs" />
9191
<Compile Include="TestFinalizer.cs" />
92+
<Compile Include="TestInstanceWrapping.cs" />
9293
<Compile Include="TestPyAnsiString.cs" />
9394
<Compile Include="TestPyFloat.cs" />
9495
<Compile Include="TestPyInt.cs" />
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using NUnit.Framework;
2+
using Python.Runtime;
3+
using Python.Runtime.Slots;
4+
5+
namespace Python.EmbeddingTest {
6+
public class TestInstanceWrapping {
7+
[OneTimeSetUp]
8+
public void SetUp() {
9+
PythonEngine.Initialize();
10+
}
11+
12+
[OneTimeTearDown]
13+
public void Dispose() {
14+
PythonEngine.Shutdown();
15+
}
16+
17+
[Test]
18+
public void GetAttrCanBeOverriden() {
19+
var overloaded = new Overloaded();
20+
using (Py.GIL()) {
21+
var o = overloaded.ToPython();
22+
dynamic getNonexistingAttr = PythonEngine.Eval("lambda o: o.non_existing_attr");
23+
string nonexistentAttrValue = getNonexistingAttr(o);
24+
Assert.AreEqual(GetAttrFallbackValue, nonexistentAttrValue);
25+
}
26+
}
27+
28+
[Test]
29+
public void SetAttrCanBeOverriden() {
30+
var overloaded = new Overloaded();
31+
using (Py.GIL())
32+
using (var scope = Py.CreateScope()) {
33+
var o = overloaded.ToPython();
34+
scope.Set(nameof(o), o);
35+
scope.Exec($"{nameof(o)}.non_existing_attr = 42");
36+
Assert.AreEqual(42, overloaded.Value);
37+
}
38+
}
39+
40+
const string GetAttrFallbackValue = "undefined";
41+
42+
class Base {}
43+
class Derived: Base { }
44+
45+
class Overloaded: Derived, IGetAttr, ISetAttr
46+
{
47+
public int Value { get; private set; }
48+
49+
public bool TryGetAttr(string name, out PyObject value) {
50+
value = GetAttrFallbackValue.ToPython();
51+
return true;
52+
}
53+
54+
public bool TrySetAttr(string name, PyObject value) {
55+
this.Value = value.As<int>();
56+
return true;
57+
}
58+
}
59+
}
60+
}

src/runtime/Python.Runtime.csproj

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@
2222
</PropertyGroup>
2323
<!--We can relax binding to platform because code references no any platform dependent assemblies-->
2424
<!--This will allows to use any build of this assebly as a compile ref assebly-->
25-
<!--<PropertyGroup Condition=" '$(Platform)' == 'x86'">
26-
<PlatformTarget>x86</PlatformTarget>
27-
</PropertyGroup>
28-
<PropertyGroup Condition=" '$(Platform)' == 'x64'">
29-
<PlatformTarget>x64</PlatformTarget>
25+
<!--<PropertyGroup Condition=" '$(Platform)' == 'x86'">
26+
<PlatformTarget>x86</PlatformTarget>
27+
</PropertyGroup>
28+
<PropertyGroup Condition=" '$(Platform)' == 'x64'">
29+
<PlatformTarget>x64</PlatformTarget>
3030
</PropertyGroup>-->
3131
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseMono'">
3232
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON2;PYTHON27;UCS4</DefineConstants>
3333
<Optimize>true</Optimize>
3434
<DebugType>pdbonly</DebugType>
3535
</PropertyGroup>
3636
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseMonoPY3'">
37-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS4</DefineConstants>
37+
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS4</DefineConstants>
3838
<Optimize>true</Optimize>
3939
<DebugType>pdbonly</DebugType>
4040
</PropertyGroup>
@@ -46,7 +46,7 @@
4646
</PropertyGroup>
4747
<PropertyGroup Condition=" '$(Configuration)' == 'DebugMonoPY3'">
4848
<DebugSymbols>true</DebugSymbols>
49-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS4;TRACE;DEBUG</DefineConstants>
49+
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS4;TRACE;DEBUG</DefineConstants>
5050
<Optimize>false</Optimize>
5151
<DebugType>full</DebugType>
5252
</PropertyGroup>
@@ -56,7 +56,7 @@
5656
<DebugType>pdbonly</DebugType>
5757
</PropertyGroup>
5858
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseWinPY3'">
59-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS2</DefineConstants>
59+
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS2</DefineConstants>
6060
<Optimize>true</Optimize>
6161
<DebugType>pdbonly</DebugType>
6262
</PropertyGroup>
@@ -68,7 +68,7 @@
6868
</PropertyGroup>
6969
<PropertyGroup Condition=" '$(Configuration)' == 'DebugWinPY3'">
7070
<DebugSymbols>true</DebugSymbols>
71-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS2;TRACE;DEBUG</DefineConstants>
71+
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON37;UCS2;TRACE;DEBUG</DefineConstants>
7272
<Optimize>false</Optimize>
7373
<DebugType>full</DebugType>
7474
</PropertyGroup>
@@ -137,11 +137,12 @@
137137
<Compile Include="pythonexception.cs" />
138138
<Compile Include="pytuple.cs" />
139139
<Compile Include="runtime.cs" />
140+
<Compile Include="slots.cs" />
140141
<Compile Include="typemanager.cs" />
141142
<Compile Include="typemethod.cs" />
142143
<Compile Include="Util.cs" />
143-
<Compile Include="platform\Types.cs" />
144-
<Compile Include="platform\LibraryLoader.cs" />
144+
<Compile Include="platform\Types.cs" />
145+
<Compile Include="platform\LibraryLoader.cs" />
145146
</ItemGroup>
146147
<ItemGroup Condition=" '$(PythonInteropFile)' != '' ">
147148
<Compile Include="$(PythonInteropFile)" />
@@ -151,7 +152,7 @@
151152
<Compile Include="interop34.cs" />
152153
<Compile Include="interop35.cs" />
153154
<Compile Include="interop36.cs" />
154-
<Compile Include="interop37.cs" />
155+
<Compile Include="interop37.cs" />
155156
</ItemGroup>
156157
<ItemGroup>
157158
<None Include="..\pythonnet.snk" />
@@ -170,4 +171,4 @@
170171
<Copy SourceFiles="$(TargetAssembly)" DestinationFolder="$(PythonBuildDir)" />
171172
<!--Copy SourceFiles="$(TargetAssemblyPdb)" Condition="Exists('$(TargetAssemblyPdb)')" DestinationFolder="$(PythonBuildDir)" /-->
172173
</Target>
173-
</Project>
174+
</Project>

src/runtime/runtime.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,14 @@ internal static unsafe void XIncref(IntPtr op)
562562
#endif
563563
}
564564

565+
/// <summary>
566+
/// Increase Python's ref counter for the given object, and return the object back.
567+
/// </summary>
568+
internal static IntPtr SelfIncRef(IntPtr op) {
569+
XIncref(op);
570+
return op;
571+
}
572+
565573
internal static unsafe void XDecref(IntPtr op)
566574
{
567575
#if PYTHON_WITH_PYDEBUG || NETSTANDARD

src/runtime/slots.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
3+
namespace Python.Runtime.Slots
4+
{
5+
/// <summary>
6+
/// Implement this interface to override Python's __getattr__ for your class
7+
/// </summary>
8+
public interface IGetAttr {
9+
bool TryGetAttr(string name, out PyObject value);
10+
}
11+
12+
/// <summary>
13+
/// Implement this interface to override Python's __setattr__ for your class
14+
/// </summary>
15+
public interface ISetAttr {
16+
bool TrySetAttr(string name, PyObject value);
17+
}
18+
19+
static class SlotOverrides {
20+
public static IntPtr tp_getattro(IntPtr ob, IntPtr key) {
21+
IntPtr genericResult = Runtime.PyObject_GenericGetAttr(ob, key);
22+
if (genericResult != IntPtr.Zero || !Runtime.PyString_Check(key)) {
23+
return genericResult;
24+
}
25+
26+
Exceptions.Clear();
27+
28+
var self = (IGetAttr)((CLRObject)ManagedType.GetManagedObject(ob)).inst;
29+
string attr = Runtime.GetManagedString(key);
30+
return self.TryGetAttr(attr, out var value)
31+
? Runtime.SelfIncRef(value.Handle)
32+
: Runtime.PyObject_GenericGetAttr(ob, key);
33+
}
34+
35+
public static int tp_setattro(IntPtr ob, IntPtr key, IntPtr val) {
36+
if (!Runtime.PyString_Check(key)) {
37+
return Runtime.PyObject_GenericSetAttr(ob, key, val);
38+
}
39+
40+
var self = (ISetAttr)((CLRObject)ManagedType.GetManagedObject(ob)).inst;
41+
string attr = Runtime.GetManagedString(key);
42+
return self.TrySetAttr(attr, new PyObject(Runtime.SelfIncRef(val)))
43+
? 0
44+
: Runtime.PyObject_GenericSetAttr(ob, key, val);
45+
}
46+
}
47+
}

src/runtime/typemanager.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
using System.Reflection;
55
using System.Runtime.InteropServices;
66
using Python.Runtime.Platform;
7+
using Python.Runtime.Slots;
78

89
namespace Python.Runtime
910
{
10-
1111
/// <summary>
1212
/// The TypeManager class is responsible for building binary-compatible
1313
/// Python type objects that are implemented in managed code.
@@ -155,6 +155,14 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType)
155155

156156
InitializeSlots(type, impl.GetType());
157157

158+
if (typeof(IGetAttr).IsAssignableFrom(clrType)) {
159+
InitializeSlot(type, TypeOffset.tp_getattro, typeof(SlotOverrides).GetMethod(nameof(SlotOverrides.tp_getattro)));
160+
}
161+
162+
if (typeof(ISetAttr).IsAssignableFrom(clrType)) {
163+
InitializeSlot(type, TypeOffset.tp_setattro, typeof(SlotOverrides).GetMethod(nameof(SlotOverrides.tp_setattro)));
164+
}
165+
158166
if (base_ != IntPtr.Zero)
159167
{
160168
Marshal.WriteIntPtr(type, TypeOffset.tp_base, base_);
@@ -779,6 +787,11 @@ static void InitializeSlot(IntPtr type, IntPtr slot, string name)
779787
Marshal.WriteIntPtr(type, offset, slot);
780788
}
781789

790+
static void InitializeSlot(IntPtr type, int slotOffset, MethodInfo method) {
791+
IntPtr thunk = Interop.GetThunk(method);
792+
Marshal.WriteIntPtr(type, slotOffset, thunk);
793+
}
794+
782795
/// <summary>
783796
/// Given a newly allocated Python type object and a managed Type that
784797
/// implements it, initialize any methods defined by the Type that need

0 commit comments

Comments
 (0)