Skip to content

Commit ad678b6

Browse files
committed
Attach structured bind-failure data to the no-method-matches TypeError
When no overload matches a call, the TypeError now carries the data its message is built from as attributes on the exception instance: _clr_method_name (snake_case method name), _clr_overload_signatures (tuple of formatted signatures) and _clr_overloads_hint (the rendered hint block appended to the message). Consumers such as Lean's exception interpreters can read these instead of parsing the message. The message itself is unchanged, and attribute attachment is best-effort: on any failure the plain TypeError with the same message is raised.
1 parent c080337 commit ad678b6

4 files changed

Lines changed: 200 additions & 29 deletions

File tree

src/runtime/Exceptions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,25 @@ public static void SetError(BorrowedReference type, BorrowedReference exceptionO
179179
}
180180

181181
internal const string DispatchInfoAttribute = "__dispatch_info__";
182+
183+
/// <summary>
184+
/// Names of the attributes attached to the TypeError raised when a method call
185+
/// cannot be bound to any overload (see MethodBinder). They carry the data the
186+
/// message is built from, so consumers can read it without parsing the message:
187+
/// the snake_case method name (str), the formatted overload signatures
188+
/// (tuple of str) and the rendered overloads hint block (str) exactly as it
189+
/// appears at the end of the message. Each attribute is only present when the
190+
/// corresponding information is available.
191+
/// (Internal like <see cref="DispatchInfoAttribute"/>: Initialize() resolves every
192+
/// public static field of this class against the builtins module.)
193+
/// </summary>
194+
internal const string BindFailureMethodNameAttribute = "_clr_method_name";
195+
196+
/// <inheritdoc cref="BindFailureMethodNameAttribute"/>
197+
internal const string BindFailureSignaturesAttribute = "_clr_overload_signatures";
198+
199+
/// <inheritdoc cref="BindFailureMethodNameAttribute"/>
200+
internal const string BindFailureOverloadsHintAttribute = "_clr_overloads_hint";
182201
/// <summary>
183202
/// SetError Method
184203
/// </summary>

src/runtime/MethodBinder.cs

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,15 +1016,21 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
10161016
// If we already have an exception pending, don't create a new one
10171017
if (!Exceptions.ErrorOccurred())
10181018
{
1019-
var value = new StringBuilder("No method matches given arguments");
10201019
// Use the snake_case name Python callers use, matching the hinted signatures below.
1020+
string methodName = null;
10211021
if (methodinfo != null && methodinfo.Length > 0)
10221022
{
1023-
value.Append($" for {MethodSignatureFormatter.SnakeCaseName(methodinfo[0])}");
1023+
methodName = MethodSignatureFormatter.SnakeCaseName(methodinfo[0]);
10241024
}
10251025
else if (list.Count > 0)
10261026
{
1027-
value.Append($" for {MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase)}");
1027+
methodName = MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase);
1028+
}
1029+
1030+
var value = new StringBuilder("No method matches given arguments");
1031+
if (methodName != null)
1032+
{
1033+
value.Append($" for {methodName}");
10281034
}
10291035

10301036
value.Append(": ");
@@ -1036,13 +1042,14 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
10361042
var candidates = methodinfo != null && methodinfo.Length > 0
10371043
? methodinfo.Cast<MethodBase>()
10381044
: list?.Select(m => m.MethodBase);
1039-
var overloads = MethodSignatureFormatter.FormatOverloads(candidates);
1040-
if (overloads.Length > 0)
1045+
var signatures = MethodSignatureFormatter.GetSignatures(candidates);
1046+
var overloadsHint = MethodSignatureFormatter.FormatOverloadsHint(signatures);
1047+
if (overloadsHint.Length > 0)
10411048
{
1042-
value.Append(". ").Append(overloads);
1049+
value.Append(". ").Append(overloadsHint);
10431050
}
10441051

1045-
Exceptions.RaiseTypeError(value.ToString());
1052+
RaiseBindFailure(value.ToString(), methodName, signatures, overloadsHint);
10461053
}
10471054

10481055
return default;
@@ -1123,6 +1130,90 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
11231130
return Converter.ToPython(result, returnType);
11241131
}
11251132

