Skip to content

Commit 8b12b56

Browse files
greateggsgregfilmor
authored andcommitted
Propagate exceptions from TryGetMember in tp_getattro_dlr_proxy (#2718)
The dynamic getter swallowed any exception from TryGetMember and returned default to Python with the prior AttributeError still set, so user code observed a misleading AttributeError instead of the real failure. Set a Python exception in the catch arm. We use RuntimeError with the message string rather than Converter.ToPython(e) because wrapping the CLR exception object can trigger type initialisation that re-enters this same slot on the live dynamic object, producing infinite recursion. Mirrors the symmetry already present in the setter (#2706 review, @lostmsu) and adds a regression test alongside the existing ThrowingSetDynamicObject coverage.
1 parent 2408c43 commit 8b12b56

3 files changed

Lines changed: 21 additions & 1 deletion

File tree

src/runtime/TypeManager.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,13 @@ public static NewReference tp_getattro_dlr_proxy(BorrowedReference ob, BorrowedR
123123
{
124124
resolved = dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out value);
125125
}
126-
catch
126+
catch (Exception e)
127127
{
128+
// Avoid wrapping the CLR exception via Converter.ToPython here: that would trigger
129+
// CLR type initialisation which can re-enter this slot on the same live object,
130+
// causing infinite recursion. A plain RuntimeError with the message is safe.
131+
Runtime.PyErr_Clear();
132+
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
128133
return default;
129134
}
130135

src/testing/dlrtest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ public override bool TrySetMember(SetMemberBinder binder, object value)
6262
}
6363
}
6464

65+
public class ThrowingGetDynamicObject : DynamicStorageObject
66+
{
67+
public override bool TryGetMember(GetMemberBinder binder, out object result)
68+
=> throw new InvalidOperationException($"TryGetMember failed for '{binder.Name}'");
69+
}
70+
6571
public class ThrowingSetDynamicObject : DynamicStorageObject
6672
{
6773
public override bool TrySetMember(SetMemberBinder binder, object value)

tests/test_dynamic.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from Python.Test import RejectingDeleteDynamicObject
99
from Python.Test import RejectingSetDynamicObject
1010
from Python.Test import ThrowingDeleteDynamicObject
11+
from Python.Test import ThrowingGetDynamicObject
1112
from Python.Test import ThrowingSetDynamicObject
1213

1314

@@ -186,6 +187,14 @@ def test_trysetmember_false_raises_attributeerror_instead_of_silent_python_setat
186187
assert not hasattr(obj, "typoed_name")
187188

188189

190+
def test_trygetmember_exception_is_raised_in_python():
191+
obj = ThrowingGetDynamicObject()
192+
obj.AddDynamicMember("any_key", 1)
193+
194+
with pytest.raises(Exception, match="TryGetMember failed for 'any_key'"):
195+
_ = obj.any_key
196+
197+
189198
def test_trysetmember_exception_is_raised_in_python():
190199
obj = ThrowingSetDynamicObject()
191200

0 commit comments

Comments
 (0)