Skip to content

Commit eb6577f

Browse files
committed
allow .NET classes to override __getattr__ and __setattr__
1 parent a8a9426 commit eb6577f

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy