/* * 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; } } } } }