1133+
/// <summary>
1134+
/// Raises the bind-failure TypeError with the given message, attaching the method
1135+
/// name, overload signatures and rendered overloads hint as attributes on the
1136+
/// exception instance (see the Exceptions.BindFailure*Attribute constants) so
1137+
/// consumers can read them without parsing the message. Attribute attachment is
1138+
/// best-effort: on any failure the plain TypeError with the same message remains set.
1139+
/// </summary>
1140+
private static void RaiseBindFailure(string message, string methodName, IReadOnlyList<string> signatures, string overloadsHint)
1141+
{
1142+
Exceptions.SetError(Exceptions.TypeError, message);
1143+
if (methodName == null && (signatures == null || signatures.Count == 0))
1144+
{
1145+
return;
1146+
}
1147+
1148+
try
1149+
{
1150+
// Normalize the freshly raised error into an exception instance, decorate
1151+
// it, and restore it as the pending error.
1152+
Runtime.PyErr_Fetch(out var errType, out var errVal, out var errTb);
1153+
try
1154+
{
1155+
Runtime.PyErr_NormalizeException(ref errType, ref errVal, ref errTb);
1156+
1157+
if (!errVal.IsNull())
1158+
{
1159+
var instance = errVal.Borrow();
1160+
1161+
if (methodName != null)
1162+
{
1163+
using var namePy = Runtime.PyString_FromString(methodName);
1164+
if (!namePy.IsNull())
1165+
{
1166+
Runtime.PyObject_SetAttrString(instance, Exceptions.BindFailureMethodNameAttribute, namePy.Borrow());
1167+
}
1168+
}
1169+
1170+
if (signatures != null && signatures.Count > 0)
1171+
{
1172+
using var tuple = Runtime.PyTuple_New(signatures.Count);
1173+
var populated = !tuple.IsNull();
1174+
for (var i = 0; i < signatures.Count && populated; i++)
1175+
{
1176+
using var signature = Runtime.PyString_FromString(signatures[i]);
1177+
populated = !signature.IsNull()
1178+
&& Runtime.PyTuple_SetItem(tuple.Borrow(), i, signature.Borrow()) == 0;
1179+
}
1180+
1181+
if (populated)
1182+
{
1183+
Runtime.PyObject_SetAttrString(instance, Exceptions.BindFailureSignaturesAttribute, tuple.Borrow());
1184+
1185+
if (!string.IsNullOrEmpty(overloadsHint))
1186+
{
1187+
using var hintPy = Runtime.PyString_FromString(overloadsHint);
1188+
if (!hintPy.IsNull())
1189+
{
1190+
Runtime.PyObject_SetAttrString(instance, Exceptions.BindFailureOverloadsHintAttribute, hintPy.Borrow());
1191+
}
1192+
}
1193+
}
1194+
}
1195+
}
1196+
1197+
// Decoration must never replace the bind failure with its own error.
1198+
if (Exceptions.ErrorOccurred())
1199+
{
1200+
Runtime.PyErr_Clear();
1201+
}
1202+
}
1203+
finally
1204+
{
1205+
Runtime.PyErr_Restore(errType.StealNullable(), errVal.StealNullable(), errTb.StealNullable());
1206+
}
1207+
}
1208+
catch
1209+
{
1210+
if (!Exceptions.ErrorOccurred())
1211+
{
1212+
Exceptions.SetError(Exceptions.TypeError, message);
1213+
}
1214+
}
1215+
}
1216+
11261217
/// <summary>
11271218
/// Utility class to store the information about a <see cref="MethodBase"/>
11281219
/// </summary>

src/runtime/MethodSignatureFormatter.cs

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,25 @@ public static class MethodSignatureFormatter
2828
/// <param name="displayName">Optional name to display for the methods, e.g. the type
2929
/// name for constructors instead of the special <c>.ctor</c> token</param>
3030
public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxShown = 10, string displayName = null)
31+
{
32+
return FormatOverloadsHint(GetSignatures(methods, displayName), maxShown);
33+
}
34+
35+
/// <summary>
36+
/// The distinct formatted signatures of the candidate overloads, preserving order.
37+
/// Snake-cased duplicates and repeated overloads collapse into a single entry, and
38+
/// overloads taking PyObject parameters are skipped unless every candidate takes one
39+
/// (see <see cref="FormatOverloads"/>). Never throws: signature formatting only runs
40+
/// on error paths and must not mask the original failure. Returns an empty list when
41+
/// there is nothing to show.
42+
/// </summary>
43+
internal static IReadOnlyList<string> GetSignatures(IEnumerable<MethodBase> methods, string displayName = null)
3144
{
3245
if (methods == null)
3346
{
34-
return string.Empty;
47+
return Array.Empty<string>();
3548
}
3649

37-
// Building this only runs on error paths; never let it throw and mask
38-
// the original failure.
3950
try
4051
{
4152
var candidates = methods.Where(method => method != null).ToList();
@@ -45,8 +56,6 @@ public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxSho
4556
candidates = withoutPyObject;
4657
}
4758

48-
// Distinct signatures, preserving order. Snake-cased duplicates and
49-
// repeated overloads collapse into a single entry.
5059
var signatures = new List<string>();
5160
var seen = new HashSet<string>();
5261
foreach (var method in candidates)
@@ -58,29 +67,40 @@ public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxSho
5867
}
5968
}
6069

