Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/runtime/InteropConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static InteropConfiguration MakeDefault()
{
DefaultBaseTypeProvider.Instance,
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
new DynamicObjectMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.dlr"))),
},
};
}
Expand Down
47 changes: 47 additions & 0 deletions src/runtime/Mixins/DynamicObjectMixinsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Dynamic;

namespace Python.Runtime.Mixins;

class DynamicObjectMixinsProvider : IPythonBaseTypeProvider, IDisposable
{
readonly Lazy<PyObject> mixinsModule;

public DynamicObjectMixinsProvider(Lazy<PyObject> mixinsModule) =>
this.mixinsModule = mixinsModule ?? throw new ArgumentNullException(nameof(mixinsModule));

public PyObject Mixins => mixinsModule.Value;

public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> existingBases)
{
if (type is null)
throw new ArgumentNullException(nameof(type));

if (existingBases is null)
throw new ArgumentNullException(nameof(existingBases));

if (!typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type))
return existingBases;

var newBases = new List<PyType>(existingBases)
{
new(Mixins.GetAttr("DynamicMetaObjectProviderMixin"))
};

if (type.IsInterface && type.BaseType is null)
{
newBases.RemoveAll(@base => PythonReferenceComparer.Instance.Equals(@base, Runtime.PyBaseObjectType));
}

return newBases;
}

