-
Notifications
You must be signed in to change notification settings - Fork 772
Implement support for DLR get/set #2706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """ | ||
| Implements helpers for Dynamic Language Runtime (DLR) types. | ||
| """ | ||
|
|
||
| class DynamicMetaObjectProviderMixin: | ||
| def __dir__(self): | ||
| names = set(super().__dir__()) | ||
|
|
||
| get_names = getattr(self, "GetDynamicMemberNames", None) | ||
| if callable(get_names): | ||
| try: | ||
| names.update(get_names()) | ||
| except Exception: | ||
| pass | ||
|
|
||
| return list(sorted(names)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Dynamic; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Runtime.InteropServices; | ||
|
|
@@ -22,6 +23,10 @@ namespace Python.Runtime | |
| [Serializable] | ||
| internal class ClassBase : ManagedType, IDeserializationCallback | ||
| { | ||
| static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new(); | ||
|
|
||
| internal static void Reset() => dynamicMemberAccessor.Clear(); | ||
|
|
||
| [NonSerialized] | ||
| internal List<string> dotNetMembers = new(); | ||
| internal Indexer? indexer; | ||
|
|
@@ -603,6 +608,105 @@ static IEnumerable<MethodInfo> GetCallImplementations(Type type) | |
| => type.GetMethods(BindingFlags.Public | BindingFlags.Instance) | ||
| .Where(m => m.Name == "__call__"); | ||
|
|
||
| static NewReference tp_getattro_dlr(BorrowedReference ob, BorrowedReference key) | ||
| { | ||
| var attr = Runtime.PyObject_GenericGetAttr(ob, key); | ||
| if (!attr.IsNull()) | ||
| { | ||
| return attr; | ||
| } | ||
|
|
||
| // Only run the DLR binder if the error was AttributeError, otherwise preserve the original error | ||
| if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0) | ||
| { | ||
| return default; | ||
| } | ||
|
|
||
| if (!Runtime.PyString_Check(key)) | ||
| { | ||
| return default; | ||
| } | ||
|
|
||
| if (GetManagedObject(ob) is not CLRObject co) | ||
| { | ||
| return default; | ||
| } | ||
|
|
||
| // Slot registration already guarantees this type supports DLR | ||
| var dynamicObject = (IDynamicMetaObjectProvider)co.inst; | ||
|
|
||
| string? memberName = Runtime.GetManagedString(key); | ||
| if (memberName is null) | ||
| { | ||
| return default; | ||
| } | ||
|
|
||
| if (!dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out object? value)) | ||
| { | ||
| return default; | ||
| } | ||
|
|
||
| // Clear the lingering AttributeError | ||
| Runtime.PyErr_Clear(); | ||
|
|
||
| using var pyValue = value.ToPython(); | ||
| return pyValue.NewReferenceOrNull(); | ||
| } | ||
|
|
||
| static int tp_setattro_dlr(BorrowedReference ob, BorrowedReference key, BorrowedReference val) | ||
| { | ||
| int result = Runtime.PyObject_GenericSetAttr(ob, key, val); | ||
| if (result == 0) | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| // Preserve non-attribute errors exactly as they are. | ||
| if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| // Deletion fallback is intentionally not handled by DLR binder yet. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "yet" here means we can't fix it until 4.0 as it would be breaking change |
||
| if (val == null) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember if BorrowedReference == null is implemented. It might be better to explicitly use IsNull |
||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| if (!Runtime.PyString_Check(key)) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| if (GetManagedObject(ob) is not CLRObject co) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| // Slot registration already guarantees this type supports DLR. | ||
| var dynamicObject = (IDynamicMetaObjectProvider)co.inst; | ||
|
|
||
| string? memberName = Runtime.GetManagedString(key); | ||
| if (memberName is null) | ||
| { | ||
| return -1; | ||
| } | ||
|
Comment on lines
+678
to
+693
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, silent error masking |
||
|
|
||
| if (!Converter.ToManaged(val, typeof(object), out object? managedValue, true)) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| if (!dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue)) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| // Clear the lingering AttributeError | ||
| Runtime.PyErr_Clear(); | ||
| return 0; | ||
| } | ||
|
|
||
| public virtual void InitializeSlots(BorrowedReference pyType, SlotsHolder slotsHolder) | ||
| { | ||
| if (!this.type.Valid) return; | ||
|
|
@@ -612,6 +716,12 @@ public virtual void InitializeSlots(BorrowedReference pyType, SlotsHolder slotsH | |
| TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.tp_call, new Interop.BBB_N(tp_call_impl), slotsHolder); | ||
| } | ||
|
|
||
| if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(this.type.Value)) | ||
| { | ||
| TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr), slotsHolder); | ||
| TypeManager.InitializeSlotIfEmpty(pyType, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr), slotsHolder); | ||
|
Comment on lines
+721
to
+722
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there any conflicts possible? |
||
| } | ||
|
|
||
| if (indexer is not null) | ||
| { | ||
| if (indexer.CanGet) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| using System; | ||
| using System.Dynamic; | ||
| using System.Runtime.CompilerServices; | ||
| using Microsoft.CSharp.RuntimeBinder; | ||
|
|
||
| namespace Python.Runtime; | ||
|
|
||
| class DynamicObjectMemberAccessor | ||
| { | ||
| const int MaxCacheEntries = 1000; | ||
|
|
||
| readonly ConcurrentLruCache<MemberKey, Func<object, object>> getters = new(MaxCacheEntries); | ||
| readonly ConcurrentLruCache<MemberKey, Action<object, object?>> setters = new(MaxCacheEntries); | ||
|
|
||
| static readonly CSharpArgumentInfo[] getArgumentInfo = | ||
| { | ||
| CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), | ||
| }; | ||
|
|
||
| static readonly CSharpArgumentInfo[] setArgumentInfo = | ||
| { | ||
| CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), | ||
| CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), | ||
| }; | ||
|
|
||
| public bool TryGetMember(IDynamicMetaObjectProvider obj, string memberName, out object? value) | ||
| { | ||
| if (obj is null) | ||
| throw new ArgumentNullException(nameof(obj)); | ||
| if (memberName is null) | ||
| throw new ArgumentNullException(nameof(memberName)); | ||
|
|
||
| var getter = getters.GetOrAdd(new MemberKey(obj.GetType(), memberName), static key => | ||
| { | ||
| var binder = Binder.GetMember(CSharpBinderFlags.None, key.MemberName, key.Type, getArgumentInfo); | ||
| var callSite = CallSite<Func<CallSite, object, object>>.Create(binder); | ||
| return obj => callSite.Target(callSite, obj); | ||
| }); | ||
|
|
||
| try | ||
| { | ||
| value = getter(obj); | ||
| return true; | ||
| } | ||
| catch (RuntimeBinderException) | ||
| { | ||
| value = null; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| public bool TrySetMember(IDynamicMetaObjectProvider obj, string memberName, object? value) | ||
| { | ||
| if (obj is null) | ||
| throw new ArgumentNullException(nameof(obj)); | ||
| if (memberName is null) | ||
| throw new ArgumentNullException(nameof(memberName)); | ||
|
|
||
| var setter = setters.GetOrAdd(new MemberKey(obj.GetType(), memberName), static key => | ||
| { | ||
| var binder = Binder.SetMember(CSharpBinderFlags.None, key.MemberName, key.Type, setArgumentInfo); | ||
| var callSite = CallSite<Action<CallSite, object, object?>>.Create(binder); | ||
| return (obj, value) => callSite.Target(callSite, obj, value); | ||
| }); | ||
|
|
||
| try | ||
| { | ||
| setter(obj, value); | ||
| return true; | ||
| } | ||
| catch (RuntimeBinderException) | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| readonly record struct MemberKey(Type Type, string MemberName); | ||
|
|
||
| public void Clear() | ||
| { | ||
| getters.Clear(); | ||
| setters.Clear(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These return defaults are a little concerning. We are basically hiding errors (which I am not sure can even happen in the first place).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are not. If we arrive at any of these, we have already gotten an error from the initial Python getattr. Since we are not clearing it until the very end, this is what Python is going to see. I'll add some more tests for that.