Skip to content

Commit 5dacfb4

Browse files
committed
Implement support for DLR get/set
1 parent ccb980a commit 5dacfb4

10 files changed

Lines changed: 787 additions & 1 deletion

src/runtime/InteropConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static InteropConfiguration MakeDefault()
2222
{
2323
DefaultBaseTypeProvider.Instance,
2424
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
25+
new DynamicObjectMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.dlr"))),
2526
},
2627
};
2728
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Dynamic;
4+
5+
namespace Python.Runtime.Mixins;
6+
7+
class DynamicObjectMixinsProvider : IPythonBaseTypeProvider, IDisposable
8+
{
9+
readonly Lazy<PyObject> mixinsModule;
10+
11+
public DynamicObjectMixinsProvider(Lazy<PyObject> mixinsModule) =>
12+
this.mixinsModule = mixinsModule ?? throw new ArgumentNullException(nameof(mixinsModule));
13+
14+
public PyObject Mixins => mixinsModule.Value;
15+
16+
public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> existingBases)
17+
{
18+
if (type is null)
19+
throw new ArgumentNullException(nameof(type));
20+
21+
if (existingBases is null)
22+
throw new ArgumentNullException(nameof(existingBases));
23+
24+
if (!typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type))
25+
return existingBases;
26+
27+
var newBases = new List<PyType>(existingBases)
28+
{
29+
new(Mixins.GetAttr("DynamicMetaObjectProviderMixin"))
30+
};
31+
32+
if (type.IsInterface && type.BaseType is null)
33+
{
34+
newBases.RemoveAll(@base => PythonReferenceComparer.Instance.Equals(@base, Runtime.PyBaseObjectType));
35+
}
36+
37+
return newBases;
38+
}
39+
40+
public void Dispose()
41+
{
42+
if (this.mixinsModule.IsValueCreated)
43+
{
44+
this.mixinsModule.Value.Dispose();
45+
}
46+
}
47+
}