61-
if (signatures.Count == 0)
62-
{
63-
return string.Empty;
64-
}
65-
66-
var to = new StringBuilder(signatures.Count == 1
67-
? "The expected signature is:"
68-
: "The following overloads are available:");
69-
for (var i = 0; i < signatures.Count && i < maxShown; i++)
70-
{
71-
to.Append("\n ").Append(signatures[i]);
72-
}
73-
if (signatures.Count > maxShown)
74-
{
75-
to.Append($"\n ... and {signatures.Count - maxShown} more");
76-
}
77-
return to.ToString();
70+
return signatures;
7871
}
7972
catch
8073
{
8174
// Best-effort hint only.
75+
return Array.Empty<string>();
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Renders the signatures produced by <see cref="GetSignatures"/> as the hint block
81+
/// appended to bind-failure messages: a header line followed by one signature per
82+
/// line, capped at <paramref name="maxShown"/> entries. Returns an empty string when
83+
/// there are no signatures to show.
84+
/// </summary>
85+
internal static string FormatOverloadsHint(IReadOnlyList<string> signatures, int maxShown = 10)
86+
{
87+
if (signatures == null || signatures.Count == 0)
88+
{
8289
return string.Empty;
8390
}
91+
92+
var to = new StringBuilder(signatures.Count == 1
93+
? "The expected signature is:"
94+
: "The following overloads are available:");
95+
for (var i = 0; i < signatures.Count && i < maxShown; i++)
96+
{
97+
to.Append("\n ").Append(signatures[i]);
98+
}
99+
if (signatures.Count > maxShown)
100+
{
101+
to.Append($"\n ... and {signatures.Count - maxShown} more");
102+
}
103+
return to.ToString();
84104
}
85105

86106
/// <summary>

tests/test_method.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,47 @@ def test_params_array_overloaded_failing():
12551255
res = MethodTest.ParamsArrayOverloaded(paramsArray=[], i=1)
12561256
assert res == "with params-array"
12571257

1258+
def test_bind_failure_structured_attributes():
1259+
"""A bind-failure TypeError carries the method name, overload signatures
1260+
and rendered overloads hint as attributes, matching the message."""
1261+
with pytest.raises(TypeError) as excinfo:
1262+
MethodTest.TestOverloadedParams({}, "x")
1263+
e = excinfo.value
1264+
1265+
assert e._clr_method_name == "test_overloaded_params"
1266+
1267+
signatures = e._clr_overload_signatures
1268+
assert isinstance(signatures, tuple)
1269+
assert len(signatures) > 1
1270+
assert all(isinstance(s, str) and s.startswith("test_overloaded_params(")
1271+
for s in signatures)
1272+
1273+
hint = e._clr_overloads_hint
1274+
assert hint.startswith("The following overloads are available:")
1275+
for signature in signatures:
1276+
assert signature in hint
1277+
1278+
# The message itself is unchanged: prefix + argument types + the same hint
1279+
message = str(e)
1280+
assert message.startswith(
1281+
"No method matches given arguments for test_overloaded_params: ")
1282+
assert "(<class 'dict'>, <class 'str'>)" in message
1283+
assert message.endswith(hint)
1284+
1285+
1286+
def test_bind_failure_structured_attributes_single_overload():
1287+
"""Single-overload failures use the singular hint header and still carry
1288+
the structured attributes."""
1289+
with pytest.raises(TypeError) as excinfo:
1290+
MethodTest.TestOverloadedNoObject("foo")
1291+
e = excinfo.value
1292+
1293+
assert e._clr_method_name == "test_overloaded_no_object"
1294+
assert e._clr_overload_signatures == ("test_overloaded_no_object(i: int)",)
1295+
assert e._clr_overloads_hint.startswith("The expected signature is:")
1296+
assert str(e).endswith(e._clr_overloads_hint)
1297+
1298+
12581299
def test_method_encoding():
12591300
MethodTest.EncodingTestÅngström()
12601301

0 commit comments

Comments
 (0)