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')