Skip to content
Closed
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
14 changes: 14 additions & 0 deletions src/runtime/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ public static void SetError(BorrowedReference type, BorrowedReference exceptionO
}

internal const string DispatchInfoAttribute = "__dispatch_info__";

/// <summary>
/// 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.
/// </summary>
internal const string BindFailureMethodNameAttribute = "_clr_method_name";

/// <inheritdoc cref="BindFailureMethodNameAttribute"/>
internal const string BindFailureSignaturesAttribute = "_clr_overload_signatures";

/// <inheritdoc cref="BindFailureMethodNameAttribute"/>
internal const string BindFailureOverloadsHintAttribute = "_clr_overloads_hint";
/// <summary>
/// SetError Method
/// </summary>
Expand Down
102 changes: 95 additions & 7 deletions src/runtime/MethodBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(": ");
Expand All @@ -1036,13 +1042,14 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
var candidates = methodinfo != null && methodinfo.Length > 0
? methodinfo.Cast<MethodBase>()
: 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;
Expand Down Expand Up @@ -1123,6 +1130,87 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
return Converter.ToPython(result, returnType);
}

/// <summary>
/// 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.
/// </summary>
private static void RaiseBindFailure(string message, string methodName, IReadOnlyList<string> 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);
}
}
}

/// <summary>
/// Utility class to store the information about a <see cref="MethodBase"/>
/// </summary>
Expand Down
60 changes: 38 additions & 22 deletions src/runtime/MethodSignatureFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,22 @@ public static class MethodSignatureFormatter
/// <param name="displayName">Optional name to display for the methods, e.g. the type
/// name for constructors instead of the special <c>.ctor</c> token</param>
public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxShown = 10, string displayName = null)
{
return FormatOverloadsHint(GetSignatures(methods, displayName), maxShown);
}

/// <summary>
/// The distinct formatted signatures of the candidate overloads, in order, with the
/// PyObject-overload filtering described on <see cref="FormatOverloads"/>. Never
/// throws: it only runs on error paths and must not mask the original failure.
/// </summary>
internal static IReadOnlyList<string> GetSignatures(IEnumerable<MethodBase> methods, string displayName = null)
{
if (methods == null)
{
return string.Empty;
return Array.Empty<string>();
}

// 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();
Expand All @@ -45,8 +53,6 @@ public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxSho
candidates = withoutPyObject;
}

// Distinct signatures, preserving order. Snake-cased duplicates and
// repeated overloads collapse into a single entry.
var signatures = new List<string>();
var seen = new HashSet<string>();
foreach (var method in candidates)
Expand All @@ -58,29 +64,39 @@ public static string FormatOverloads(IEnumerable<MethodBase> 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<string>();
}
}

/// <summary>
/// Renders the signatures from <see cref="GetSignatures"/> as the hint block appended
/// to bind-failure messages: a header plus one signature per line, capped at
/// <paramref name="maxShown"/>.
/// </summary>
internal static string FormatOverloadsHint(IReadOnlyList<string> 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();
}

/// <summary>
Expand Down
41 changes: 41 additions & 0 deletions tests/test_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "(<class 'dict'>, <class 'str'>)" 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()

Expand Down
Loading