@@ -21,6 +21,7 @@ internal sealed class MetaType : ManagedType
2121 // set in Initialize
2222 private static PyType PyCLRMetaType ;
2323 private static SlotsHolder _metaSlotsHodler ;
24+ private static int TypeDictOffset = - 1 ;
2425#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
2526
2627 internal static readonly string [ ] CustomMethods = new string [ ]
@@ -35,6 +36,25 @@ internal sealed class MetaType : ManagedType
3536 public static PyType Initialize ( )
3637 {
3738 PyCLRMetaType = TypeManager . CreateMetaType ( typeof ( MetaType ) , out _metaSlotsHodler ) ;
39+
40+ // Retrieve the offset of the type's dictionary from PyType_Type for
41+ // use in the tp_setattro implementation.
42+ using ( NewReference dictOffset = Runtime . PyObject_GetAttr ( Runtime . PyTypeType , PyIdentifier . __dictoffset__ ) )
43+ {
44+ if ( dictOffset . IsNull ( ) )
45+ {
46+ throw new InvalidOperationException ( "Could not get __dictoffset__ from PyType_Type" ) ;
47+ }
48+
49+ nint dictOffsetVal = Runtime . PyLong_AsSignedSize_t ( dictOffset . Borrow ( ) ) ;
50+ if ( dictOffsetVal <= 0 )
51+ {
52+ throw new InvalidOperationException ( "Could not get __dictoffset__ from PyType_Type" ) ;
53+ }
54+
55+ TypeDictOffset = checked ( ( int ) dictOffsetVal ) ;
56+ }
57+
3858 return PyCLRMetaType ;
3959 }
4060
@@ -44,6 +64,7 @@ public static void Release()
4464 {
4565 _metaSlotsHodler . ResetSlots ( ) ;
4666 }
67+ TypeDictOffset = - 1 ;
4768 PyCLRMetaType . Dispose ( ) ;
4869 }
4970
@@ -287,7 +308,28 @@ public static int tp_setattro(BorrowedReference tp, BorrowedReference name, Borr
287308 }
288309 }
289310
290- int res = Runtime . PyObject_GenericSetAttr ( tp , name , value ) ;
311+ // Access the type's dictionary directly
312+ //
313+ // We can not use the PyObject_GenericSetAttr because since Python
314+ // 3.14 as https://github.com/python/cpython/pull/118454 intrdoduced
315+ // an assertion to prevent it from being called from metatypes.
316+ //
317+ // The direct dictionary access is equivalent to what Cython does
318+ // to work around the same issue: https://github.com/cython/cython/pull/6325
319+ BorrowedReference typeDict = new ( Util . ReadIntPtr ( tp , TypeDictOffset ) ) ;
320+ int res ;
321+ if ( value . IsNull )
322+ {
323+ res = Runtime . PyDict_DelItem ( typeDict , name ) ;
324+ if ( res != 0 )
325+ {
326+ Exceptions . SetError ( Exceptions . AttributeError , "attribute not found" ) ;
327+ }
328+ }
329+ else
330+ {
331+ res = Runtime . PyDict_SetItem ( typeDict , name , value ) ;
332+ }
291333 Runtime . PyType_Modified ( tp ) ;
292334
293335 return res ;
0 commit comments