diff --git a/src/runtime/Exceptions.cs b/src/runtime/Exceptions.cs
index c3ac889ed..9b5e0e3d1 100644
--- a/src/runtime/Exceptions.cs
+++ b/src/runtime/Exceptions.cs
@@ -179,6 +179,20 @@ public static void SetError(BorrowedReference type, BorrowedReference exceptionO
}
internal const string DispatchInfoAttribute = "__dispatch_info__";
+
+ ///
+ /// Attributes attached to the bind-failure TypeError (see MethodBinder) carrying the
+ /// data its message is built from, so consumers do not have to parse the message.
+ /// Must stay internal: Initialize() resolves every public static field of this class
+ /// against the builtins module.
+ ///
+ internal const string BindFailureMethodNameAttribute = "_clr_method_name";
+
+ ///
+ internal const string BindFailureSignaturesAttribute = "_clr_overload_signatures";
+
+ ///
+ internal const string BindFailureOverloadsHintAttribute = "_clr_overloads_hint";
///
/// SetError Method
///
diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs
index a20624d2b..a1dcf7022 100644
--- a/src/runtime/MethodBinder.cs
+++ b/src/runtime/MethodBinder.cs
@@ -1016,15 +1016,21 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
// If we already have an exception pending, don't create a new one
if (!Exceptions.ErrorOccurred())
{
- var value = new StringBuilder("No method matches given arguments");
// Use the snake_case name Python callers use, matching the hinted signatures below.
+ string methodName = null;
if (methodinfo != null && methodinfo.Length > 0)
{
- value.Append($" for {MethodSignatureFormatter.SnakeCaseName(methodinfo[0])}");
+ methodName = MethodSignatureFormatter.SnakeCaseName(methodinfo[0]);
}
else if (list.Count > 0)
{
- value.Append($" for {MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase)}");
+ methodName = MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase);
+ }
+
+ var value = new StringBuilder("No method matches given arguments");
+ if (methodName != null)
+ {
+ value.Append($" for {methodName}");
}
value.Append(": ");
@@ -1036,13 +1042,14 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
var candidates = methodinfo != null && methodinfo.Length > 0
? methodinfo.Cast()
: list?.Select(m => m.MethodBase);
- var overloads = MethodSignatureFormatter.FormatOverloads(candidates);
- if (overloads.Length > 0)
+ var signatures = MethodSignatureFormatter.GetSignatures(candidates);
+ var overloadsHint = MethodSignatureFormatter.FormatOverloadsHint(signatures);
+ if (overloadsHint.Length > 0)
{
- value.Append(". ").Append(overloads);
+ value.Append(". ").Append(overloadsHint);
}
- Exceptions.RaiseTypeError(value.ToString());
+ RaiseBindFailure(value.ToString(), methodName, signatures, overloadsHint);
}
return default;
@@ -1123,6 +1130,87 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
return Converter.ToPython(result, returnType);
}
+ ///
+ /// Raises the bind-failure TypeError, attaching the method name, signatures and
+ /// overloads hint as attributes (the Exceptions.BindFailure*Attribute constants).
+ /// Best-effort: on any failure the plain TypeError with the same message remains set.
+ ///
+ private static void RaiseBindFailure(string message, string methodName, IReadOnlyList signatures, string overloadsHint)
+ {
+ Exceptions.SetError(Exceptions.TypeError, message);
+ if (methodName == null && (signatures == null || signatures.Count == 0))
+ {
+ return;
+ }
+
+ try
+ {
+ Runtime.PyErr_Fetch(out var errType, out var errVal, out var errTb);
+ try
+ {
+ Runtime.PyErr_NormalizeException(ref errType, ref errVal, ref errTb);
+
+ if (!errVal.IsNull())
+ {
+ var instance = errVal.Borrow();
+
+ if (methodName != null)
+ {
+ using var namePy = Runtime.PyString_FromString(methodName);
+ if (!namePy.IsNull())
+ {
+ Runtime.PyObject_SetAttrString(instance, Exceptions.BindFailureMethodNameAttribute, namePy.Borrow());
+ }
+ }
+
+ if (signatures != null && signatures.Count > 0)
+ {
+ using var tuple = Runtime.PyTuple_New(signatures.Count);
+ var populated = !tuple.IsNull();
+ for (var i = 0; i < signatures.Count && populated; i++)
+ {
+ using var signature = Runtime.PyString_FromString(signatures[i]);
+ populated = !signature.IsNull()
+ && Runtime.PyTuple_SetItem(tuple.Borrow(), i, signature.Borrow()) == 0;
+ }
+
+ if (populated)
+ {
+ Runtime.PyObject_SetAttrString(instance, Exceptions.BindFailureSignaturesAttribute, tuple.Borrow());
+
+ if (!string.IsNullOrEmpty(overloadsHint))
+ {
+ using var hintPy = Runtime.PyString_FromString(overloadsHint);
+ if (!hintPy.IsNull())
+ {
+ Runtime.PyObject_SetAttrString(instance, Exceptions.BindFailureOverloadsHintAttribute, hintPy.Borrow());
+ }
+ }
+ }
+ }
+ }
+
+ // A failed attribute set must not replace the bind failure with its own error
+ if (Exceptions.ErrorOccurred())
+ {
+ Runtime.PyErr_Clear();
+ }
+ }
+ finally
+ {
+ Runtime.PyErr_Restore(errType.StealNullable(), errVal.StealNullable(), errTb.StealNullable());
+ }
+ }
+ catch
+ {
+ // The error state may have been consumed by the failed fetch/restore
+ if (!Exceptions.ErrorOccurred())
+ {
+ Exceptions.SetError(Exceptions.TypeError, message);
+ }
+ }
+ }
+
///
/// Utility class to store the information about a
///
diff --git a/src/runtime/MethodSignatureFormatter.cs b/src/runtime/MethodSignatureFormatter.cs
index a382ee172..476be4627 100644
--- a/src/runtime/MethodSignatureFormatter.cs
+++ b/src/runtime/MethodSignatureFormatter.cs
@@ -28,14 +28,22 @@ public static class MethodSignatureFormatter
/// Optional name to display for the methods, e.g. the type
/// name for constructors instead of the special .ctor token
public static string FormatOverloads(IEnumerable methods, int maxShown = 10, string displayName = null)
+ {
+ return FormatOverloadsHint(GetSignatures(methods, displayName), maxShown);
+ }
+
+ ///
+ /// The distinct formatted signatures of the candidate overloads, in order, with the
+ /// PyObject-overload filtering described on . Never
+ /// throws: it only runs on error paths and must not mask the original failure.
+ ///
+ internal static IReadOnlyList GetSignatures(IEnumerable methods, string displayName = null)
{
if (methods == null)
{
- return string.Empty;
+ return Array.Empty();
}
- // Building this only runs on error paths; never let it throw and mask
- // the original failure.
try
{
var candidates = methods.Where(method => method != null).ToList();
@@ -45,8 +53,6 @@ public static string FormatOverloads(IEnumerable methods, int maxSho
candidates = withoutPyObject;
}
- // Distinct signatures, preserving order. Snake-cased duplicates and
- // repeated overloads collapse into a single entry.
var signatures = new List();
var seen = new HashSet();
foreach (var method in candidates)
@@ -58,29 +64,39 @@ public static string FormatOverloads(IEnumerable methods, int maxSho
}
}
- if (signatures.Count == 0)
- {
- return string.Empty;
- }
-
- var to = new StringBuilder(signatures.Count == 1
- ? "The expected signature is:"
- : "The following overloads are available:");
- for (var i = 0; i < signatures.Count && i < maxShown; i++)
- {
- to.Append("\n ").Append(signatures[i]);
- }
- if (signatures.Count > maxShown)
- {
- to.Append($"\n ... and {signatures.Count - maxShown} more");
- }
- return to.ToString();
+ return signatures;
}
catch
{
// Best-effort hint only.
+ return Array.Empty();
+ }
+ }
+
+ ///
+ /// Renders the signatures from as the hint block appended
+ /// to bind-failure messages: a header plus one signature per line, capped at
+ /// .
+ ///
+ internal static string FormatOverloadsHint(IReadOnlyList signatures, int maxShown = 10)
+ {
+ if (signatures == null || signatures.Count == 0)
+ {
return string.Empty;
}
+
+ var to = new StringBuilder(signatures.Count == 1
+ ? "The expected signature is:"
+ : "The following overloads are available:");
+ for (var i = 0; i < signatures.Count && i < maxShown; i++)
+ {
+ to.Append("\n ").Append(signatures[i]);
+ }
+ if (signatures.Count > maxShown)
+ {
+ to.Append($"\n ... and {signatures.Count - maxShown} more");
+ }
+ return to.ToString();
}
///
diff --git a/tests/test_method.py b/tests/test_method.py
index 07b5c5a34..6f9272490 100644
--- a/tests/test_method.py
+++ b/tests/test_method.py
@@ -1255,6 +1255,47 @@ def test_params_array_overloaded_failing():
res = MethodTest.ParamsArrayOverloaded(paramsArray=[], i=1)
assert res == "with params-array"
+def test_bind_failure_structured_attributes():
+ """A bind-failure TypeError carries the method name, overload signatures
+ and rendered overloads hint as attributes, matching the message."""
+ with pytest.raises(TypeError) as excinfo:
+ MethodTest.TestOverloadedParams({}, "x")
+ e = excinfo.value
+
+ assert e._clr_method_name == "test_overloaded_params"
+
+ signatures = e._clr_overload_signatures
+ assert isinstance(signatures, tuple)
+ assert len(signatures) > 1
+ assert all(isinstance(s, str) and s.startswith("test_overloaded_params(")
+ for s in signatures)
+
+ hint = e._clr_overloads_hint
+ assert hint.startswith("The following overloads are available:")
+ for signature in signatures:
+ assert signature in hint
+
+ # The message itself is unchanged: prefix + argument types + the same hint
+ message = str(e)
+ assert message.startswith(
+ "No method matches given arguments for test_overloaded_params: ")
+ assert "(, )" in message
+ assert message.endswith(hint)
+
+
+def test_bind_failure_structured_attributes_single_overload():
+ """Single-overload failures use the singular hint header and still carry
+ the structured attributes."""
+ with pytest.raises(TypeError) as excinfo:
+ MethodTest.TestOverloadedNoObject("foo")
+ e = excinfo.value
+
+ assert e._clr_method_name == "test_overloaded_no_object"
+ assert e._clr_overload_signatures == ("test_overloaded_no_object(i: int)",)
+ assert e._clr_overloads_hint.startswith("The expected signature is:")
+ assert str(e).endswith(e._clr_overloads_hint)
+
+
def test_method_encoding():
MethodTest.EncodingTestÅngström()