Skip to content

Commit

Permalink
Add Pythonnet's ClrBubbledException interpreter (#7523)
Browse files Browse the repository at this point in the history
* Add ClrBubbleExceptionInterpreter

* Add ClrBubbledExceptionInterpreter tests

* Bump pythonnet version to 2.0.23

* Minor changes

* Minor change

* Minor fix

* Fix failing unit tests
  • Loading branch information
jhonabreul authored Oct 20, 2023
1 parent fff037b commit 1df11c0
Show file tree
Hide file tree
Showing 22 changed files with 229 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<DebugType>portable</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="Accord" Version="3.6.0" />
<PackageReference Include="Accord.Fuzzy" Version="3.6.0" />
<PackageReference Include="Accord.MachineLearning" Version="3.6.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="Accord" Version="3.6.0" />
<PackageReference Include="Accord.Math" Version="3.6.0" />
<PackageReference Include="Accord.Statistics" Version="3.6.0" />
Expand Down
2 changes: 1 addition & 1 deletion Algorithm.Python/QuantConnect.Algorithm.Python.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<Compile Include="..\Common\Properties\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
</ItemGroup>
<ItemGroup>
<Content Include="AccumulativeInsightPortfolioRegressionAlgorithm.py" />
Expand Down
2 changes: 1 addition & 1 deletion Algorithm/QuantConnect.Algorithm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NodaTime" Version="3.0.5" />
Expand Down
2 changes: 1 addition & 1 deletion AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="NodaTime" Version="3.0.5" />
</ItemGroup>
<ItemGroup>
Expand Down
54 changes: 54 additions & 0 deletions Common/Exceptions/ClrBubbledExceptionInterpreter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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
{
/// <summary>
/// Interprets <see cref="ClrBubbledException"/> instances
/// </summary>
public class ClrBubbledExceptionInterpreter : SystemExceptionInterpreter
{
/// <summary>
/// Determines the order that an instance of this class should be called
/// </summary>
public override int Order => int.MaxValue - 1;

/// <summary>
/// Determines if this interpreter should be applied to the specified exception. f
/// </summary>
/// <param name="exception">The exception to check</param>
/// <returns>True if the exception can be interpreted, false otherwise</returns>
public override bool CanInterpret(Exception exception) => exception?.GetType() == typeof(ClrBubbledException);

/// <summary>
/// Interprets the specified exception into a new exception
/// </summary>
/// <param name="exception">The exception to be interpreted</param>
/// <param name="innerInterpreter">An interpreter that should be applied to the inner exception.</param>
/// <returns>The interpreted exception</returns>
public override Exception Interpret(Exception exception, IExceptionInterpreter innerInterpreter)
{
var pe = (ClrBubbledException)exception;
var sanitized = base.Interpret(exception, innerInterpreter);
var inner = sanitized.InnerException ?? sanitized;

return new Exception(pe.Message + PythonUtil.PythonExceptionStackParser(pe.PythonTraceback), inner);
}
}
}
6 changes: 3 additions & 3 deletions Common/Exceptions/SystemExceptionInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ public class SystemExceptionInterpreter : IExceptionInterpreter
/// <summary>
/// Determines the order that an instance of this class should be called
/// </summary>
public int Order => int.MaxValue;
public virtual int Order => int.MaxValue;

/// <summary>
/// Determines if this interpreter should be applied to the specified exception. f
/// </summary>
/// <param name="exception">The exception to check</param>
/// <returns>True if the exception can be interpreted, false otherwise</returns>
public bool CanInterpret(Exception exception) => true;
public virtual bool CanInterpret(Exception exception) => true;

