Skip to content
Merged
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
27 changes: 25 additions & 2 deletions Common/Exceptions/NoMethodMatchPythonExceptionInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,36 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i
{
var pe = (PythonException)exception;

var startIndex = pe.Message.LastIndexOfInvariant(" ");
var methodName = pe.Message.Substring(startIndex).Trim();
var methodName = GetMethodName(pe.Message);
var message = Messages.NoMethodMatchPythonExceptionInterpreter.AttemptedToAccessMethodThatDoesNotExist(methodName);

message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);

return new MissingMethodException(message, pe);
}

/// <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})",
/// so the method name sits between the "for " keyword and the following ":".
/// </summary>
private static string GetMethodName(string exceptionMessage)
{
const string forKeyword = "for ";
var forIndex = exceptionMessage.IndexOfInvariant(forKeyword);
if (forIndex == -1)
{
// Unexpected format, fall back to the whole message
return exceptionMessage.Trim();
}

var methodNameStart = forIndex + forKeyword.Length;
var colonIndex = exceptionMessage.IndexOf(':', methodNameStart);
var methodName = colonIndex > methodNameStart
? exceptionMessage.Substring(methodNameStart, colonIndex - methodNameStart)
: exceptionMessage.Substring(methodNameStart);

return methodName.Trim();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,42 @@ public void VerifyMessageContainsStackTraceInformation()
Assert.True(exception.Message.Contains("self.set_cash('SPY')"));
}

[Test]
public void VerifyMessageContainsTheMethodName()
{
var exception = CreateExceptionFromType(typeof(PythonException));
var interpreter = new NoMethodMatchPythonExceptionInterpreter();
exception = interpreter.Interpret(exception, NullExceptionInterpreter.Instance);

// The interpreter should reference the actual method name that failed to resolve (SetCash),
// not a fragment of the argument type list (e.g. "'str'>)").
Assert.That(exception.Message, Does.Contain("SetCash"));
Assert.That(exception.Message, Does.Not.Contain(">)"));
}

[Test]
public void VerifyMessageContainsTheMethodNameForOverloadedMethod()
{
PythonException pythonException;
using (Py.GIL())
{
var module = Py.Import("Test_PythonExceptionInterpreter");
dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();

// self.rsi(symbol, 15, Resolution.DAILY) -- the third argument should be a
// MovingAverageType, so no RSI overload matches the given arguments.
pythonException = Assert.Throws<PythonException>(() => algorithm.no_method_match_rsi());
}

var interpreter = new NoMethodMatchPythonExceptionInterpreter();
var exception = interpreter.Interpret(pythonException, NullExceptionInterpreter.Instance);

// The interpreter should reference the RSI method name, not a fragment of the
// argument type list (e.g. "'QuantConnect.Resolution'>)").
Assert.That(exception.Message, Does.Contain("RSI"));
Assert.That(exception.Message, Does.Not.Contain(">)"));
}

private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
}
}
6 changes: 6 additions & 0 deletions Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def key_error(self):
def no_method_match(self):
self.set_cash('SPY')

def no_method_match_rsi(self):
symbol = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
# The third positional argument should be a MovingAverageType, not a Resolution,
# so no RSI overload matches the given arguments.
self._indicator = self.rsi(symbol, 15, Resolution.DAILY)

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

Expand Down
Loading