/*
* 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 Python.Runtime;
using System.Collections.Generic;
namespace QuantConnect.Python
{
///
/// Base class for Python wrapper classes
///
public class BasePythonWrapper
{
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 GetProperty(propertyName).GetAndDispose();
}
///
/// 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)
{
using var _ = Py.GIL();
var method = GetMethod(methodName);
return method.Invoke(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);
}
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
};
}
}
}