/// <summary>
/// Interprets the specified exception into a new exception
/// </summary>
/// <param name="exception">The exception to be interpreted</param>
/// <param name="innerInterpreter">An interpreter that should be applied to the inner exception.</param>
/// <returns>The interpreted exception</returns>
public Exception Interpret(Exception exception, IExceptionInterpreter innerInterpreter)
public virtual Exception Interpret(Exception exception, IExceptionInterpreter innerInterpreter)
{
var sanitized = new SanitizedException(exception.Message, exception.StackTrace);

Expand Down
2 changes: 1 addition & 1 deletion Common/QuantConnect.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<Message Text="SelectedOptimization $(SelectedOptimization)" Importance="high" />
</Target>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="CloneExtensions" Version="1.3.0" />
<PackageReference Include="fasterflect" Version="3.0.0" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
Expand Down
2 changes: 1 addition & 1 deletion Engine/QuantConnect.Lean.Engine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<Message Text="SelectedOptimization $(SelectedOptimization)" Importance="high" />
</Target>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="fasterflect" Version="3.0.0" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
Expand Down
2 changes: 1 addition & 1 deletion Indicators/QuantConnect.Indicators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<Message Text="SelectedOptimization $(SelectedOptimization)" Importance="high" />
</Target>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Report/QuantConnect.Report.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="Deedle" Version="2.1.0" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
Expand Down
2 changes: 1 addition & 1 deletion Research/QuantConnect.Research.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<ItemGroup>
<PackageReference Include="Plotly.NET" Version="3.0.1" />
<PackageReference Include="Plotly.NET.Interactive" Version="3.0.2" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.22" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.23" />
<PackageReference Include="NodaTime" Version="3.0.5" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ public void PortfolioBiasIsRespected(Language language, PortfolioBias bias)
{
if (bias == PortfolioBias.Short)
{
var exception = Assert.Throws<ArgumentException>(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily));
Assert.That(exception.Message, Is.EqualTo("Long position must be allowed in MeanReversionPortfolioConstructionModel."));
var throwsConstraint = language == Language.CSharp
? Throws.InstanceOf<ArgumentException>()
: Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>();
Assert.That(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily),
throwsConstraint.And.Message.EqualTo("Long position must be allowed in MeanReversionPortfolioConstructionModel."));
return;
}

Expand Down Expand Up @@ -304,8 +307,10 @@ public void SimplexProjectionPython(double regulator)

if (regulator <= 0)
{
var exception = Assert.Throws<ArgumentException>(() => model.InvokeMethod("SimplexProjection", _simplexTestArray.ToPython(), new PyFloat(regulator)));
Assert.That(exception.Message, Is.EqualTo("Total must be > 0 for Euclidean Projection onto the Simplex."));
Assert.That(() => model.InvokeMethod("SimplexProjection", _simplexTestArray.ToPython(), new PyFloat(regulator)),
Throws.InstanceOf<ClrBubbledException>()
.With.InnerException.InstanceOf<ArgumentException>()
.And.Message.EqualTo("Total must be > 0 for Euclidean Projection onto the Simplex."));
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ public void PortfolioBiasIsRespected(Language language, PortfolioBias bias)
{
if (bias == PortfolioBias.Short)
{
var exception = Assert.Throws<ArgumentException>(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily));
Assert.That(exception.Message, Is.EqualTo("Long position must be allowed in RiskParityPortfolioConstructionModel."));
var throwsConstraint = language == Language.CSharp
? Throws.InstanceOf<ArgumentException>()
: Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>();
Assert.That(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily),
throwsConstraint.And.Message.EqualTo("Long position must be allowed in RiskParityPortfolioConstructionModel."));
return;
}

Expand Down
23 changes: 17 additions & 6 deletions Tests/Common/Data/SliceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,9 @@ from AlgorithmImports import *
def Test(slice):
slice.clear()").GetAttr("Test");

Assert.Throws<InvalidOperationException>(() => test(GetPythonSlice()), "Slice is read-only: cannot clear the collection");
Assert.That(() => test(GetPythonSlice()),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<InvalidOperationException>(),
"Slice is read-only: cannot clear the collection");
}
}

Expand All @@ -842,7 +844,9 @@ from AlgorithmImports import *
def Test(slice):
slice.popitem()").GetAttr("Test");

Assert.Throws<NotSupportedException>(() => test(GetPythonSlice()), "Slice is read-only: cannot pop an item from the collection");
Assert.That(() => test(GetPythonSlice()),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<NotSupportedException>(),
$"Slice is read-only: cannot pop the value for {Symbols.SPY} from the collection");
}
}

Expand All @@ -858,7 +862,9 @@ from AlgorithmImports import *
def Test(slice, symbol):
slice.pop(symbol)").GetAttr("Test");

Assert.Throws<InvalidOperationException>(() => test(GetPythonSlice(), Symbols.SPY), $"Slice is read-only: cannot pop the value for {Symbols.SPY} from the collection");
Assert.That(() => test(GetPythonSlice(), Symbols.SPY),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<InvalidOperationException>(),
$"Slice is read-only: cannot pop the value for {Symbols.SPY} from the collection");
}
}

Expand All @@ -874,7 +880,9 @@ from AlgorithmImports import *
def Test(slice, symbol, default_value):
slice.pop(symbol, default_value)").GetAttr("Test");

