Skip to content

Commit 27caf5c

Browse files
authored
Merge pull request #2706 from pythonnet/dlr
Implement support for DLR get/set
2 parents 6bde465 + 3a08990 commit 27caf5c

10 files changed

Lines changed: 912 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: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
4+
using System.Dynamic;
35
using System.Linq;
46
using System.Reflection;
57
using System.Runtime.InteropServices;
68
using System.Diagnostics;
9+
710
using Python.Runtime.Native;
811
using Python.Runtime.StateSerialization;
912

@@ -37,10 +40,190 @@ internal class TypeManager
3740
"tp_clear",
3841
};
3942

43+
static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new();
44+
45+
// tp_getattro_dlr_proxy / tp_setattro_dlr_proxy hit HasClrMember on every
46+
// attribute access; cache the reflection result per (Type, name).
47+
static readonly ConcurrentDictionary<(Type, string), bool> _hasClrMemberCache = new();
48+
49+
static bool HasClrMember(object instance, string memberName) =>
50+
_hasClrMemberCache.GetOrAdd(
51+
(instance.GetType(), memberName),
52+
k => k.Item1.GetMember(k.Item2, BindingFlags.Public | BindingFlags.Instance).Length > 0);
53+
54+
static bool IsPythonSpecialAttributeName(string memberName) =>
55+
memberName.Length > 4 && memberName.StartsWith("__") && memberName.EndsWith("__");
56+
57+
static bool TryGetDynamicInstance(BorrowedReference ob, out object instance, out IDynamicMetaObjectProvider dynamicObject)
58+
{
59+
if (ManagedType.GetManagedObject(ob) is CLRObject co && co.inst is IDynamicMetaObjectProvider coDynamic)
60+
{
61+
instance = co.inst;
62+
dynamicObject = coDynamic;
63+
return true;
64+
}
65+
66+
if (Converter.ToManaged(ob, typeof(IDynamicMetaObjectProvider), out object? managedDynamic, false)
67+
&& managedDynamic is IDynamicMetaObjectProvider convertedDynamic)
68+
{
69+
instance = managedDynamic;
70+
dynamicObject = convertedDynamic;
71+
return true;
72+
}
73+
74+
if (Converter.ToManaged(ob, typeof(object), out object? managedInstance, false)
75+
&& managedInstance is IDynamicMetaObjectProvider boxedDynamic)
76+
{
77+
instance = managedInstance;
78+
dynamicObject = boxedDynamic;
79+
return true;
80+
}
81+
82+
instance = null!;
83+
dynamicObject = null!;
84+
return false;
85+
}
86+
87+
public static NewReference tp_getattro_dlr_proxy(BorrowedReference ob, BorrowedReference key)
88+
{
89+
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);
90+
91+
// The whole DLR machinery only makes sense with string keys and dynamic objects
92+
if (!isDynamic || !Runtime.PyString_Check(key))
93+
{
94+
return Runtime.PyObject_GenericGetAttr(ob, key);
95+
}
96+
97+
string memberName = Runtime.GetManagedString(key)!;
98+
99+
// Forward requests to GetDynamicMemberNames to the mixin implementation
100+
if (memberName == nameof(DynamicObjectMemberAccessor.GetDynamicMemberNames)
101+
&& !HasClrMember(instance, memberName))
102+
{
103+
using var pyMemberNames = new Func<IReadOnlyCollection<string>>(
104+
() => dynamicMemberAccessor.GetDynamicMemberNames(dynamicObject)
105+
).ToPython();
106+
return pyMemberNames.NewReferenceOrNull();
107+
}
108+
109+
// Now, first try to access the Python attribute
110+
var attr = Runtime.PyObject_GenericGetAttr(ob, key);
111+
if (!attr.IsNull())
112+
return attr;
113+
114+
// attr is null, so an exception must be set. If that exception is not an AttributeError,
115+
// we return from this function immediately without clearing. All later returns until the
116+
// very end will lead to the AttributeError getting raised.
117+
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
118+
{
119+
return default;
120+
}
121+
122+
if (HasClrMember(instance, memberName) || IsPythonSpecialAttributeName(memberName))
123+
{
124+
return default;
125+
}
126+
127+
bool resolved = false;
128+
object? value = null;
129+
try
130+
{
131+
resolved = dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out value);
132+
}
133+
catch (Exception e)
134+
{
135+
// Avoid wrapping the CLR exception via Converter.ToPython here: that would trigger
136+
// CLR type initialisation which can re-enter this slot on the same live object,
137+
// causing infinite recursion. A plain RuntimeError with the message is safe.
138+
Runtime.PyErr_Clear();
139+
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
140+
return default;
141+
}
142+
143+
if (!resolved)
144+
{
145+
return default;
146+
}
147+
148+
Runtime.PyErr_Clear();
149+
150+
using var pyValue = value.ToPython();
151+
return pyValue.NewReferenceOrNull();
152+
}
153+
154+
public static int tp_setattro_dlr_proxy(BorrowedReference ob, BorrowedReference key, BorrowedReference val)
155+
{
156+
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);
157+
158+
// The whole DLR machinery only makes sense with string keys and dynamic objects
159+
if (!isDynamic || !Runtime.PyString_Check(key))
160+
{
161+
return Runtime.PyObject_GenericSetAttr(ob, key, val);
162+
}
163+
164+
string memberName = Runtime.GetManagedString(key)!;
165+
166+
// For Python-derived types (IPythonDerivedType), the Python descriptor protocol
167+
// (e.g. @property setters) takes priority over DLR member storage.
168+
if (instance is IPythonDerivedType)
169+
{
170+
int pyResult = Runtime.PyObject_GenericSetAttr(ob, key, val);
171+
if (pyResult == 0)
172+
return 0;
173+
174+
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
175+
return pyResult;
176+
177+
Runtime.PyErr_Clear();
178+
// Fall through to DLR fallback below
179+
}
180+
181+
if (!HasClrMember(instance, memberName) && !IsPythonSpecialAttributeName(memberName))
182+
{
183+
// Try DLR member storage first
184+
bool handled;
185+
186+
try
187+
{
188+
if (val.IsNull)
189+
{
190+
handled = dynamicMemberAccessor.TryDeleteMember(dynamicObject, memberName);
191+
}
192+
else
193+
{
194+
object? managedValue = null;
195+
if (val != Runtime.PyNone && !Converter.ToManaged(val, typeof(object), out managedValue, true))
196+
return -1;
197+
198+
handled = dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue);
199+
if (!handled)
200+
{
201+
Exceptions.SetError(Exceptions.AttributeError, $"'{instance.GetType().Name}' object has no attribute '{memberName}'");
202+
return -1;
203+
}
204+
}
205+
}
206+
catch (Exception e)
207+
{
208+
// Same reasoning as the getter: avoid Converter.ToPython(e) to keep this
209+
// slot re-entry-safe on live dynamic objects.
210+
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
211+
return -1;
212+
}
213+
214+
if (handled)
215+
return 0;
216+
}
217+
218+
// Fall back to Python attribute setting
219+
return Runtime.PyObject_GenericSetAttr(ob, key, val);
220+
}
221+
40222
internal static void Initialize()
41223
{
42224
Debug.Assert(cache.Count == 0, "Cache should be empty",
43225
"Some errors may occurred on last shutdown");
226+
dynamicMemberAccessor.Clear();
44227
using (var plainType = SlotHelper.CreateObjectType())
45228
{
46229
subtype_traverse = Util.ReadIntPtr(plainType.Borrow(), TypeOffset.tp_traverse);
@@ -64,6 +247,8 @@ internal static void RemoveTypes()
64247
}
65248
}
66249

250+
dynamicMemberAccessor.Clear();
251+
67252
foreach (var type in cache.Values)
68253
{
69254
type.Dispose();
@@ -313,6 +498,13 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
313498
throw PythonException.ThrowLastAsClrException();
314499
}
315500

501+
if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(clrType))
502+
{
503+
InitializeSlot(type, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr_proxy), slotsHolder);
504+
InitializeSlot(type, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr_proxy), slotsHolder);
505+
Runtime.PyType_Modified(type.Reference);
506+
}
507+
316508
var dict = Util.ReadRef(type, TypeOffset.tp_dict);
317509
string mn = clrType.Namespace ?? "";
318510
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)