diff --git a/Common/Exceptions/ModuleNotFoundPythonExceptionInterpreter.cs b/Common/Exceptions/ModuleNotFoundPythonExceptionInterpreter.cs
new file mode 100644
index 000000000000..b04d47ad542b
--- /dev/null
+++ b/Common/Exceptions/ModuleNotFoundPythonExceptionInterpreter.cs
@@ -0,0 +1,83 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Python.Runtime;
+using QuantConnect.Util;
+
+namespace QuantConnect.Exceptions
+{
+ ///
+ /// Interprets Python ModuleNotFoundError exceptions
+ ///
+ public class ModuleNotFoundPythonExceptionInterpreter : PythonExceptionInterpreter
+ {
+ private static readonly Regex _camelCasedNameRegex = new Regex(@"^[A-Z][a-zA-Z0-9]*(\.[A-Z][a-zA-Z0-9]*)*$", RegexOptions.Compiled);
+
+ ///
+ /// Determines the order that an instance of this class should be called
+ ///
+ public override int Order => 0;
+
+ ///
+ /// Determines if this interpreter should be applied to the specified exception.
+ ///
+ /// The exception to check
+ /// True if the exception can be interpreted, false otherwise
+ public override bool CanInterpret(Exception exception)
+ {
+ var pythonException = exception as PythonException;
+ if (pythonException == null)
+ {
+ return false;
+ }
+
+ using (Py.GIL())
+ {
+ return base.CanInterpret(exception) &&
+ pythonException.Type.Name.Equals("ModuleNotFoundError", StringComparison.InvariantCultureIgnoreCase);
+ }
+ }
+
+ ///
+ /// Interprets the specified exception into a new exception
+ ///
+ /// The exception to be interpreted
+ /// An interpreter that should be applied to the inner exception.
+ /// The interpreted exception
+ public override Exception Interpret(Exception exception, IExceptionInterpreter innerInterpreter)
+ {
+ var pe = (PythonException)exception;
+
+ var moduleName = pe.Message.GetStringBetweenChars('\'', '\'');
+ var message = Messages.ModuleNotFoundPythonExceptionInterpreter.ModuleNotFound(moduleName, IsCamelCased(moduleName));
+ message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
+
+ return new Exception(message, pe);
+ }
+
+ ///
+ /// Determines whether the module name follows .NET namespace casing (e.g. "QuantConnect.Indicators"):
+ /// every dot-separated segment starts with an uppercase letter and the name contains at least one
+ /// lowercase letter, so all-uppercase Python packages like "PIL" are not treated as assemblies.
+ ///
+ private static bool IsCamelCased(string moduleName)
+ {
+ return _camelCasedNameRegex.IsMatch(moduleName) && moduleName.Any(char.IsLower);
+ }
+ }
+}
diff --git a/Common/Exceptions/MultipleInheritancePythonExceptionInterpreter.cs b/Common/Exceptions/MultipleInheritancePythonExceptionInterpreter.cs
new file mode 100644
index 000000000000..67f9661a7718
--- /dev/null
+++ b/Common/Exceptions/MultipleInheritancePythonExceptionInterpreter.cs
@@ -0,0 +1,60 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using Python.Runtime;
+using QuantConnect.Util;
+
+namespace QuantConnect.Exceptions
+{
+ ///
+ /// Interprets Python TypeError exceptions caused by inheriting from multiple classes
+ /// when one of them is a managed (C#) class
+ ///
+ public class MultipleInheritancePythonExceptionInterpreter : PythonExceptionInterpreter
+ {
+ ///
+ /// Determines the order that an instance of this class should be called
+ ///
+ public override int Order => 0;
+
+ ///
+ /// Determines if this interpreter should be applied to the specified exception.
+ ///
+ /// The exception to check
+ /// True if the exception can be interpreted, false otherwise
+ public override bool CanInterpret(Exception exception)
+ {
+ return base.CanInterpret(exception) &&
+ exception.Message.Contains(Messages.MultipleInheritancePythonExceptionInterpreter.MultipleInheritanceExpectedSubstring);
+ }
+
+ ///
+ /// Interprets the specified exception into a new exception
+ ///
+ /// The exception to be interpreted
+ /// An interpreter that should be applied to the inner exception.
+ /// The interpreted exception
+ public override Exception Interpret(Exception exception, IExceptionInterpreter innerInterpreter)
+ {
+ var pe = (PythonException)exception;
+
+ var message = Messages.MultipleInheritancePythonExceptionInterpreter.InvalidMultipleInheritance;
+ message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
+
+ return new Exception(message, pe);
+ }
+ }
+}
diff --git a/Common/Messages/Messages.Exceptions.cs b/Common/Messages/Messages.Exceptions.cs
index 61bea9a86430..1cda211c8f05 100644
--- a/Common/Messages/Messages.Exceptions.cs
+++ b/Common/Messages/Messages.Exceptions.cs
@@ -90,6 +90,47 @@ public static string KeyNotFoundInCollection(string key)
}
}
+ ///
+ /// Provides user-facing messages for the class and its consumers or related classes
+ ///
+ public static class ModuleNotFoundPythonExceptionInterpreter
+ {
+ ///
+ /// Returns a string message saying the given module could not be found, with advice on how to fix it.
+ /// The .NET assembly advice is only included when the module name looks like a .NET namespace
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string ModuleNotFound(string moduleName, bool isModuleNameCamelCased)
+ {
+ var message = $"No module named '{moduleName}'. If it is a Python package, ensure it is installed in the environment.";
+ if (isModuleNameCamelCased)
+ {
+ message += " If it is a .NET assembly, importing it is not supported, use an equivalent Python package instead.";
+ }
+ return message;
+ }
+ }
+
+ ///
+ /// Provides user-facing messages for the class and its consumers or related classes
+ ///
+ public static class MultipleInheritancePythonExceptionInterpreter
+ {
+ ///
+ /// String message saying: cannot use multiple inheritance with managed classes
+ ///
+ public static string MultipleInheritanceExpectedSubstring = "cannot use multiple inheritance with managed classes";
+
+ ///
+ /// String message saying a Python class cannot inherit from multiple classes when one of them is a C# class.
+ /// It also contains an advice on how to fix it
+ ///
+ public static string InvalidMultipleInheritance =
+ "A Python class cannot inherit from multiple classes when one of them is a C# class, like QCAlgorithm. " +
+ "Keep the C# class as the only base and move the other bases' members into helper classes used via " +
+ "composition (e.g. self._helper = MyHelper()).";
+ }
+
///
/// Provides user-facing messages for the class and its consumers or related classes
///
diff --git a/Tests/Common/Exceptions/ModuleNotFoundPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/ModuleNotFoundPythonExceptionInterpreterTests.cs
new file mode 100644
index 000000000000..4ff32efc5474
--- /dev/null
+++ b/Tests/Common/Exceptions/ModuleNotFoundPythonExceptionInterpreterTests.cs
@@ -0,0 +1,121 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using NUnit.Framework;
+using NUnit.Framework.Constraints;
+using Python.Runtime;
+using QuantConnect.Exceptions;
+using System;
+using System.Collections.Generic;
+
+namespace QuantConnect.Tests.Common.Exceptions
+{
+ [TestFixture]
+ public class ModuleNotFoundPythonExceptionInterpreterTests
+ {
+ private PythonException _pythonException;
+
+ [SetUp]
+ public void Setup()
+ {
+ using (Py.GIL())
+ {
+ var module = Py.Import("Test_PythonExceptionInterpreter");
+ dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();
+
+ try
+ {
+ // from MissingClrNamespace.Distributions import Normal
+ algorithm.module_not_found();
+ }
+ catch (PythonException pythonException)
+ {
+ _pythonException = pythonException;
+ }
+ }
+ }
+
+ [Test]
+ [TestCase(typeof(Exception), ExpectedResult = false)]
+ [TestCase(typeof(KeyNotFoundException), ExpectedResult = false)]
+ [TestCase(typeof(DivideByZeroException), ExpectedResult = false)]
+ [TestCase(typeof(InvalidOperationException), ExpectedResult = false)]
+ [TestCase(typeof(PythonException), ExpectedResult = true)]
+ public bool CanInterpretReturnsTrueForOnlyModuleNotFoundPythonExceptionType(Type exceptionType)
+ {
+ var exception = CreateExceptionFromType(exceptionType);
+ return new ModuleNotFoundPythonExceptionInterpreter().CanInterpret(exception);
+ }
+
+ [Test]
+ [TestCase(typeof(Exception), true)]
+ [TestCase(typeof(KeyNotFoundException), true)]
+ [TestCase(typeof(DivideByZeroException), true)]
+ [TestCase(typeof(InvalidOperationException), true)]
+ [TestCase(typeof(PythonException), false)]
+ public void InterpretThrowsForNonModuleNotFoundPythonExceptionTypes(Type exceptionType, bool expectThrow)
+ {
+ var exception = CreateExceptionFromType(exceptionType);
+ var interpreter = new ModuleNotFoundPythonExceptionInterpreter();
+ var constraint = expectThrow ? (IResolveConstraint)Throws.Exception : Throws.Nothing;
+ Assert.That(() => interpreter.Interpret(exception, NullExceptionInterpreter.Instance), constraint);
+ }
+
+ [Test]
+ public void VerifyMessageContainsModuleNameAndAdvice()
+ {
+ var exception = CreateExceptionFromType(typeof(PythonException));
+ var interpreter = StackExceptionInterpreter.CreateFromAssemblies();
+ exception = interpreter.Interpret(exception, NullExceptionInterpreter.Instance);
+ Assert.True(exception.Message.Contains("No module named 'MissingClrNamespace'"));
+ Assert.True(exception.Message.Contains("use an equivalent Python package instead"));
+ }
+
+ [Test]
+ [TestCase("FakeMissingAssembly", true)]
+ [TestCase("FakeMissingAssembly99", true)]
+ [TestCase("fake_missing_module", false)]
+ [TestCase("fakemissingmodule", false)]
+ [TestCase("FAKEMISSINGMODULE", false)]
+ [TestCase("Fake_Missing_Module", false)]
+ public void AddsDotNetAssemblyAdviceOnlyForCamelCasedModuleNames(string moduleName, bool expectDotNetAdvice)
+ {
+ PythonException pythonException = null;
+ using (Py.GIL())
+ {
+ try
+ {
+ Py.Import(moduleName);
+ }
+ catch (PythonException exception)
+ {
+ pythonException = exception;
+ }
+ }
+
+ Assert.IsNotNull(pythonException);
+
+ var interpreter = new ModuleNotFoundPythonExceptionInterpreter();
+ Assert.IsTrue(interpreter.CanInterpret(pythonException));
+
+ var interpretedException = interpreter.Interpret(pythonException, NullExceptionInterpreter.Instance);
+ StringAssert.Contains($"No module named '{moduleName}'", interpretedException.Message);
+ StringAssert.Contains("ensure it is installed in the environment", interpretedException.Message);
+ Assert.AreEqual(expectDotNetAdvice, interpretedException.Message.Contains(".NET assembly", StringComparison.Ordinal));
+ }
+
+ private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
+ }
+}
diff --git a/Tests/Common/Exceptions/MultipleInheritancePythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/MultipleInheritancePythonExceptionInterpreterTests.cs
new file mode 100644
index 000000000000..876971c40276
--- /dev/null
+++ b/Tests/Common/Exceptions/MultipleInheritancePythonExceptionInterpreterTests.cs
@@ -0,0 +1,88 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using NUnit.Framework;
+using NUnit.Framework.Constraints;
+using Python.Runtime;
+using QuantConnect.Exceptions;
+using System;
+using System.Collections.Generic;
+
+namespace QuantConnect.Tests.Common.Exceptions
+{
+ [TestFixture]
+ public class MultipleInheritancePythonExceptionInterpreterTests
+ {
+ private PythonException _pythonException;
+
+ [SetUp]
+ public void Setup()
+ {
+ using (Py.GIL())
+ {
+ var module = Py.Import("Test_PythonExceptionInterpreter");
+ dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();
+
+ try
+ {
+ // class MultipleInheritanceAlgorithm(QCAlgorithm, MultipleInheritanceMixin)
+ algorithm.multiple_inheritance();
+ }
+ catch (PythonException pythonException)
+ {
+ _pythonException = pythonException;
+ }
+ }
+ }
+
+ [Test]
+ [TestCase(typeof(Exception), ExpectedResult = false)]
+ [TestCase(typeof(KeyNotFoundException), ExpectedResult = false)]
+ [TestCase(typeof(DivideByZeroException), ExpectedResult = false)]
+ [TestCase(typeof(InvalidOperationException), ExpectedResult = false)]
+ [TestCase(typeof(PythonException), ExpectedResult = true)]
+ public bool CanInterpretReturnsTrueForOnlyMultipleInheritancePythonExceptionType(Type exceptionType)
+ {
+ var exception = CreateExceptionFromType(exceptionType);
+ return new MultipleInheritancePythonExceptionInterpreter().CanInterpret(exception);
+ }
+
+ [Test]
+ [TestCase(typeof(Exception), true)]
+ [TestCase(typeof(KeyNotFoundException), true)]
+ [TestCase(typeof(DivideByZeroException), true)]
+ [TestCase(typeof(InvalidOperationException), true)]
+ [TestCase(typeof(PythonException), false)]
+ public void InterpretThrowsForNonMultipleInheritancePythonExceptionTypes(Type exceptionType, bool expectThrow)
+ {
+ var exception = CreateExceptionFromType(exceptionType);
+ var interpreter = new MultipleInheritancePythonExceptionInterpreter();
+ var constraint = expectThrow ? (IResolveConstraint)Throws.Exception : Throws.Nothing;
+ Assert.That(() => interpreter.Interpret(exception, NullExceptionInterpreter.Instance), constraint);
+ }
+
+ [Test]
+ public void VerifyMessageContainsAdviceAndStackTraceInformation()
+ {
+ var exception = CreateExceptionFromType(typeof(PythonException));
+ var interpreter = StackExceptionInterpreter.CreateFromAssemblies();
+ exception = interpreter.Interpret(exception, NullExceptionInterpreter.Instance);
+ Assert.True(exception.Message.Contains("composition"));
+ Assert.True(exception.Message.Contains("MultipleInheritanceAlgorithm(QCAlgorithm, MultipleInheritanceMixin)"));
+ }
+
+ 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 f1a1d088f8cb..de3b05ca8faf 100644
--- a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
+++ b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
@@ -32,6 +32,15 @@ def no_method_match_rsi(self):
def unsupported_operand(self):
x = None + "Pepe Grillo"
+ def module_not_found(self):
+ from MissingClrNamespace.Distributions import Normal
+
+ def multiple_inheritance(self):
+ class MultipleInheritanceMixin:
+ pass
+ class MultipleInheritanceAlgorithm(QCAlgorithm, MultipleInheritanceMixin):
+ pass
+
def zero_division_error(self):
x = 1 / 0