Skip to content
Open
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
2 changes: 1 addition & 1 deletion Common/Data/Market/DataDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion Common/Data/Slice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
116 changes: 116 additions & 0 deletions Common/Exceptions/AttributeErrorPythonExceptionInterpreter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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(
@"'(?<type>\w+)' object has no attribute '(?<attribute>\w+)'", RegexOptions.Compiled);

/// <summary>
/// Determines the order that an instance of this class should be called
/// </summary>
public override int Order => 0;

/// <summary>
/// Determines if this interpreter should be applied to the specified exception.
/// </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)
{
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 _);
}

/// <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 = (PythonException)exception;

TryGetHint(pe.Message, out var hint);
var message = $"{pe.Message.Trim()} {hint}";
message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);

return new MissingMemberException(message, pe);
}

/// <summary>
/// Gets the wrong-bar-type hint for the given AttributeError message, if there is one
/// </summary>
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;
}
}
}
46 changes: 42 additions & 4 deletions Common/Exceptions/KeyErrorPythonExceptionInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// "<QuantConnect.Symbol object at 0x...>", 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);

Expand Down
2 changes: 1 addition & 1 deletion Common/ExtendedDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 33 additions & 5 deletions Common/Messages/Messages.Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,49 @@ public static string InterpretException(PythonException exception)
}
}

/// <summary>
/// Provides user-facing messages for the <see cref="Exceptions.AttributeErrorPythonExceptionInterpreter"/> class and its consumers or related classes
/// </summary>
public static class AttributeErrorPythonExceptionInterpreter
{
/// <summary>
/// Returns a hint explaining that the accessed attribute belongs to TradeBar, not QuoteBar, and how to get it
/// </summary>
[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).";
}

/// <summary>
/// Returns a hint explaining that the accessed attribute belongs to QuoteBar, not TradeBar, and how to get it
/// </summary>
[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.";
}
}

/// <summary>
/// Provides user-facing messages for the <see cref="Exceptions.KeyErrorPythonExceptionInterpreter"/> class and its consumers or related classes
/// </summary>
public static class KeyErrorPythonExceptionInterpreter
{
/// <summary>
/// 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
/// </summary>
[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:'.";
}
}

Expand Down
34 changes: 29 additions & 5 deletions Common/Messages/Messages.QuantConnect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ private static string AlgorithmPrefix()
return _algorithmLanguage == Language.Python ? "self" : "QCAlgorithm";
}

/// <summary>
/// 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
/// </summary>
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}].";
}

/// <summary>
/// Provides user-facing messages for the <see cref="AlphaRuntimeStatistics"/> class and its consumers or related classes
/// </summary>
Expand Down Expand Up @@ -243,15 +257,25 @@ public static string PopitemMethodNotSupported<TKey, TValue>(ExtendedDictionary<
}

/// <summary>
/// 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
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string KeyNotFoundDueToNoData<TKey, TValue>(ExtendedDictionary<TKey, TValue> 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")}";
}

/// <summary>
/// Returns the <see cref="KeyNotFoundDueToNoData{TKey, TValue}"/> message plus a note explaining that
/// setdefault could not insert the default because the collection is read-only
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SetDefaultKeyNotFoundDueToNoData<TKey, TValue>(ExtendedDictionary<TKey, TValue> instance, TKey key)
{
return $"{KeyNotFoundDueToNoData(instance, key)} The collection is read-only, cannot set default.";
}

/// <summary>
Expand Down
8 changes: 4 additions & 4 deletions Common/Messages/Messages.Securities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -840,14 +840,14 @@ public static string ToString(Securities.SecurityHolding instance)
public static class SecurityManager
{
/// <summary>
/// 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
/// </summary>
[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"))}";
}

/// <summary>
Expand Down
30 changes: 30 additions & 0 deletions Tests/Common/Data/SliceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyNotFoundException>(() => { 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<KeyNotFoundException>(() => { 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<TradeBar>();
exception = Assert.Throws<KeyNotFoundException>(() => { var data = dictionary[Symbols.AAPL]; });
StringAssert.Contains("DataDictionary<TradeBar>", exception.Message);
}
finally
{
Messages.SetAlgorithmLanguage(Language.CSharp);
}
}

[Test]
public void AccessesTradeBarCollection()
{
Expand Down
Loading
Loading