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
63 changes: 61 additions & 2 deletions Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ namespace QuantConnect.Exceptions
/// </summary>
public class NoMethodMatchPythonExceptionInterpreter : PythonExceptionInterpreter
{
/// <summary>
/// Attributes pythonnet attaches to the bind-failure TypeError with the data its
/// message is built from: the method name and the overloads hint block.
/// </summary>
private const string MethodNameAttribute = "_clr_method_name";
private const string OverloadsHintAttribute = "_clr_overloads_hint";

/// <summary>
/// Determines the order that an instance of this class should be called
/// </summary>
Expand All @@ -50,10 +57,12 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i
{
var pe = (PythonException)exception;

var methodName = GetMethodName(pe.Message);
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}";
Expand All @@ -64,6 +73,56 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i
return new MissingMethodException(message, pe);
}

/// <summary>
/// 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.
/// </summary>
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<string>();
}
if (value.HasAttr(OverloadsHintAttribute))
{
using var hintAttribute = value.GetAttr(OverloadsHintAttribute);
overloadsHint = hintAttribute.As<string>();
}
}

// Empty means absent so the caller's null-coalescing fallback kicks in
if (string.IsNullOrEmpty(methodName))
{
methodName = null;
}
if (string.IsNullOrEmpty(overloadsHint))
{
overloadsHint = null;
}
}
catch
{
methodName = null;
overloadsHint = null;
}
}

/// <summary>
/// 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})",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,45 @@ public void VerifyMessageKeepsTheExpectedSignatureHint()
Assert.That(exception.Message, Does.Contain("rsi("));
}

[Test]
public void InterpretPrefersStructuredAttributesOverMessageParsing()
{
// 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();
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 attributes, as raised by older pythonnet versions
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<PythonException>(() => algorithm.GetAttr(methodName).Invoke());
}
}

private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
}
}
13 changes: 13 additions & 0 deletions Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ 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):
# A bind-failure TypeError whose attributes name a different method than its message
error = TypeError("No method matches given arguments for parsed_name: (<class 'str'>). "
"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 message-only shape raised by pythonnet versions without the attributes
raise TypeError("No method matches given arguments for parsed_name: (<class 'str'>). "
"The following overloads are available:\n parsed_name(x: str)")

def unsupported_operand(self):
x = None + "Pepe Grillo"

Expand Down
Loading