Skip to content

Commit c799057

Browse files
filmorMartin-Molinero
authored andcommitted
Implement support for DLR get/set
(cherry picked from commit 5dacfb4)
1 parent ca19e49 commit c799057

10 files changed

Lines changed: 789 additions & 4 deletions

‎src/runtime/InteropConfiguration.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ public static InteropConfiguration MakeDefault()
2020
{
2121
PythonBaseTypeProviders =
2222
{
23-
DefaultBaseTypeProvider.Instance
24-
// see https://github.com/pythonnet/pythonnet/issues/1785
25-
// new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
23+
DefaultBaseTypeProvider.Instance,
24+
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
25+
new DynamicObjectMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.dlr"))),
2626
},
2727
};
2828
}
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
@@ -291,7 +291,7 @@ static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, s
291291

292292
static void LoadMixins(BorrowedReference targetModuleDict)
293293
{
294-
foreach (string nested in new[] { "collections" })
294+
foreach (string nested in new[] { "collections", "dlr" })
295295
{
296296
LoadSubmodule(targetModuleDict,
297297
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();
@@ -296,6 +454,13 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
296454
throw PythonException.ThrowLastAsClrException();
297455
}
298456

457+
if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(clrType))
458+
{
459+
InitializeSlot(type, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr_proxy), slotsHolder);
460+
InitializeSlot(type, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr_proxy), slotsHolder);
461+
Runtime.PyType_Modified(type.Reference);
462+
}
463+
299464
var dict = Util.ReadRef(type, TypeOffset.tp_dict);
300465
string mn = clrType.Namespace ?? "";
301466
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;
@@ -227,6 +228,13 @@ internal static Type CreateDerivedType(string name,
227228
continue;
228229
}
229230

231+
// Avoid re-entrant DLR binder recursion when Python derives from
232+
// DynamicObject-based types (including overrides in intermediate bases).
233+
if (IsDynamicObjectHookMethod(method))
234+
{
235+
continue;
236+
}
237+
230238
// skip if this property has already been overridden
231239
if ((method.Name.StartsWith("get_") || method.Name.StartsWith("set_"))
232240
&& pyProperties.Contains(method.Name.Substring(4)))
@@ -298,6 +306,35 @@ internal static Type CreateDerivedType(string name,
298306
return type;
299307
}
300308

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

0 commit comments

Comments
 (0)