From 321defadfffadf712329a27e4fa7c579cc404c5a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 7 Jul 2026 14:00:16 -0400 Subject: [PATCH] Keep pythonnet overloads hint in NoMethodMatchPythonExceptionInterpreter message The interpreter replaced the original pythonnet binding-failure message with a friendly one built from the parsed method name, discarding the candidate-signatures hint pythonnet 2.0.57+ appends ("The expected signature is:" / "The following overloads are available:" followed by the signatures). That hint is what lets users see which parameter didn't match, so extract it from the original message and append it to the interpreted one. Co-Authored-By: Claude Fable 5 --- ...NoMethodMatchPythonExceptionInterpreter.cs | 22 +++++++++++ ...hodMatchPythonExceptionInterpreterTests.cs | 39 +++++++++++++++++++ 2 files changed, 61 insertions(+) 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); } }