Skip to content

Commit 9346334

Browse files
filmorMartin-Molinero
authored andcommitted
Workaround for blocked PyObject_GenericSetAttr in metatypes
Python 3.14 introduced a new assertion that prevents us from using PyObject_GenericSetAttr directly in our meta type. To work around this, we manipulate the type dict directly. This workaround is a simplified variant of Cython's workaround from cython/cython#6325. The relevant Python change is in python/cpython#118454 (cherry picked from commit 08550d0)
1 parent 5e67d64 commit 9346334

3 files changed

Lines changed: 47 additions & 1 deletion

File tree

src/runtime/Native/PyIdentifier_.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ static class PyIdentifier
2525
public static BorrowedReference __self__ => new(f__self__);
2626
static IntPtr f__annotations__;
2727
public static BorrowedReference __annotations__ => new(f__annotations__);
28+
static IntPtr f__dictoffset__;
29+
public static BorrowedReference __dictoffset__ => new(f__dictoffset__);
2830
static IntPtr f__init__;
2931
public static BorrowedReference __init__ => new(f__init__);
3032
static IntPtr f__repr__;
@@ -57,6 +59,7 @@ static partial class InternString
5759
"__slots__",
5860
"__self__",
5961
"__annotations__",
62+
"__dictoffset__",
6063
"__init__",
6164
"__repr__",
6265
"__import__",

src/runtime/Native/PyIdentifier_.tt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"__slots__",
1414
"__self__",
1515
"__annotations__",
16+
"__dictoffset__",
1617

1718
"__init__",
1819
"__repr__",

src/runtime/Types/MetaType.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal sealed class MetaType : ManagedType
1818
// set in Initialize
1919
private static PyType PyCLRMetaType;
2020
private static SlotsHolder _metaSlotsHodler;
21+
private static int TypeDictOffset = -1;
2122
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
2223

2324
internal static readonly string[] CustomMethods = new string[]
@@ -32,6 +33,25 @@ internal sealed class MetaType : ManagedType
3233
public static PyType Initialize()
3334
{
3435
PyCLRMetaType = TypeManager.CreateMetaType(typeof(MetaType), out _metaSlotsHodler);
36+
37+
// Retrieve the offset of the type's dictionary from PyType_Type for
38+
// use in the tp_setattro implementation.
39+
using (NewReference dictOffset = Runtime.PyObject_GetAttr(Runtime.PyTypeType, PyIdentifier.__dictoffset__))
40+
{
41+
if (dictOffset.IsNull())
42+
{
43+
throw new InvalidOperationException("Could not get __dictoffset__ from PyType_Type");
44+
}
45+
46+
nint dictOffsetVal = Runtime.PyLong_AsSignedSize_t(dictOffset.Borrow());
47+
if (dictOffsetVal <= 0)
48+
{
49+
throw new InvalidOperationException("Could not get __dictoffset__ from PyType_Type");
50+
}
51+
52+
TypeDictOffset = checked((int)dictOffsetVal);
53+
}
54+
3555
return PyCLRMetaType;
3656
}
3757

@@ -41,6 +61,7 @@ public static void Release()
4161
{
4262
_metaSlotsHodler.ResetSlots();
4363
}
64+
TypeDictOffset = -1;
4465
PyCLRMetaType.Dispose();
4566
}
4667

@@ -246,7 +267,28 @@ public static int tp_setattro(BorrowedReference tp, BorrowedReference name, Borr
246267
}
247268
}
248269

249-
int res = Runtime.PyObject_GenericSetAttr(tp, name, value);
270+
// Access the type's dictionary directly
271+
//
272+
// We can not use the PyObject_GenericSetAttr because since Python
273+
// 3.14 as https://github.com/python/cpython/pull/118454 intrdoduced
274+
// an assertion to prevent it from being called from metatypes.
275+
//
276+
// The direct dictionary access is equivalent to what Cython does
277+
// to work around the same issue: https://github.com/cython/cython/pull/6325
278+
BorrowedReference typeDict = new(Util.ReadIntPtr(tp, TypeDictOffset));
279+
int res;
280+
if (value.IsNull)
281+
{
282+
res = Runtime.PyDict_DelItem(typeDict, name);
283+
if (res != 0)
284+
{
285+
Exceptions.SetError(Exceptions.AttributeError, "attribute not found");
286+
}
287+
}
288+
else
289+
{
290+
res = Runtime.PyDict_SetItem(typeDict, name, value);
291+
}
250292
Runtime.PyType_Modified(tp);
251293

252294
return res;

0 commit comments

Comments
 (0)