public void Dispose()
{
if (this.mixinsModule.IsValueCreated)
{
this.mixinsModule.Value.Dispose();
}
}
}
18 changes: 18 additions & 0 deletions src/runtime/Mixins/dlr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Implements helpers for Dynamic Language Runtime (DLR) types.
"""

class DynamicMetaObjectProviderMixin:
def __dir__(self):
names = set(super().__dir__())

get_dynamic_member_names = getattr(self, "GetDynamicMemberNames", None)
if callable(get_dynamic_member_names):
try:
for name in get_dynamic_member_names():
if isinstance(name, str):
names.add(name)
except Exception:
pass

return list(sorted(names))
2 changes: 1 addition & 1 deletion src/runtime/PythonEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, s

static void LoadMixins(BorrowedReference targetModuleDict)
{
foreach (string nested in new[] { "collections" })
foreach (string nested in new[] { "collections", "dlr" })
{
LoadSubmodule(targetModuleDict,
fullName: "clr._extras." + nested,
Expand Down
192 changes: 192 additions & 0 deletions src/runtime/TypeManager.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Diagnostics;

using Python.Runtime.Native;
using Python.Runtime.StateSerialization;

Expand Down Expand Up @@ -37,10 +40,190 @@ internal class TypeManager
"tp_clear",
};

static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new();

// tp_getattro_dlr_proxy / tp_setattro_dlr_proxy hit HasClrMember on every
// attribute access; cache the reflection result per (Type, name).
static readonly ConcurrentDictionary<(Type, string), bool> _hasClrMemberCache = new();

static bool HasClrMember(object instance, string memberName) =>
_hasClrMemberCache.GetOrAdd(
(instance.GetType(), memberName),
k => k.Item1.GetMember(k.Item2, BindingFlags.Public | BindingFlags.Instance).Length > 0);

static bool IsPythonSpecialAttributeName(string memberName) =>
memberName.Length > 4 && memberName.StartsWith("__") && memberName.EndsWith("__");

static bool TryGetDynamicInstance(BorrowedReference ob, out object instance, out IDynamicMetaObjectProvider dynamicObject)
{
if (ManagedType.GetManagedObject(ob) is CLRObject co && co.inst is IDynamicMetaObjectProvider coDynamic)
{
instance = co.inst;
dynamicObject = coDynamic;
return true;
}

if (Converter.ToManaged(ob, typeof(IDynamicMetaObjectProvider), out object? managedDynamic, false)
&& managedDynamic is IDynamicMetaObjectProvider convertedDynamic)
{
instance = managedDynamic;
dynamicObject = convertedDynamic;
return true;
}

if (Converter.ToManaged(ob, typeof(object), out object? managedInstance, false)
&& managedInstance is IDynamicMetaObjectProvider boxedDynamic)
{
instance = managedInstance;
dynamicObject = boxedDynamic;
return true;
}

instance = null!;
dynamicObject = null!;
return false;
}

public static NewReference tp_getattro_dlr_proxy(BorrowedReference ob, BorrowedReference key)
{
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);

// The whole DLR machinery only makes sense with string keys and dynamic objects
if (!isDynamic || !Runtime.PyString_Check(key))
{
return Runtime.PyObject_GenericGetAttr(ob, key);
}

string memberName = Runtime.GetManagedString(key)!;

// Forward requests to GetDynamicMemberNames to the mixin implementation
if (memberName == nameof(DynamicObjectMemberAccessor.GetDynamicMemberNames)
&& !HasClrMember(instance, memberName))
{
using var pyMemberNames = new Func<IReadOnlyCollection<string>>(
() => dynamicMemberAccessor.GetDynamicMemberNames(dynamicObject)
).ToPython();
return pyMemberNames.NewReferenceOrNull();
}

// Now, first try to access the Python attribute
var attr = Runtime.PyObject_GenericGetAttr(ob, key);
if (!attr.IsNull())
return attr;

// attr is null, so an exception must be set. If that exception is not an AttributeError,
// we return from this function immediately without clearing. All later returns until the
// very end will lead to the AttributeError getting raised.
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
{
return default;
}

if (HasClrMember(instance, memberName) || IsPythonSpecialAttributeName(memberName))
{
return default;
}

bool resolved = false;
object? value = null;
try
{
resolved = dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out value);
}
catch (Exception e)
{
// Avoid wrapping the CLR exception via Converter.ToPython here: that would trigger
// CLR type initialisation which can re-enter this slot on the same live object,
// causing infinite recursion. A plain RuntimeError with the message is safe.
Runtime.PyErr_Clear();
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
return default;
}

if (!resolved)
{
return default;
}

Runtime.PyErr_Clear();

using var pyValue = value.ToPython();
return pyValue.NewReferenceOrNull();
}

public static int tp_setattro_dlr_proxy(BorrowedReference ob, BorrowedReference key, BorrowedReference val)
{
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);

// The whole DLR machinery only makes sense with string keys and dynamic objects
if (!isDynamic || !Runtime.PyString_Check(key))
{
return Runtime.PyObject_GenericSetAttr(ob, key, val);
}

string memberName = Runtime.GetManagedString(key)!;

// For Python-derived types (IPythonDerivedType), the Python descriptor protocol
// (e.g. @property setters) takes priority over DLR member storage.
if (instance is IPythonDerivedType)
{
int pyResult = Runtime.PyObject_GenericSetAttr(ob, key, val);
if (pyResult == 0)
return 0;

if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
return pyResult;

Runtime.PyErr_Clear();
// Fall through to DLR fallback below
}

if (!HasClrMember(instance, memberName) && !IsPythonSpecialAttributeName(memberName))
{
// Try DLR member storage first
bool handled;

try
{
if (val.IsNull)
{
handled = dynamicMemberAccessor.TryDeleteMember(dynamicObject, memberName);
}
else
{
object? managedValue = null;
if (val != Runtime.PyNone && !Converter.ToManaged(val, typeof(object), out managedValue, true))
return -1;

handled = dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue);
if (!handled)
{
Exceptions.SetError(Exceptions.AttributeError, $"'{instance.GetType().Name}' object has no attribute '{memberName}'");
return -1;
}
}
}
catch (Exception e)
{
// Same reasoning as the getter: avoid Converter.ToPython(e) to keep this
// slot re-entry-safe on live dynamic objects.
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
return -1;
}

if (handled)
return 0;
}

// Fall back to Python attribute setting
return Runtime.PyObject_GenericSetAttr(ob, key, val);
}

internal static void Initialize()
{
Debug.Assert(cache.Count == 0, "Cache should be empty",
"Some errors may occurred on last shutdown");
dynamicMemberAccessor.Clear();
using (var plainType = SlotHelper.CreateObjectType())
{
subtype_traverse = Util.ReadIntPtr(plainType.Borrow(), TypeOffset.tp_traverse);
Expand All @@ -64,6 +247,8 @@ internal static void RemoveTypes()
}
}

dynamicMemberAccessor.Clear();

foreach (var type in cache.Values)
{
type.Dispose();
Expand Down Expand Up @@ -313,6 +498,13 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
throw PythonException.ThrowLastAsClrException();
}

if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(clrType))
{
InitializeSlot(type, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr_proxy), slotsHolder);
InitializeSlot(type, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr_proxy), slotsHolder);
Runtime.PyType_Modified(type.Reference);
}

var dict = Util.ReadRef(type, TypeOffset.tp_dict);
string mn = clrType.Namespace ?? "";
using (var mod = Runtime.PyString_FromString(mn))
Expand Down
37 changes: 37 additions & 0 deletions src/runtime/Types/ClassDerived.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -232,6 +233,13 @@ internal static Type CreateDerivedType(string name,
continue;
}

// Avoid re-entrant DLR binder recursion when Python derives from
// DynamicObject-based types (including overrides in intermediate bases).
if (IsDynamicObjectHookMethod(method))
{
continue;
}

// skip if this property has already been overridden
if ((method.Name.StartsWith("get_") || method.Name.StartsWith("set_"))
&& pyProperties.Contains(method.Name.Substring(4)))
Expand Down Expand Up @@ -300,6 +308,35 @@ internal static Type CreateDerivedType(string name,
return type;
}

static bool IsDynamicObjectHookMethod(MethodInfo method)
{
MethodInfo origin = method.GetBaseDefinition();
Type? originType = origin.DeclaringType;
if (originType == typeof(DynamicObject))
{
return origin.Name switch
{
nameof(DynamicObject.TryGetMember)
or nameof(DynamicObject.TrySetMember)
or nameof(DynamicObject.TryDeleteMember)
or nameof(DynamicObject.TryInvokeMember)
or nameof(DynamicObject.TryConvert)
or nameof(DynamicObject.TryGetIndex)
or nameof(DynamicObject.TrySetIndex)
or nameof(DynamicObject.GetDynamicMemberNames)
or nameof(IDynamicMetaObjectProvider.GetMetaObject) => true,
_ => false,
};
}

if (originType == typeof(IDynamicMetaObjectProvider))
{
return origin.Name == nameof(IDynamicMetaObjectProvider.GetMetaObject);
}

return false;
}

/// <summary>
/// Add a constructor override that calls the python ctor after calling the base type constructor.
/// </summary>
Expand Down
Loading
Loading