11using System ;
22using System . Collections . Generic ;
3+ using System . Dynamic ;
34using System . Linq ;
45using System . Reflection ;
56using System . Runtime . InteropServices ;
67using System . Diagnostics ;
8+
79using Python . Runtime . Native ;
810using 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 ) )
0 commit comments