From 64ee7f9c543cea4940b494da113c9aa7c5a95250 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 12:03:54 -0400 Subject: [PATCH 1/2] Prefer pythonnet's structured bind-failure data over message parsing NoMethodMatchPythonExceptionInterpreter now reads the method name and overloads hint from the attributes pythonnet attaches to the bind-failure TypeError (_clr_method_name, _clr_overloads_hint) instead of parsing them out of the exception message. When the attributes are absent (older pythonnet versions) or unreadable, it silently falls back to the existing message parsing. The rendered user-facing message is unchanged. --- ...NoMethodMatchPythonExceptionInterpreter.cs | 68 ++++++++++++++++++- ...hodMatchPythonExceptionInterpreterTests.cs | 41 +++++++++++ .../Test_PythonExceptionInterpreter.py | 16 +++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs b/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs index 65ba4adbc16d..6e3aaf342d01 100644 --- a/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs +++ b/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs @@ -24,6 +24,14 @@ namespace QuantConnect.Exceptions /// public class NoMethodMatchPythonExceptionInterpreter : PythonExceptionInterpreter { + /// + /// Attribute names pythonnet attaches to the bind-failure TypeError, carrying the + /// data its message is built from: the snake_case method name and the rendered + /// overloads hint block exactly as it appears at the end of the message. + /// + private const string MethodNameAttribute = "_clr_method_name"; + private const string OverloadsHintAttribute = "_clr_overloads_hint"; + /// /// Determines the order that an instance of this class should be called /// @@ -50,10 +58,14 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i { var pe = (PythonException)exception; - var methodName = GetMethodName(pe.Message); + // Prefer the structured data pythonnet attaches to the bind-failure TypeError, + // falling back to parsing the message for versions that do not attach it. + TryGetStructuredBindFailureData(pe, out var methodName, out var overloadsHint); + + methodName ??= GetMethodName(pe.Message); var message = Messages.NoMethodMatchPythonExceptionInterpreter.AttemptedToAccessMethodThatDoesNotExist(methodName); - var overloadsHint = GetOverloadsHint(pe.Message); + overloadsHint ??= GetOverloadsHint(pe.Message); if (!string.IsNullOrEmpty(overloadsHint)) { message += $" {overloadsHint}"; @@ -64,6 +76,58 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i return new MissingMethodException(message, pe); } + /// + /// Reads the structured bind-failure data pythonnet attaches to the TypeError so + /// the method name and overloads hint do not have to be parsed out of the message. + /// Both outputs are null when the attributes are absent (raised by a pythonnet + /// version that does not attach them) or cannot be read. + /// + private static void TryGetStructuredBindFailureData(PythonException exception, out string methodName, + out string overloadsHint) + { + methodName = null; + overloadsHint = null; + + try + { + var value = exception.Value; + if (value == null) + { + return; + } + + using (Py.GIL()) + { + if (value.HasAttr(MethodNameAttribute)) + { + using var nameAttribute = value.GetAttr(MethodNameAttribute); + methodName = nameAttribute.As(); + } + if (value.HasAttr(OverloadsHintAttribute)) + { + using var hintAttribute = value.GetAttr(OverloadsHintAttribute); + overloadsHint = hintAttribute.As(); + } + } + + // Normalize so the caller's null-coalescing fallback kicks in + if (string.IsNullOrEmpty(methodName)) + { + methodName = null; + } + if (string.IsNullOrEmpty(overloadsHint)) + { + overloadsHint = null; + } + } + catch + { + // Fall back to message parsing on any failure reading the attributes + methodName = null; + overloadsHint = null; + } + } + /// /// Extracts the name of the method that failed to resolve from the Python exception message. /// The message has the form: "No method matches given arguments for {methodName}: ({argumentTypes})", diff --git a/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs index 7f0c4d0aa5f3..c62cf09ef161 100644 --- a/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs +++ b/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs @@ -159,6 +159,47 @@ public void VerifyMessageKeepsTheExpectedSignatureHint() Assert.That(exception.Message, Does.Contain("rsi(")); } + [Test] + public void InterpretPrefersStructuredAttributesOverMessageParsing() + { + // The fixture raises a TypeError whose message names 'parsed_name' but whose + // structured attributes name 'structured_name'; the attributes must win. + var pythonException = ThrowFixtureMethod("no_method_match_with_structured_attributes"); + + var interpreter = new NoMethodMatchPythonExceptionInterpreter(); + var exception = interpreter.Interpret(pythonException, NullExceptionInterpreter.Instance); + + Assert.That(exception.Message, Does.Contain("required by the structured_name method")); + Assert.That(exception.Message, Does.Contain("The expected signature is:")); + Assert.That(exception.Message, Does.Contain("structured_name(x: int)")); + Assert.That(exception.Message, Does.Not.Contain("parsed_name")); + } + + [Test] + public void InterpretFallsBackToMessageParsingWithoutStructuredAttributes() + { + // Same message shape but no structured attributes (older pythonnet versions): + // the interpreter must extract the method name and hint from the message. + var pythonException = ThrowFixtureMethod("no_method_match_without_structured_attributes"); + + var interpreter = new NoMethodMatchPythonExceptionInterpreter(); + var exception = interpreter.Interpret(pythonException, NullExceptionInterpreter.Instance); + + Assert.That(exception.Message, Does.Contain("required by the parsed_name method")); + Assert.That(exception.Message, Does.Contain("The following overloads are available:")); + Assert.That(exception.Message, Does.Contain("parsed_name(x: str)")); + } + + private static PythonException ThrowFixtureMethod(string methodName) + { + using (Py.GIL()) + { + var module = Py.Import("Test_PythonExceptionInterpreter"); + var algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke(); + return Assert.Throws(() => algorithm.GetAttr(methodName).Invoke()); + } + } + private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type); } } diff --git a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py index de3b05ca8faf..2ddea10223d8 100644 --- a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py +++ b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py @@ -29,6 +29,22 @@ def no_method_match_rsi(self): # so no RSI overload matches the given arguments. self._indicator = self.rsi(symbol, 15, Resolution.DAILY) + def no_method_match_with_structured_attributes(self): + # Synthesizes the TypeError pythonnet raises on a bind failure, carrying the + # structured attributes but a different method name in the message, so tests + # can verify the attributes take precedence over message parsing. + error = TypeError("No method matches given arguments for parsed_name: (). " + "The following overloads are available:\n parsed_name(x: str)") + error._clr_method_name = 'structured_name' + error._clr_overloads_hint = 'The expected signature is:\n structured_name(x: int)' + raise error + + def no_method_match_without_structured_attributes(self): + # The bind-failure message shape from pythonnet versions that do not attach + # the structured attributes; the interpreter must fall back to parsing it. + raise TypeError("No method matches given arguments for parsed_name: (). " + "The following overloads are available:\n parsed_name(x: str)") + def unsupported_operand(self): x = None + "Pepe Grillo" From 9883957a6967d1e37bda87882625301f80d26dc2 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 13:11:42 -0400 Subject: [PATCH 2/2] Tighten comments on the structured bind-failure read --- .../NoMethodMatchPythonExceptionInterpreter.cs | 17 ++++++----------- ...ethodMatchPythonExceptionInterpreterTests.cs | 6 ++---- .../Test_PythonExceptionInterpreter.py | 7 ++----- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs b/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs index 6e3aaf342d01..90d2d6fbc795 100644 --- a/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs +++ b/Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs @@ -25,9 +25,8 @@ namespace QuantConnect.Exceptions public class NoMethodMatchPythonExceptionInterpreter : PythonExceptionInterpreter { /// - /// Attribute names pythonnet attaches to the bind-failure TypeError, carrying the - /// data its message is built from: the snake_case method name and the rendered - /// overloads hint block exactly as it appears at the end of the message. + /// Attributes pythonnet attaches to the bind-failure TypeError with the data its + /// message is built from: the method name and the overloads hint block. /// private const string MethodNameAttribute = "_clr_method_name"; private const string OverloadsHintAttribute = "_clr_overloads_hint"; @@ -58,8 +57,6 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i { var pe = (PythonException)exception; - // Prefer the structured data pythonnet attaches to the bind-failure TypeError, - // falling back to parsing the message for versions that do not attach it. TryGetStructuredBindFailureData(pe, out var methodName, out var overloadsHint); methodName ??= GetMethodName(pe.Message); @@ -77,10 +74,9 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i } /// - /// Reads the structured bind-failure data pythonnet attaches to the TypeError so - /// the method name and overloads hint do not have to be parsed out of the message. - /// Both outputs are null when the attributes are absent (raised by a pythonnet - /// version that does not attach them) or cannot be read. + /// Reads the structured bind-failure data pythonnet attaches to the TypeError. + /// Outputs are null when the attributes are absent (older pythonnet) or unreadable, + /// so the caller falls back to parsing the message. /// private static void TryGetStructuredBindFailureData(PythonException exception, out string methodName, out string overloadsHint) @@ -110,7 +106,7 @@ private static void TryGetStructuredBindFailureData(PythonException exception, o } } - // Normalize so the caller's null-coalescing fallback kicks in + // Empty means absent so the caller's null-coalescing fallback kicks in if (string.IsNullOrEmpty(methodName)) { methodName = null; @@ -122,7 +118,6 @@ private static void TryGetStructuredBindFailureData(PythonException exception, o } catch { - // Fall back to message parsing on any failure reading the attributes methodName = null; overloadsHint = null; } diff --git a/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs index c62cf09ef161..18fb8cc7d7ca 100644 --- a/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs +++ b/Tests/Common/Exceptions/NoMethodMatchPythonExceptionInterpreterTests.cs @@ -162,8 +162,7 @@ public void VerifyMessageKeepsTheExpectedSignatureHint() [Test] public void InterpretPrefersStructuredAttributesOverMessageParsing() { - // The fixture raises a TypeError whose message names 'parsed_name' but whose - // structured attributes name 'structured_name'; the attributes must win. + // The fixture's message names 'parsed_name' but its attributes name 'structured_name' var pythonException = ThrowFixtureMethod("no_method_match_with_structured_attributes"); var interpreter = new NoMethodMatchPythonExceptionInterpreter(); @@ -178,8 +177,7 @@ public void InterpretPrefersStructuredAttributesOverMessageParsing() [Test] public void InterpretFallsBackToMessageParsingWithoutStructuredAttributes() { - // Same message shape but no structured attributes (older pythonnet versions): - // the interpreter must extract the method name and hint from the message. + // Same message shape but no attributes, as raised by older pythonnet versions var pythonException = ThrowFixtureMethod("no_method_match_without_structured_attributes"); var interpreter = new NoMethodMatchPythonExceptionInterpreter(); diff --git a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py index 2ddea10223d8..ab65c6042921 100644 --- a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py +++ b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py @@ -30,9 +30,7 @@ def no_method_match_rsi(self): self._indicator = self.rsi(symbol, 15, Resolution.DAILY) def no_method_match_with_structured_attributes(self): - # Synthesizes the TypeError pythonnet raises on a bind failure, carrying the - # structured attributes but a different method name in the message, so tests - # can verify the attributes take precedence over message parsing. + # A bind-failure TypeError whose attributes name a different method than its message error = TypeError("No method matches given arguments for parsed_name: (). " "The following overloads are available:\n parsed_name(x: str)") error._clr_method_name = 'structured_name' @@ -40,8 +38,7 @@ def no_method_match_with_structured_attributes(self): raise error def no_method_match_without_structured_attributes(self): - # The bind-failure message shape from pythonnet versions that do not attach - # the structured attributes; the interpreter must fall back to parsing it. + # The message-only shape raised by pythonnet versions without the attributes raise TypeError("No method matches given arguments for parsed_name: (). " "The following overloads are available:\n parsed_name(x: str)")