Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Propagate exceptions from TryGetMember in tp_getattro_dlr_proxy
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.
  • Loading branch information
greateggsgreg committed May 9, 2026
commit 5d64310756dfb7a508b21836bc676d8144357b2f
7 changes: 6 additions & 1 deletion src/runtime/TypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,13 @@ public static NewReference tp_getattro_dlr_proxy(BorrowedReference ob, BorrowedR
{
resolved = dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out value);
}
catch
catch (Exception e)
{
// Avoid wrapping the CLR exception via Converter.ToPython here: that would trigger
// CLR type initialisation which can re-enter this slot on the same live object,
// causing infinite recursion. A plain RuntimeError with the message is safe.
Runtime.PyErr_Clear();
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
return default;
}

Expand Down
6 changes: 6 additions & 0 deletions src/testing/dlrtest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public override bool TrySetMember(SetMemberBinder binder, object value)
}
}

public class ThrowingGetDynamicObject : DynamicStorageObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
=> throw new InvalidOperationException($"TryGetMember failed for '{binder.Name}'");
}

public class ThrowingSetDynamicObject : DynamicStorageObject
{
public override bool TrySetMember(SetMemberBinder binder, object value)
Expand Down
9 changes: 9 additions & 0 deletions tests/test_dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from Python.Test import RejectingDeleteDynamicObject
from Python.Test import RejectingSetDynamicObject
from Python.Test import ThrowingDeleteDynamicObject
from Python.Test import ThrowingGetDynamicObject
from Python.Test import ThrowingSetDynamicObject


Expand Down Expand Up @@ -186,6 +187,14 @@ def test_trysetmember_false_raises_attributeerror_instead_of_silent_python_setat
assert not hasattr(obj, "typoed_name")


def test_trygetmember_exception_is_raised_in_python():
obj = ThrowingGetDynamicObject()
obj.AddDynamicMember("any_key", 1)

with pytest.raises(Exception, match="TryGetMember failed for 'any_key'"):
_ = obj.any_key


def test_trysetmember_exception_is_raised_in_python():
obj = ThrowingSetDynamicObject()

Expand Down
Loading