/*
* 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;
using System.Collections.Generic;
namespace QuantConnect.Python
{
///
/// Base class for Python wrapper classes
///
public class BasePythonWrapper : IEquatable>, IDisposable
{
private PyObject _instance;
private object _underlyingClrObject;
private Dictionary _pythonMethods;
private Dictionary _pythonPropertyNames;
private readonly bool _validateInterface;
///
/// Gets the underlying python instance
///
protected PyObject Instance => _instance;
///
/// Creates a new instance of the class
///
/// Whether to perform validations for interface implementation
public BasePythonWrapper(bool validateInterface = true)
{
_pythonMethods = new();
_pythonPropertyNames = new();
_validateInterface = validateInterface;
}
///
/// Creates a new instance of the class with the specified instance
///
/// The underlying python instance
/// Whether to perform validations for interface implementation
public BasePythonWrapper(PyObject instance, bool validateInterface = true)
: this(validateInterface)
{
SetPythonInstance(instance);
}
///
/// Sets the python instance
///
/// The underlying python instance
public void SetPythonInstance(PyObject instance)
{
if (_instance != null)
{
_pythonMethods.Clear();
_pythonPropertyNames.Clear();
}
_instance = _validateInterface ? instance.ValidateImplementationOf() : instance;
_instance.TryConvert(out _underlyingClrObject);
}
///
/// Gets the Python instance property with the specified name
///
/// The name of the property
public T GetProperty(string propertyName)
{
using var _ = Py.GIL();
return PythonRuntimeChecker.ConvertAndDispose(GetProperty(propertyName), propertyName, isMethod: false);
}
///
/// Gets the Python instance property with the specified name
///
/// The name of the property
public PyObject GetProperty(string propertyName)
{
using var _ = Py.GIL();
return _instance.GetAttr(GetPropertyName(propertyName));
}
///
/// Sets the Python instance property with the specified name
///
/// The name of the property
/// The property value
public void SetProperty(string propertyName, object value)
{
using var _ = Py.GIL();
_instance.SetAttr(GetPropertyName(propertyName), value.ToPython());
}
///
/// Gets the Python instance event with the specified name
///
/// The name of the event
public dynamic GetEvent(string name)
{
using var _ = Py.GIL();
return _instance.GetAttr(GetPropertyName(name, true));
}
///
/// Determines whether the Python instance has the specified attribute
///
/// The attribute name
/// Whether the Python instance has the specified attribute
public bool HasAttr(string name)
{
using var _ = Py.GIL();
return _instance.HasAttr(name) || _instance.HasAttr(name.ToSnakeCase());
}
///
/// Gets the Python instances method with the specified name and caches it
///
/// The name of the method
/// The matched method
public PyObject GetMethod(string methodName)
{
if (!_pythonMethods.TryGetValue(methodName, out var method))
{
method = _instance.GetMethod(methodName);
_pythonMethods = AddToDictionary(_pythonMethods, methodName, method);
}
return method;
}
///
/// Invokes the specified method with the specified arguments
///
/// The name of the method
/// The arguments to call the method with
/// The returned valued converted to the given type
public T InvokeMethod(string methodName, params object[] args)
{
var method = GetMethod(methodName);
return PythonRuntimeChecker.InvokeMethod(method, methodName, args);
}
///
/// Invokes the specified method with the specified arguments
///
/// The name of the method
/// The arguments to call the method with
public PyObject InvokeMethod(string methodName, params object[] args)
{
using var _ = Py.GIL();
var method = GetMethod(methodName);
return method.Invoke(args);
}
///
/// Invokes the specified method with the specified arguments without returning a value
///
/// The name of the method
/// The arguments to call the method with
public void InvokeVoidMethod(string methodName, params object[] args)
{
InvokeMethod(methodName, args).Dispose();
}
///
/// Invokes the specified method with the specified arguments and iterates over the returned values
///
/// The name of the method
/// The arguments to call the method with
/// The returned valued converted to the given type
public IEnumerable InvokeMethodAndEnumerate(string methodName, params object[] args)
{
var method = GetMethod(methodName);
return PythonRuntimeChecker.InvokeMethodAndEnumerate(method, methodName, args);
}
///
/// Invokes the specified method with the specified arguments and iterates over the returned values
///
/// The name of the method
/// The arguments to call the method with
/// The returned valued converted to the given type
public Dictionary InvokeMethodAndGetDictionary(string methodName, params object[] args)
{
var method = GetMethod(methodName);
return PythonRuntimeChecker.InvokeMethodAndGetDictionary(method, methodName, args);
}
///
/// Invokes the specified method with the specified arguments and out parameters
///
/// The name of the method
/// The types of the out parameters
/// The out parameters values
/// The arguments to call the method with
/// The returned valued converted to the given type
public T InvokeMethodWithOutParameters(string methodName, Type[] outParametersTypes, out object[] outParameters, params object[] args)
{
var method = GetMethod(methodName);
return PythonRuntimeChecker.InvokeMethodAndGetOutParameters(method, methodName, outParametersTypes, out outParameters, args);
}
///
/// Invokes the specified method with the specified arguments and wraps the result
/// by calling the given function if the result is not a C# object
///
/// The name of the method
/// Method that wraps a Python object in the corresponding Python Wrapper
/// The arguments to call the method with
/// The returned value wrapped using the given method if the result is not a C# object
public T InvokeMethodAndWrapResult(string methodName, Func wrapResult, params object[] args)
{
var method = GetMethod(methodName);
return PythonRuntimeChecker.InvokeMethodAndWrapResult(method, methodName, wrapResult, args);
}
private string GetPropertyName(string propertyName, bool isEvent = false)
{
if (!_pythonPropertyNames.TryGetValue(propertyName, out var pythonPropertyName))
{
var snakeCasedPropertyName = propertyName.ToSnakeCase();
// If the object is actually a C# object (e.g. a child class of a C# class),
// we check which property was defined in the Python class (if any), either the snake-cased or the original name.
if (!isEvent && _underlyingClrObject != null)
{
var underlyingClrObjectType = _underlyingClrObject.GetType();
var property = underlyingClrObjectType.GetProperty(propertyName);
if (property != null)
{
var clrPropertyValue = property.GetValue(_underlyingClrObject);
var pyObjectSnakeCasePropertyValue = _instance.GetAttr(snakeCasedPropertyName);
if (!pyObjectSnakeCasePropertyValue.TryConvert(out object pyObjectSnakeCasePropertyClrValue, true) ||
!ReferenceEquals(clrPropertyValue, pyObjectSnakeCasePropertyClrValue))
{
pythonPropertyName = snakeCasedPropertyName;
}
else
{
pythonPropertyName = propertyName;
}
}
}
if (pythonPropertyName == null)
{
pythonPropertyName = snakeCasedPropertyName;
if (!_instance.HasAttr(pythonPropertyName))
{
pythonPropertyName = propertyName;
}
}
_pythonPropertyNames = AddToDictionary(_pythonPropertyNames, propertyName, pythonPropertyName);
}
return pythonPropertyName;
}
///
/// Adds a key-value pair to the dictionary by copying the original one first and returning a new dictionary
/// containing the new key-value pair along with the original ones.
/// We do this in order to avoid the overhead of using locks or concurrent dictionaries and still be thread-safe.
///
private static Dictionary AddToDictionary(Dictionary dictionary, string key, T value)
{
return new Dictionary(dictionary)
{
[key] = value
};
}
///
/// Determines whether the specified instance wraps the same Python object reference as this instance,
/// which would indicate that they are equal.
///
/// The other object to compare this with
/// True if both instances are equal, that is if both wrap the same Python object reference
public virtual bool Equals(BasePythonWrapper other)
{
return other is not null && (ReferenceEquals(this, other) || Equals(other._instance));
}
///
/// Determines whether the specified object is an instance of
/// and wraps the same Python object reference as this instance, which would indicate that they are equal.
///
/// The other object to compare this with
/// True if both instances are equal, that is if both wrap the same Python object reference
public override bool Equals(object obj)
{
return Equals(obj as PyObject) || Equals(obj as BasePythonWrapper);
}
///
/// Gets the hash code for the current instance
///
/// The hash code of the current instance
public override int GetHashCode()
{
using var _ = Py.GIL();
return PythonReferenceComparer.Instance.GetHashCode(_instance);
}
///
/// Determines whether the specified is equal to the current instance's underlying Python object.
///
private bool Equals(PyObject other)
{
if (other is null) return false;
if (ReferenceEquals(_instance, other)) return true;
using var _ = Py.GIL();
// We only care about the Python object reference, not the underlying C# object reference for comparison
return PythonReferenceComparer.Instance.Equals(_instance, other);
}
///
/// Dispose of this instance
///
public virtual void Dispose()
{
using var _ = Py.GIL();
if (_pythonMethods != null)
{
foreach (var methods in _pythonMethods.Values)
{
methods.Dispose();
}
_pythonMethods.Clear();
}
_instance?.Dispose();
}
///
/// Set of helper methods to invoke Python methods with runtime checks for return values and out parameter's conversions.
///
public class PythonRuntimeChecker
{
///
/// Invokes method and converts the returned value to type
///
public static TResult InvokeMethod(PyObject method, string pythonMethodName, params object[] args)
{
using var _ = Py.GIL();
using var result = method.Invoke(args);
return Convert(result, pythonMethodName);
}
///
/// Invokes method , expecting an enumerable or generator as return value,
/// converting each item to type on demand.
///
public static IEnumerable InvokeMethodAndEnumerate(PyObject method, string pythonMethodName, params object[] args)
{
using var _ = Py.GIL();
var result = method.Invoke(args);
foreach (var item in EnumerateAndDisposeItems(result, pythonMethodName))
{
yield return item;
}
result.Dispose();
}
///
/// Invokes method , expecting a dictionary as return value,
/// which then will be converted to a managed dictionary, with type checking on each item conversion.
///
public static Dictionary InvokeMethodAndGetDictionary(PyObject method, string pythonMethodName, params object[] args)
{
using var _ = Py.GIL();
using var result = method.Invoke(args);
Dictionary dict;
if (result.TryConvert(out dict))
{
// this is required if the python implementation is actually returning a C# dict, not common,
// but could happen if its actually calling a base C# implementation
return dict;
}
dict = new();
Func keyErrorMessageFunc =
(pyItem) => Messages.BasePythonWrapper.InvalidDictionaryKeyType(pythonMethodName, typeof(TKey), pyItem.GetPythonType());
foreach (var (managedKey, pyKey) in Enumerate(result, pythonMethodName, keyErrorMessageFunc))
{
var pyValue = result.GetItem(pyKey);
try
{
dict[managedKey] = pyValue.GetAndDispose();
}
catch (InvalidCastException ex)
{
throw new InvalidCastException(
Messages.BasePythonWrapper.InvalidDictionaryValueType(pythonMethodName, typeof(TValue), pyValue.GetPythonType()),
ex);
}
}
return dict;
}
///
/// Invokes method and tries to convert the returned value to type .
/// If conversion is not possible, the returned PyObject is passed to the provided method,
/// which should try to do the proper conversion, wrapping or handling of the PyObject.
///
public static TResult InvokeMethodAndWrapResult(PyObject method, string pythonMethodName, Func wrapResult,
params object[] args)
{
using var _ = Py.GIL();
var result = method.Invoke(args);
if (!result.TryConvert(out var managedResult))
{
return wrapResult(result);
}
result.Dispose();
return managedResult;
}
///
/// Invokes method and converts the returned value to type .
/// It also makes sure the Python method returns values for the out parameters, converting them into the expected types
/// in and placing them in the array.
///
public static TResult InvokeMethodAndGetOutParameters(PyObject method, string pythonMethodName, Type[] outParametersTypes,
out object[] outParameters, params object[] args)
{
using var _ = Py.GIL();
using var result = method.Invoke(args);
// Since pythonnet does not support out parameters, the methods return
// a tuple where the out parameter come after the other returned values
if (!PyTuple.IsTupleType(result))
{
throw new ArgumentException(
Messages.BasePythonWrapper.InvalidReturnTypeForMethodWithOutParameters(pythonMethodName, result.GetPythonType()));
}
if (result.Length() < outParametersTypes.Length + 1)
{
throw new ArgumentException(Messages.BasePythonWrapper.InvalidReturnTypeTupleSizeForMethodWithOutParameters(
pythonMethodName, outParametersTypes.Length + 1, result.Length()));
}
var managedResult = Convert(result[0], pythonMethodName);
outParameters = new object[outParametersTypes.Length];
var i = 0;
try
{
for (; i < outParametersTypes.Length; i++)
{
outParameters[i] = result[i + 1].AsManagedObject(outParametersTypes[i]);
}
}
catch (InvalidCastException exception)
{
throw new InvalidCastException(
Messages.BasePythonWrapper.InvalidOutParameterType(pythonMethodName, i, outParametersTypes[i], result[i + 1].GetPythonType()),
exception);
}
return managedResult;
}
///
/// Converts the given PyObject into the provided type,
/// generating an exception with a user-friendly message if conversion is not possible.
///
public static T Convert(PyObject pyObject, string pythonName, bool isMethod = true)
{
var type = typeof(T);
try
{
if (type == typeof(void))
{
return default;
}
if (type == typeof(PyObject))
{
return (T)(object)pyObject;
}
return (T)pyObject.AsManagedObject(type);
}
catch (InvalidCastException e)
{
throw new InvalidCastException(Messages.BasePythonWrapper.InvalidReturnType(pythonName, type, pyObject.GetPythonType(), isMethod), e);
}
}
///
/// Converts the given PyObject into the provided type,
/// generating an exception with a user-friendly message if conversion is not possible.
/// It will dispose of the source PyObject.
///
public static T ConvertAndDispose(PyObject pyObject, string pythonName, bool isMethod = true)
{
try
{
return Convert(pyObject, pythonName, isMethod);
}
finally
{
pyObject.Dispose();
}
}
///
/// Verifies that the value is iterable and converts each item into the type,
/// returning also the corresponding source PyObject for each one of them.
///
private static IEnumerable<(TItem, PyObject)> Enumerate(PyObject result, string pythonMethodName,
Func getInvalidCastExceptionMessage = null)
{
if (!result.IsIterable())
{
throw new InvalidCastException(Messages.BasePythonWrapper.InvalidIterable(pythonMethodName, typeof(TItem), result.GetPythonType()));
}
using var iterator = result.GetIterator();
foreach (PyObject item in iterator)
{
TItem managedItem;
try
{
managedItem = item.As();
}
catch (InvalidCastException ex)
{
var message = getInvalidCastExceptionMessage?.Invoke(item) ??
Messages.BasePythonWrapper.InvalidMethodIterableItemType(pythonMethodName, typeof(TItem), item.GetPythonType());
throw new InvalidCastException(message, ex);
}
yield return (managedItem, item);
}
}
///
/// Verifies that the value is iterable and converts each item into the type.
///
private static IEnumerable EnumerateAndDisposeItems(PyObject result, string pythonMethodName)
{
foreach (var (managedItem, pyItem) in Enumerate(result, pythonMethodName))
{
pyItem.Dispose();
yield return managedItem;
}
}
}
}
}