diff --git a/Common/Data/Market/DataDictionary.cs b/Common/Data/Market/DataDictionary.cs
index f41ea0017828..4e4e36b36811 100644
--- a/Common/Data/Market/DataDictionary.cs
+++ b/Common/Data/Market/DataDictionary.cs
@@ -81,7 +81,7 @@ public override T this[Symbol symbol]
return data;
}
CheckForImplicitlyCreatedSymbol(symbol);
- throw new KeyNotFoundException($"'{symbol}' wasn't found in the {GetType().GetBetterTypeName()} object, likely because there was no-data at this moment in time and it wasn't possible to fillforward historical data. Please check the data exists before accessing it with data.ContainsKey(\"{symbol}\")");
+ throw new KeyNotFoundException(Messages.ExtendedDictionary.KeyNotFoundDueToNoData(this, symbol));
}
set
{
diff --git a/Common/Data/Slice.cs b/Common/Data/Slice.cs
index 18c5a3ade2fb..c64350b51a78 100644
--- a/Common/Data/Slice.cs
+++ b/Common/Data/Slice.cs
@@ -340,7 +340,7 @@ public override dynamic this[Symbol symbol]
return value.GetData();
}
CheckForImplicitlyCreatedSymbol(symbol);
- throw new KeyNotFoundException($"'{symbol}' wasn't found in the Slice object, likely because there was no-data at this moment in time and it wasn't possible to fillforward historical data. Please check the data exists before accessing it with data.ContainsKey(\"{symbol}\")");
+ throw new KeyNotFoundException(Messages.ExtendedDictionary.KeyNotFoundDueToNoData(this, symbol));
}
set
{
diff --git a/Common/Exceptions/AttributeErrorPythonExceptionInterpreter.cs b/Common/Exceptions/AttributeErrorPythonExceptionInterpreter.cs
new file mode 100644
index 000000000000..d5a5353dde90
--- /dev/null
+++ b/Common/Exceptions/AttributeErrorPythonExceptionInterpreter.cs
@@ -0,0 +1,116 @@
+/*
+ * 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.Text.RegularExpressions;
+using Python.Runtime;
+using QuantConnect.Util;
+
+namespace QuantConnect.Exceptions
+{
+ ///
+ /// Interprets Python AttributeError exceptions caused by accessing an attribute of the wrong bar type,
+ /// e.g. reading 'volume' off a QuoteBar (quote-only subscriptions deliver QuoteBars in the slice) or
+ /// bid/ask attributes off a TradeBar. Only fires when it has a targeted hint for the failed attribute;
+ /// all other AttributeErrors keep the default interpretation.
+ ///
+ public class AttributeErrorPythonExceptionInterpreter : PythonExceptionInterpreter
+ {
+ // Python renders these errors as "'QuoteBar' object has no attribute 'volume'",
+ // optionally followed by a "Did you mean: ..." suggestion (Python 3.10+)
+ private static readonly Regex AttributeErrorRegex = new(
+ @"'(?\w+)' object has no attribute '(?\w+)'", 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())
+ {
+ if (!base.CanInterpret(exception) ||
+ !pythonException.Type.Name.Contains("AttributeError", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ return TryGetHint(pythonException.Message, out _);
+ }
+
+ ///
+ /// 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;
+
+ TryGetHint(pe.Message, out var hint);
+ var message = $"{pe.Message.Trim()} {hint}";
+ message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
+
+ return new MissingMemberException(message, pe);
+ }
+
+ ///
+ /// Gets the wrong-bar-type hint for the given AttributeError message, if there is one
+ ///
+ private static bool TryGetHint(string exceptionMessage, out string hint)
+ {
+ hint = null;
+ var match = AttributeErrorRegex.Match(exceptionMessage ?? string.Empty);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ var type = match.Groups["type"].Value;
+ // both snake cased ('bid_size') and C# style ('BidSize') accesses raise the same error shape
+ var attribute = match.Groups["attribute"].Value;
+ var normalizedAttribute = attribute.Replace("_", string.Empty, StringComparison.InvariantCulture).ToLowerInvariant();
+
+ if (type == "QuoteBar" && normalizedAttribute == "volume")
+ {
+ hint = Messages.AttributeErrorPythonExceptionInterpreter.QuoteBarHasNoTradeData(attribute);
+ return true;
+ }
+
+ if (type == "TradeBar" && normalizedAttribute is "bid" or "ask" or "bidprice" or "askprice"
+ or "bidsize" or "asksize" or "lastbidsize" or "lastasksize")
+ {
+ hint = Messages.AttributeErrorPythonExceptionInterpreter.TradeBarHasNoQuoteData(attribute);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Common/Exceptions/KeyErrorPythonExceptionInterpreter.cs b/Common/Exceptions/KeyErrorPythonExceptionInterpreter.cs
index 047413af3694..e702a5631a4f 100644
--- a/Common/Exceptions/KeyErrorPythonExceptionInterpreter.cs
+++ b/Common/Exceptions/KeyErrorPythonExceptionInterpreter.cs
@@ -59,14 +59,52 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i
{
var pe = (PythonException)exception;
+ // KeyError's message is the repr() of the missing key, so string keys come through quoted ("'SPY'")
+ // but object keys (e.g. a Symbol used on a plain dict) come through as
+ // "", which the quote/bracket parsing below can't handle and
+ // used to render a blank key in the final message ("ensure that the key exist in the collection").
+ // Read the key from the exception value instead, where str() yields a meaningful name in both cases.
+ // Depending on whether the error indicator was normalized, the value is either the KeyError instance
+ // (the key is args[0]) or the raw args tuple the exception was created with.
var key = string.Empty;
- if (pe.Message.Contains('[', StringComparison.InvariantCulture))
+ using (Py.GIL())
{
- key = pe.Message.GetStringBetweenChars('[', ']');
+ try
+ {
+ using var args = pe.Value != null && pe.Value.HasAttr("args") ? pe.Value.GetAttr("args") : null;
+ var container = args ?? pe.Value;
+ if (container != null && !container.IsNone())
+ {
+ if (PyTuple.IsTupleType(container))
+ {
+ if (container.Length() > 0)
+ {
+ using var firstArg = container[0];
+ key = firstArg.ToString();
+ }
+ }
+ else
+ {
+ key = container.ToString();
+ }
+ }
+ }
+ catch (PythonException)
+ {
+ // best effort, fall back to parsing the message below
+ }
}
- else if (pe.Message.Contains('\'', StringComparison.InvariantCulture))
+
+ if (string.IsNullOrWhiteSpace(key))
{
- key = pe.Message.GetStringBetweenChars('\'', '\'');
+ if (pe.Message.Contains('[', StringComparison.InvariantCulture))
+ {
+ key = pe.Message.GetStringBetweenChars('[', ']');
+ }
+ else if (pe.Message.Contains('\'', StringComparison.InvariantCulture))
+ {
+ key = pe.Message.GetStringBetweenChars('\'', '\'');
+ }
}
var message = Messages.KeyErrorPythonExceptionInterpreter.KeyNotFoundInCollection(key);
diff --git a/Common/ExtendedDictionary.cs b/Common/ExtendedDictionary.cs
index 42b6b8781796..7d4dd28d1131 100644
--- a/Common/ExtendedDictionary.cs
+++ b/Common/ExtendedDictionary.cs
@@ -251,7 +251,7 @@ public TValue setdefault(TKey key, TValue default_value)
if (IsReadOnly)
{
- throw new KeyNotFoundException(Messages.ExtendedDictionary.KeyNotFoundDueToNoData(this, key));
+ throw new KeyNotFoundException(Messages.ExtendedDictionary.SetDefaultKeyNotFoundDueToNoData(this, key));
}
this[key] = default_value;
diff --git a/Common/Messages/Messages.Exceptions.cs b/Common/Messages/Messages.Exceptions.cs
index 1cda211c8f05..7db891f142f9 100644
--- a/Common/Messages/Messages.Exceptions.cs
+++ b/Common/Messages/Messages.Exceptions.cs
@@ -72,21 +72,49 @@ public static string InterpretException(PythonException exception)
}
}
+ ///
+ /// Provides user-facing messages for the class and its consumers or related classes
+ ///
+ public static class AttributeErrorPythonExceptionInterpreter
+ {
+ ///
+ /// Returns a hint explaining that the accessed attribute belongs to TradeBar, not QuoteBar, and how to get it
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string QuoteBarHasNoTradeData(string attribute)
+ {
+ return $"QuoteBar holds quote data (bid/ask bars and sizes) and has no '{attribute}': trade data like volume comes with " +
+ "TradeBar. Use data.bars.get(symbol) for the TradeBar, and note that data[symbol] returns a QuoteBar when only " +
+ "quote data exists at that moment (common for forex, futures and crypto).";
+ }
+
+ ///
+ /// Returns a hint explaining that the accessed attribute belongs to QuoteBar, not TradeBar, and how to get it
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string TradeBarHasNoQuoteData(string attribute)
+ {
+ return $"TradeBar holds trade data (open/high/low/close/volume) and has no '{attribute}': bid/ask quotes come with " +
+ "QuoteBar. Use data.quote_bars.get(symbol) for the QuoteBar.";
+ }
+ }
+
///
/// Provides user-facing messages for the class and its consumers or related classes
///
public static class KeyErrorPythonExceptionInterpreter
{
///
- /// Returns a string message saying the given key does not exists in the collection and the exception that is thrown
- /// in this case. It also advises the user on how to prevent this exception
+ /// Returns a string message naming the key that was not found in the collection (when it could be extracted
+ /// from the KeyError) and advising the user on how to prevent this exception
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string KeyNotFoundInCollection(string key)
{
- return "Trying to retrieve an element from a collection using a key that does not exist " +
- $@"in that collection throws a KeyError exception. To prevent the exception, ensure that the {
- key} key exist in the collection and/or that collection is not empty.";
+ var keyDescription = string.IsNullOrWhiteSpace(key) ? "The requested key" : $"The key '{key}'";
+ return $"{keyDescription} was not found in the collection, which raises a KeyError exception. " +
+ "To prevent the exception, use collection.get(key), which returns None when the key is not found, " +
+ "or guard the access with 'if key in collection:'.";
}
}
diff --git a/Common/Messages/Messages.QuantConnect.cs b/Common/Messages/Messages.QuantConnect.cs
index e0380df49c88..181691d980fb 100644
--- a/Common/Messages/Messages.QuantConnect.cs
+++ b/Common/Messages/Messages.QuantConnect.cs
@@ -72,6 +72,20 @@ private static string AlgorithmPrefix()
return _algorithmLanguage == Language.Python ? "self" : "QCAlgorithm";
}
+ ///
+ /// Returns a language-aware, one-line suggestion of the safe access idioms for a keyed collection.
+ /// Appended to key-not-found messages so the failure always carries the guard idiom that prevents it,
+ /// instead of just naming the missing key
+ ///
+ private static string SafeKeyAccessSuggestion(string collection, string key = "symbol")
+ {
+ return _algorithmLanguage == Language.Python
+ ? $"To prevent the exception, use {collection}.get({key}), which returns None when the {key} is not found, " +
+ $"or guard the access with 'if {key} in {collection}:'."
+ : $"To prevent the exception, use {collection}.TryGetValue({key}, out var value) or check " +
+ $"{collection}.ContainsKey({key}) before accessing {collection}[{key}].";
+ }
+
///
/// Provides user-facing messages for the class and its consumers or related classes
///
@@ -243,15 +257,25 @@ public static string PopitemMethodNotSupported(ExtendedDictionary<
}
///
- /// Returns a string message saying that the given symbol wasn't found in the give instance object. It also shows
- /// a recommendation for solving this problem
+ /// Returns a string message saying that the given key wasn't found in the given instance object, likely because
+ /// there was no data at that moment in time. It also suggests the safe access idioms that prevent the exception.
+ /// This is the single template for the Slice/DataDictionary-family key-not-found errors
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string KeyNotFoundDueToNoData(ExtendedDictionary instance, TKey key)
{
- return $"'{key}' wasn't found in the {instance.GetType().Name} object, likely because there was no-data at this moment in " +
- "time and it wasn't possible to fillforward historical data. Please check the data exists before accessing it with " +
- $"data.ContainsKey(\"{key}\"). The collection is read-only, cannot set default.";
+ return $"'{key}' wasn't found in the {instance.GetType().GetBetterTypeName()} object, likely because there was no-data at this moment " +
+ $"in time and it wasn't possible to fillforward historical data. {SafeKeyAccessSuggestion("data")}";
+ }
+
+ ///
+ /// Returns the message plus a note explaining that
+ /// setdefault could not insert the default because the collection is read-only
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string SetDefaultKeyNotFoundDueToNoData(ExtendedDictionary instance, TKey key)
+ {
+ return $"{KeyNotFoundDueToNoData(instance, key)} The collection is read-only, cannot set default.";
}
///
diff --git a/Common/Messages/Messages.Securities.cs b/Common/Messages/Messages.Securities.cs
index 2e844357aad6..b67572700c39 100644
--- a/Common/Messages/Messages.Securities.cs
+++ b/Common/Messages/Messages.Securities.cs
@@ -840,14 +840,14 @@ public static string ToString(Securities.SecurityHolding instance)
public static class SecurityManager
{
///
- /// Returns a string message saying the given symbol was not found in the user security list
+ /// Returns a string message saying the given symbol was not found in the user security list.
+ /// It also suggests the safe access idioms that prevent the exception
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SymbolNotFoundInSecurities(QuantConnect.Symbol symbol)
{
- return Invariant($@"This asset symbol ({
- symbol}) was not found in your security list. Please add this security or check it exists before using it with 'Securities.ContainsKey(""{
- QuantConnect.SymbolCache.GetTicker(symbol)}"")'");
+ return Invariant($"This asset symbol ({symbol}) was not found in your security list. ") +
+ $"Please add this security before using it. {SafeKeyAccessSuggestion(FormatCodeRoot("Securities"))}";
}
///
diff --git a/Tests/Common/Data/SliceTests.cs b/Tests/Common/Data/SliceTests.cs
index e674b46af541..399b8a29e6e5 100644
--- a/Tests/Common/Data/SliceTests.cs
+++ b/Tests/Common/Data/SliceTests.cs
@@ -160,6 +160,36 @@ public void EquitiesIgnoreQuoteBars()
Assert.AreEqual(0, slice.Count);
}
+ [Test]
+ public void KeyNotFoundMessageNamesKeyAndSuggestsSafeAccess()
+ {
+ var tradeBar = new TradeBar { Symbol = Symbols.SPY, Time = DateTime.Now };
+ var slice = new Slice(DateTime.Now, new[] { tradeBar }, DateTime.Now);
+ try
+ {
+ Messages.SetAlgorithmLanguage(Language.CSharp);
+ var exception = Assert.Throws(() => { var data = slice[Symbols.AAPL]; });
+ StringAssert.Contains("'AAPL", exception.Message);
+ StringAssert.Contains("Slice", exception.Message);
+ StringAssert.Contains("data.TryGetValue(symbol, out var value)", exception.Message);
+ StringAssert.Contains("data.ContainsKey(symbol)", exception.Message);
+
+ Messages.SetAlgorithmLanguage(Language.Python);
+ exception = Assert.Throws(() => { var data = slice[Symbols.AAPL]; });
+ StringAssert.Contains("data.get(symbol)", exception.Message);
+ StringAssert.Contains("if symbol in data:", exception.Message);
+
+ // DataDictionary subtypes render their friendly generic type name through the same template
+ var dictionary = new DataDictionary();
+ exception = Assert.Throws(() => { var data = dictionary[Symbols.AAPL]; });
+ StringAssert.Contains("DataDictionary", exception.Message);
+ }
+ finally
+ {
+ Messages.SetAlgorithmLanguage(Language.CSharp);
+ }
+ }
+
[Test]
public void AccessesTradeBarCollection()
{
diff --git a/Tests/Common/Exceptions/AttributeErrorPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/AttributeErrorPythonExceptionInterpreterTests.cs
new file mode 100644
index 000000000000..412036d81b5f
--- /dev/null
+++ b/Tests/Common/Exceptions/AttributeErrorPythonExceptionInterpreterTests.cs
@@ -0,0 +1,106 @@
+/*
+ * 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 Python.Runtime;
+using QuantConnect.Exceptions;
+using System;
+using System.Collections.Generic;
+
+namespace QuantConnect.Tests.Common.Exceptions
+{
+ [TestFixture]
+ public class AttributeErrorPythonExceptionInterpreterTests
+ {
+ private PythonException _quoteBarVolumeException;
+ private PythonException _tradeBarAskPriceException;
+ private PythonException _genericAttributeErrorException;
+
+ [SetUp]
+ public void Setup()
+ {
+ using (Py.GIL())
+ {
+ var module = Py.Import("Test_PythonExceptionInterpreter");
+ dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();
+
+ _quoteBarVolumeException = CatchPythonException(() => algorithm.attribute_error_quote_bar_volume());
+ _tradeBarAskPriceException = CatchPythonException(() => algorithm.attribute_error_trade_bar_ask_price());
+ _genericAttributeErrorException = CatchPythonException(() => algorithm.attribute_error_generic());
+ }
+ }
+
+ [Test]
+ [TestCase(typeof(Exception), ExpectedResult = false)]
+ [TestCase(typeof(KeyNotFoundException), ExpectedResult = false)]
+ [TestCase(typeof(MissingMemberException), ExpectedResult = false)]
+ [TestCase(typeof(InvalidOperationException), ExpectedResult = false)]
+ [TestCase(typeof(PythonException), ExpectedResult = true)]
+ public bool CanInterpretReturnsTrueForOnlyWrongBarTypeAttributeErrors(Type exceptionType)
+ {
+ var exception = exceptionType == typeof(PythonException)
+ ? _quoteBarVolumeException
+ : (Exception)Activator.CreateInstance(exceptionType);
+ return new AttributeErrorPythonExceptionInterpreter().CanInterpret(exception);
+ }
+
+ [Test]
+ public void DoesNotInterpretAttributeErrorsWithoutAHint()
+ {
+ // a plain AttributeError ('QuoteBar' object has no attribute 'not_an_attribute') keeps
+ // the default interpretation
+ Assert.IsFalse(new AttributeErrorPythonExceptionInterpreter().CanInterpret(_genericAttributeErrorException));
+ }
+
+ [Test]
+ public void QuoteBarVolumeAccessExplainsTradeBarHoldsVolume()
+ {
+ var interpreter = new AttributeErrorPythonExceptionInterpreter();
+ Assert.IsTrue(interpreter.CanInterpret(_quoteBarVolumeException));
+
+ var interpreted = interpreter.Interpret(_quoteBarVolumeException, NullExceptionInterpreter.Instance);
+
+ StringAssert.Contains("'QuoteBar' object has no attribute 'volume'", interpreted.Message);
+ StringAssert.Contains("trade data like volume comes with", interpreted.Message);
+ StringAssert.Contains("data.bars.get(symbol)", interpreted.Message);
+ }
+
+ [Test]
+ public void TradeBarQuoteAttributeAccessExplainsQuoteBarHoldsQuotes()
+ {
+ var interpreter = new AttributeErrorPythonExceptionInterpreter();
+ Assert.IsTrue(interpreter.CanInterpret(_tradeBarAskPriceException));
+
+ var interpreted = interpreter.Interpret(_tradeBarAskPriceException, NullExceptionInterpreter.Instance);
+
+ StringAssert.Contains("'TradeBar' object has no attribute 'ask_price'", interpreted.Message);
+ StringAssert.Contains("bid/ask quotes come with", interpreted.Message);
+ StringAssert.Contains("data.quote_bars.get(symbol)", interpreted.Message);
+ }
+
+ private static PythonException CatchPythonException(Action action)
+ {
+ try
+ {
+ action();
+ }
+ catch (PythonException pythonException)
+ {
+ return pythonException;
+ }
+ throw new InvalidOperationException("Expected a PythonException to be thrown");
+ }
+ }
+}
diff --git a/Tests/Common/Exceptions/KeyErrorPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/KeyErrorPythonExceptionInterpreterTests.cs
index b86259999832..e36a8a0ab851 100644
--- a/Tests/Common/Exceptions/KeyErrorPythonExceptionInterpreterTests.cs
+++ b/Tests/Common/Exceptions/KeyErrorPythonExceptionInterpreterTests.cs
@@ -26,6 +26,7 @@ namespace QuantConnect.Tests.Common.Exceptions
public class KeyErrorPythonExceptionInterpreterTests
{
private PythonException _pythonException;
+ private PythonException _objectKeyPythonException;
[SetUp]
public void Setup()
@@ -44,6 +45,16 @@ public void Setup()
{
_pythonException = pythonException;
}
+
+ try
+ {
+ // dict()[Symbol.create('SPY', ...)]
+ algorithm.key_error_object_key();
+ }
+ catch (PythonException pythonException)
+ {
+ _objectKeyPythonException = pythonException;
+ }
}
}
@@ -73,6 +84,28 @@ public void InterpretThrowsForNonKeyErrorPythonExceptionTypes(Type exceptionType
Assert.That(() => interpreter.Interpret(exception, NullExceptionInterpreter.Instance), constraint);
}
+ [Test]
+ public void NamesStringKeyAndSuggestsSafeAccess()
+ {
+ var interpreted = new KeyErrorPythonExceptionInterpreter().Interpret(_pythonException, NullExceptionInterpreter.Instance);
+
+ StringAssert.Contains("The key 'SPY' was not found in the collection", interpreted.Message);
+ StringAssert.Contains("collection.get(key)", interpreted.Message);
+ StringAssert.Contains("if key in collection:", interpreted.Message);
+ }
+
+ [Test]
+ public void NamesObjectKeyReadFromExceptionArgs()
+ {
+ // the KeyError message is "", which used to render a blank key:
+ // "ensure that the key exist in the collection"
+ var interpreted = new KeyErrorPythonExceptionInterpreter().Interpret(_objectKeyPythonException, NullExceptionInterpreter.Instance);
+
+ StringAssert.Contains("The key 'SPY", interpreted.Message);
+ StringAssert.DoesNotContain("The key ''", interpreted.Message);
+ StringAssert.DoesNotContain("object at 0x", interpreted.Message);
+ }
+
[Test]
public void VerifyMessageContainsStackTraceInformation()
{
diff --git a/Tests/Common/Securities/SecurityManagerTests.cs b/Tests/Common/Securities/SecurityManagerTests.cs
index 6614eaf248c2..38331ce66bbd 100644
--- a/Tests/Common/Securities/SecurityManagerTests.cs
+++ b/Tests/Common/Securities/SecurityManagerTests.cs
@@ -14,6 +14,7 @@
*/
using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using NUnit.Framework;
@@ -39,6 +40,30 @@ public void Setup()
_subscriptionManager.SetDataManager(new DataManagerStub(timeKeeper));
}
+ [Test]
+ public void MissingSecurityMessageNamesSymbolAndSuggestsSafeAccess()
+ {
+ var timeKeeper = new TimeKeeper(new DateTime(2015, 12, 07));
+ var manager = new SecurityManager(timeKeeper);
+ try
+ {
+ Messages.SetAlgorithmLanguage(Language.CSharp);
+ var exception = Assert.Throws(() => { var security = manager[Symbols.AAPL]; });
+ StringAssert.Contains("AAPL", exception.Message);
+ StringAssert.Contains("Securities.TryGetValue(symbol, out var value)", exception.Message);
+ StringAssert.Contains("Securities.ContainsKey(symbol)", exception.Message);
+
+ Messages.SetAlgorithmLanguage(Language.Python);
+ exception = Assert.Throws(() => { var security = manager[Symbols.AAPL]; });
+ StringAssert.Contains("self.securities.get(symbol)", exception.Message);
+ StringAssert.Contains("if symbol in self.securities:", exception.Message);
+ }
+ finally
+ {
+ Messages.SetAlgorithmLanguage(Language.CSharp);
+ }
+ }
+
[Test]
public void NotifiesWhenSecurityAdded()
{
diff --git a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
index de3b05ca8faf..7812f40773ac 100644
--- a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
+++ b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
@@ -20,6 +20,20 @@ def initialize(self):
def key_error(self):
x = dict()['SPY']
+ def key_error_object_key(self):
+ # KeyError whose message is "": the key must be read from args
+ symbol = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
+ x = dict()[symbol]
+
+ def attribute_error_quote_bar_volume(self):
+ x = QuoteBar().volume
+
+ def attribute_error_trade_bar_ask_price(self):
+ x = TradeBar().ask_price
+
+ def attribute_error_generic(self):
+ x = QuoteBar().not_an_attribute
+
def no_method_match(self):
self.set_cash('SPY')