Skip to content

Commit 95f6f97

Browse files
Only remove CPython's slot wrappers, never failing the type creation
The cleanup now deletes a member only when it is a wrapper_descriptor, the object PyType_Ready adds, instead of anything that is not a managed object. It is best effort: a failed delete clears its error and any exception is swallowed, since a wrapper left behind only keeps the previous behavior. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 83fd880 commit 95f6f97

2 files changed

Lines changed: 41 additions & 11 deletions

File tree

‎src/embed_tests/TestOperator.cs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,5 +650,25 @@ class Plain(TestOperator.ForwardOnlyOperable):
650650
Assert.AreEqual("custom", scope.Get<string>("custom"));
651651
Assert.AreEqual("TypeError", scope.Get<string>("plain"));
652652
}
653+
654+
public class ForwardOnlyOperableWithReflectedMethod
655+
{
656+
public static ForwardOnlyOperableWithReflectedMethod operator +(ForwardOnlyOperableWithReflectedMethod a, ForwardOnlyOperableWithReflectedMethod b) => a;
657+
658+
// a CLR member named like the reflected method, not a slot wrapper
659+
public string __radd__(double other) => "clr";
660+
}
661+
662+
[Test]
663+
public void ClrMemberNamedLikeTheReflectedMethodIsKept()
664+
{
665+
using var _ = Py.GIL();
666+
using var scope = Py.CreateScope();
667+
scope.Exec(@"
668+
from Python.EmbeddingTest import TestOperator
669+
result = 1.0 + TestOperator.ForwardOnlyOperableWithReflectedMethod()
670+
");
671+
Assert.AreEqual("clr", scope.Get<string>("result"));
672+
}
653673
}
654674
}

‎src/runtime/Types/OperatorMethod.cs‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,25 +136,35 @@ public static void FixupSlots(BorrowedReference pyType, Type clrType)
136136
/// </summary>
137137
public static void RemoveUnboundSlotWrappers(BorrowedReference pyType, Type clrType)
138138
{
139-
var dict = Util.ReadRef(pyType, TypeOffset.tp_dict);
140-
var fixedSlots = GetOperatorCandidates(clrType)
141-
.Where(method => OpMethodMap.ContainsKey(method.Name))
142-
.Select(method => OpMethodMap[method.Name].MethodName)
143-
.Distinct();
144-
foreach (var pyName in fixedSlots)
139+
// best effort: a wrapper left behind only keeps the previous behavior, so nothing here fails the type creation
140+
try
145141
{
146-
RemoveSlotWrapper(dict, pyName);
147-
RemoveSlotWrapper(dict, ReversePyMethodName(pyName));
142+
var dict = Util.ReadRef(pyType, TypeOffset.tp_dict);
143+
var fixedSlots = GetOperatorCandidates(clrType)
144+
.Where(method => OpMethodMap.ContainsKey(method.Name))
145+
.Select(method => OpMethodMap[method.Name].MethodName)
146+
.Distinct();
147+
foreach (var pyName in fixedSlots)
148+
{
149+
RemoveSlotWrapper(dict, pyName);
150+
RemoveSlotWrapper(dict, ReversePyMethodName(pyName));
151+
}
152+
}
153+
catch
154+
{
155+
// pass
148156
}
149157
}
150158

151159
private static void RemoveSlotWrapper(BorrowedReference dict, string name)
152160
{
153161
var member = Runtime.PyDict_GetItemString(dict, name);
154-
// the CLR operators are managed method objects, anything else was added by PyType_Ready
155-
if (!member.IsNull && ManagedType.GetManagedObject(member) == null && Runtime.PyDict_DelItemString(dict, name) != 0)
162+
// only the slot wrappers PyType_Ready added, the CLR operators are managed method objects
163+
if (!member.IsNull && Runtime.PyObject_TYPE(member) == Runtime.PyWrapperDescriptorType
164+
&& Runtime.PyDict_DelItemString(dict, name) != 0)
156165
{
157-
throw PythonException.ThrowLastAsClrException();
166+
// don't let a failed delete leak, the wrapper just stays
167+
Exceptions.Clear();
158168
}
159169
}
160170

0 commit comments

Comments
 (0)