src/runtime/Mixins/dlr.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Implements helpers for Dynamic Language Runtime (DLR) types.
3+
"""
4+
5+
class DynamicMetaObjectProviderMixin:
6+
def __dir__(self):
7+
names = set(super().__dir__())
8+
9+
get_dynamic_member_names = getattr(self, "GetDynamicMemberNames", None)
10+
if callable(get_dynamic_member_names):
11+
try:
12+
for name in get_dynamic_member_names():
13+
if isinstance(name, str):
14+
names.add(name)
15+
except Exception:
16+
pass
17+
18+
return list(sorted(names))

src/runtime/PythonEngine.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, s
299299

300300
static void LoadMixins(BorrowedReference targetModuleDict)
301301
{
302-
foreach (string nested in new[] { "collections" })
302+
foreach (string nested in new[] { "collections", "dlr" })
303303
{
304304
LoadSubmodule(targetModuleDict,
305305
fullName: "clr._extras." + nested,

src/runtime/TypeManager.cs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Dynamic;
34
using System.Linq;
45
using System.Reflection;
56
using System.Runtime.InteropServices;
67
using System.Diagnostics;
8+
79
using Python.Runtime.Native;
810
using Python.Runtime.StateSerialization;
911

@@ -37,10 +39,164 @@ internal class TypeManager
3739
"tp_clear",
3840
};
3941

42+
static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new();
43+
44+
static bool HasClrMember(object instance, string memberName) =>
45+
instance.GetType().GetMember(memberName, BindingFlags.Public | BindingFlags.Instance).Length > 0;
46+
47+
static bool IsPythonSpecialAttributeName(string memberName) =>
48+
memberName.Length > 4 && memberName.StartsWith("__") && memberName.EndsWith("__");
49+
50+
static bool TryGetDynamicInstance(BorrowedReference ob, out object instance, out IDynamicMetaObjectProvider dynamicObject)
51+
{
52+
if (ManagedType.GetManagedObject(ob) is CLRObject co && co.inst is IDynamicMetaObjectProvider coDynamic)
53+
{
54+
instance = co.inst;
55+
dynamicObject = coDynamic;
56+
return true;
57+
}
58+
59+
if (Converter.ToManaged(ob, typeof(IDynamicMetaObjectProvider), out object? managedDynamic, false)
60+
&& managedDynamic is IDynamicMetaObjectProvider convertedDynamic)
61+
{
62+
instance = managedDynamic;
63+
dynamicObject = convertedDynamic;
64+
return true;
65+
}
66+
67+
if (Converter.ToManaged(ob, typeof(object), out object? managedInstance, false)
68+
&& managedInstance is IDynamicMetaObjectProvider boxedDynamic)
69+
{
70+
instance = managedInstance;
71+
dynamicObject = boxedDynamic;
72+
return true;
73+
}
74+
75+
instance = null!;
76+
dynamicObject = null!;
77+
return false;
78+
}
79+
80+
public static NewReference tp_getattro_dlr_proxy(BorrowedReference ob, BorrowedReference key)
81+
{
82+
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);
83+
84+
// The whole DLR machinery only makes sense with string keys and dynamic objects
85+
if (!isDynamic || !Runtime.PyString_Check(key))
86+
{
87+
return Runtime.PyObject_GenericGetAttr(ob, key);
88+
}
89+
90+
string memberName = Runtime.GetManagedString(key)!;
91+
92+
// Forward requests to GetDynamicMemberNames to the mixin implementation
93+
if (memberName == nameof(DynamicObjectMemberAccessor.GetDynamicMemberNames)
94+
&& !HasClrMember(instance, memberName))
95+
{
96+
using var pyMemberNames = new Func<IReadOnlyCollection<string>>(
97+
() => dynamicMemberAccessor.GetDynamicMemberNames(dynamicObject)
98+
).ToPython();
99+
return pyMemberNames.NewReferenceOrNull();
100+
}
101+
102+
// Now, first try to access the Python attribute
103+
var attr = Runtime.PyObject_GenericGetAttr(ob, key);
104+
if (!attr.IsNull())
105+
return attr;
106+
107+
// attr is null, so an exception must be set. If that exception is not an AttributeError,
108+
// we return from this function immediately without clearing. All later returns until the
109+
// very end will lead to the AttributeError getting raised.
110+
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
111+
{
112+
return default;
113+
}
114+
115+
if (HasClrMember(instance, memberName) || IsPythonSpecialAttributeName(memberName))
116+
{
117+
return default;
118+
}
119+
120+
bool resolved = false;
121+
object? value = null;
122+
try
123+
{
124+
resolved = dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out value);
125+
}
126+
catch
127+
{
128+
return default;
129+
}
130+
131+
if (!resolved)
132+
{
133+
return default;
134+
}
135+
136+
Runtime.PyErr_Clear();
137+
138+
using var pyValue = value.ToPython();
139+
return pyValue.NewReferenceOrNull();
140+
}
141+
142+
public static int tp_setattro_dlr_proxy(BorrowedReference ob, BorrowedReference key, BorrowedReference val)
143+
{
144+
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);
145+
146+
// The whole DLR machinery only makes sense with string keys and dynamic objects
147+
if (!isDynamic || !Runtime.PyString_Check(key))
148+
{
149+
return Runtime.PyObject_GenericSetAttr(ob, key, val);
150+
}
151+
152+
string memberName = Runtime.GetManagedString(key)!;
153+
154+
// For Python-derived types (IPythonDerivedType), the Python descriptor protocol
155+
// (e.g. @property setters) takes priority over DLR member storage.
156+
if (instance is IPythonDerivedType)
157+
{
158+
int pyResult = Runtime.PyObject_GenericSetAttr(ob, key, val);
159+
if (pyResult == 0)
160+
return 0;
161+
162+
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
163+
return pyResult;
164+
165+
Runtime.PyErr_Clear();
166+
// Fall through to DLR fallback below
167+
}
168+
169+
if (!HasClrMember(instance, memberName) && !IsPythonSpecialAttributeName(memberName))
170+
{
171+
// Try DLR member storage first
172+
bool handled = false;
173+
174+
if (val == null)
175+
{
176+
handled = dynamicMemberAccessor.TryDeleteMember(dynamicObject, memberName);
177+
}
178+
else
179+
{
180+
object? managedValue = null;
181+
if (val != Runtime.PyNone && !Converter.ToManaged(val, typeof(object), out managedValue, true))
182+
return -1;
183+
184+
handled = dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue);
185+
}
186+
187+
if (handled)
188+
return 0;
189+
}
190+
191+
// Fall back to Python attribute setting
192+
return Runtime.PyObject_GenericSetAttr(ob, key, val);
193+
}
194+
40195
internal static void Initialize()
41196
{
42197
Debug.Assert(cache.Count == 0, "Cache should be empty",
43198
"Some errors may occurred on last shutdown");
199+
dynamicMemberAccessor.Clear();
44200
using (var plainType = SlotHelper.CreateObjectType())
45201
{
46202
subtype_traverse = Util.ReadIntPtr(plainType.Borrow(), TypeOffset.tp_traverse);
@@ -64,6 +220,8 @@ internal static void RemoveTypes()
64220
}
65221
}
66222

223+
dynamicMemberAccessor.Clear();
224+
67225
foreach (var type in cache.Values)
68226
{
69227
type.Dispose();
@@ -313,6 +471,13 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
313471
throw PythonException.ThrowLastAsClrException();
314472
}
315473

474+
if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(clrType))
475+
{
476+
InitializeSlot(type, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr_proxy), slotsHolder);
477+
InitializeSlot(type, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr_proxy), slotsHolder);
478+
Runtime.PyType_Modified(type.Reference);
479+
}
480+
316481
var dict = Util.ReadRef(type, TypeOffset.tp_dict);
317482
string mn = clrType.Namespace ?? "";
318483
using (var mod = Runtime.PyString_FromString(mn))

src/runtime/Types/ClassDerived.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel;
4+
using System.Dynamic;
45
using System.Diagnostics;
56
using System.Linq;
67
using System.Reflection;
@@ -232,6 +233,13 @@ internal static Type CreateDerivedType(string name,
232233
continue;
233234
}
234235

236+
// Avoid re-entrant DLR binder recursion when Python derives from
237+
// DynamicObject-based types (including overrides in intermediate bases).
238+
if (IsDynamicObjectHookMethod(method))
239+
{
240+
continue;
241+
}
242+
235243
// skip if this property has already been overridden
236244
if ((method.Name.StartsWith("get_") || method.Name.StartsWith("set_"))
237245
&& pyProperties.Contains(method.Name.Substring(4)))
@@ -300,6 +308,35 @@ internal static Type CreateDerivedType(string name,
300308
return type;
301309
}
302310

311+
static bool IsDynamicObjectHookMethod(MethodInfo method)
312+
{
313+
MethodInfo origin = method.GetBaseDefinition();
314+
Type? originType = origin.DeclaringType;
315+
if (originType == typeof(DynamicObject))
316+
{
317+
return origin.Name switch
318+
{
319+
nameof(DynamicObject.TryGetMember)
320+
or nameof(DynamicObject.TrySetMember)
321+
or nameof(DynamicObject.TryDeleteMember)
322+
or nameof(DynamicObject.TryInvokeMember)
323+
or nameof(DynamicObject.TryConvert)
324+
or nameof(DynamicObject.TryGetIndex)
325+
or nameof(DynamicObject.TrySetIndex)
326+
or nameof(DynamicObject.GetDynamicMemberNames)
327+
or nameof(IDynamicMetaObjectProvider.GetMetaObject) => true,
328+
_ => false,
329+
};
330+
}
331+
332+
if (originType == typeof(IDynamicMetaObjectProvider))
333+
{
334+
return origin.Name == nameof(IDynamicMetaObjectProvider.GetMetaObject);
335+
}
336+
337+
return false;
338+
}
339+
303340
/// <summary>
304341
/// Add a constructor override that calls the python ctor after calling the base type constructor.
305342
/// </summary>

0 commit comments

Comments
 (0)