Assert.Throws<InvalidOperationException>(() => test(GetPythonSlice(), Symbols.SPY, null), $"Slice is read-only: cannot pop the value for {Symbols.SPY} from the collection");
Assert.That(() => test(GetPythonSlice(), Symbols.SPY, null),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<InvalidOperationException>(),
$"Slice is read-only: cannot pop the value for {Symbols.SPY} from the collection");
}
}

Expand All @@ -891,7 +899,9 @@ def Test(slice, symbol):
item = { symbol: 1 }
slice.update(item)").GetAttr("Test");

Assert.Throws<InvalidOperationException>(() => test(GetPythonSlice(), Symbols.SPY), "Slice is read-only: cannot update the collection");
Assert.That(() => test(GetPythonSlice(), Symbols.SPY),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<InvalidOperationException>(),
"Slice is read-only: cannot update the collection");
}
}

Expand Down Expand Up @@ -1336,7 +1346,8 @@ def Test(slice, symbol):
return slice.setdefault(symbol)").GetAttr("Test");

var symbol = Symbols.EURUSD;
Assert.Throws<KeyNotFoundException>(() => test(GetPythonSlice(), symbol),
Assert.That(() => test(GetPythonSlice(), symbol),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<KeyNotFoundException>(),
$"Slice is read-only: cannot set default value to for {symbol}");
}
}
Expand Down
117 changes: 117 additions & 0 deletions Tests/Common/Exceptions/ClrBubbledExceptionInterpreterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 ClrBubbledExceptionInterpreterTests
{
private string _pythonModuleName = "Test_PythonExceptionInterpreter";
private ClrBubbledException _dotnetException;
private PythonException _pythonException;

[OneTimeSetUp]
public void Setup()
{
using (Py.GIL())
{
var module = Py.Import(_pythonModuleName);
dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();

try
{
// self.MarketOrder(null, 1)
algorithm.dotnet_error();
}
catch (ClrBubbledException e)
{
_dotnetException = e;
}

try
{
// x = 1 / 0
algorithm.zero_division_error();
}
catch (PythonException e)
{
_pythonException = e;
}

Assert.IsNotNull(_dotnetException);
Assert.IsNotNull(_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 = false)]
[TestCase(typeof(ClrBubbledException), ExpectedResult = true)]
public bool CanInterpretReturnsTrueOnlyForClrBubbledExceptionType(Type exceptionType)
{
var exception = CreateExceptionFromType(exceptionType);
return new ClrBubbledExceptionInterpreter().CanInterpret(exception);
}

[Test]
[TestCase(typeof(Exception), true)]
[TestCase(typeof(KeyNotFoundException), true)]
[TestCase(typeof(DivideByZeroException), true)]
[TestCase(typeof(InvalidOperationException), true)]
[TestCase(typeof(PythonException), true)]
[TestCase(typeof(ClrBubbledException), false)]
public void InterpretThrowsForNonClrBubbledExceptionTypes(Type exceptionType, bool expectThrow)
{
var exception = CreateExceptionFromType(exceptionType);
var interpreter = new ClrBubbledExceptionInterpreter();
var constraint = expectThrow ? (IResolveConstraint)Throws.Exception : Throws.Nothing;
Assert.That(() => interpreter.Interpret(exception, NullExceptionInterpreter.Instance), constraint);
}

[Test]
public void VerifyMessageContainsStackTraceInformation()
{
var exception = CreateExceptionFromType(typeof(ClrBubbledException));
var assembly = typeof(ClrBubbledExceptionInterpreter).Assembly;
var interpreter = StackExceptionInterpreter.CreateFromAssemblies(new[] { assembly });
exception = interpreter.Interpret(exception, NullExceptionInterpreter.Instance);

Assert.True(exception.Message.Contains("Value cannot be null. (Parameter 'key')", StringComparison.InvariantCulture));
Assert.True(exception.Message.Contains("at dotnet_error", StringComparison.InvariantCulture));
Assert.True(exception.Message.Contains("self.MarketOrder(None", StringComparison.InvariantCulture));
Assert.True(exception.Message.Contains($"in {_pythonModuleName}.py: line ", StringComparison.InvariantCulture));
}

private Exception CreateExceptionFromType(Type type)
{
if (type == typeof(ClrBubbledException))
{
return _dotnetException;
}

return type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
}
}
}
Loading

0 comments on commit 1df11c0

Please sign in to comment.