diff --git a/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs b/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs
index bae403278506..65ba4adbc16d 100644
--- a/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs
+++ b/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs
@@ -53,6 +53,12 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i
var methodName = GetMethodName(pe.Message);
var message = Messages.NoMethodMatchPythonExceptionInterpreter.AttemptedToAccessMethodThatDoesNotExist(methodName);
+ var overloadsHint = GetOverloadsHint(pe.Message);
+ if (!string.IsNullOrEmpty(overloadsHint))
+ {
+ message += $" {overloadsHint}";
+ }
+
message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
return new MissingMethodException(message, pe);
@@ -81,5 +87,21 @@ private static string GetMethodName(string exceptionMessage)
return methodName.Trim();
}
+
+ ///
+ /// Extracts the candidate-signatures hint pythonnet appends to the binding-failure
+ /// message ("The expected signature is:" or "The following overloads are available:"
+ /// followed by the signatures), so the interpreted message can keep it.
+ ///
+ private static string GetOverloadsHint(string exceptionMessage)
+ {
+ var hintIndex = exceptionMessage.IndexOfInvariant("The expected signature is:");
+ if (hintIndex == -1)
+ {
+ hintIndex = exceptionMessage.IndexOfInvariant("The following overloads are available:");
+ }
+
+ return hintIndex == -1 ? null : exceptionMessage.Substring(hintIndex).Trim();
+ }
}
}
diff --git a/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs
index fdaa0e6dd8db..7f0c4d0aa5f3 100644
--- a/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs
+++ b/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs
@@ -120,6 +120,45 @@ public void VerifyMessageContainsTheMethodNameForOverloadedMethod()
Assert.That(exception.Message, Does.Not.Contain(">)"));
}
+ [Test]
+ public void VerifyMessageKeepsTheOverloadsHint()
+ {
+ // pythonnet appends the candidate signatures to the binding-failure message.
+ // set_cash('SPY') has multiple candidate overloads.
+ var pythonException = (PythonException)CreateExceptionFromType(typeof(PythonException));
+ StringAssert.Contains("The following overloads are available:", pythonException.Message);
+
+ var interpreter = new NoMethodMatchPythonExceptionInterpreter();
+ var exception = interpreter.Interpret(pythonException, NullExceptionInterpreter.Instance);
+
+ // The interpreted message must keep the overloads hint so the user can see
+ // what the method expects, not just that no overload matched.
+ Assert.That(exception.Message, Does.Contain("The following overloads are available:"));
+ Assert.That(exception.Message, Does.Contain("set_cash("));
+ }
+
+ [Test]
+ public void VerifyMessageKeepsTheExpectedSignatureHint()
+ {
+ PythonException pythonException;
+ using (Py.GIL())
+ {
+ var module = Py.Import("Test_PythonExceptionInterpreter");
+ dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();
+ pythonException = Assert.Throws(() => algorithm.no_method_match_rsi());
+ }
+
+ // rsi's candidate overloads collapse into a single snake-case signature,
+ // so pythonnet words the hint as "The expected signature is:"
+ StringAssert.Contains("The expected signature is:", pythonException.Message);
+
+ var interpreter = new NoMethodMatchPythonExceptionInterpreter();
+ var exception = interpreter.Interpret(pythonException, NullExceptionInterpreter.Instance);
+
+ Assert.That(exception.Message, Does.Contain("The expected signature is:"));
+ Assert.That(exception.Message, Does.Contain("rsi("));
+ }
+
private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
}
}