Lean  $LEAN_TAG$
BasePythonWrapper.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using Python.Runtime;
17 using System;
18 using System.Collections.Generic;
19 
21 {
22  /// <summary>
23  /// Base class for Python wrapper classes
24  /// </summary>
25  public class BasePythonWrapper<TInterface> : IEquatable<BasePythonWrapper<TInterface>>
26  {
27  private PyObject _instance;
28  private object _underlyingClrObject;
29  private Dictionary<string, PyObject> _pythonMethods;
30  private Dictionary<string, string> _pythonPropertyNames;
31 
32  private readonly bool _validateInterface;
33 
34  /// <summary>
35  /// Gets the underlying python instance
36  /// </summary>
37  protected PyObject Instance => _instance;
38 
39  /// <summary>
40  /// Creates a new instance of the <see cref="BasePythonWrapper{TInterface}" /> class
41  /// </summary>
42  /// <param name="validateInterface">Whether to perform validations for interface implementation</param>
43  public BasePythonWrapper(bool validateInterface = true)
44  {
45  _pythonMethods = new();
46  _pythonPropertyNames = new();
47  _validateInterface = validateInterface;
48  }
49 
50  /// <summary>
51  /// Creates a new instance of the <see cref="BasePythonWrapper{TInterface}"/> class with the specified instance
52  /// </summary>
53  /// <param name="instance">The underlying python instance</param>
54  /// <param name="validateInterface">Whether to perform validations for interface implementation</param>
55  public BasePythonWrapper(PyObject instance, bool validateInterface = true)
56  : this(validateInterface)
57  {
58  SetPythonInstance(instance);
59  }
60 
61  /// <summary>
62  /// Sets the python instance
63  /// </summary>
64  /// <param name="instance">The underlying python instance</param>
65  public void SetPythonInstance(PyObject instance)
66  {
67  if (_instance != null)
68  {
69  _pythonMethods.Clear();
70  _pythonPropertyNames.Clear();
71  }
72 
73  _instance = _validateInterface ? instance.ValidateImplementationOf<TInterface>() : instance;
74  _instance.TryConvert(out _underlyingClrObject);
75  }
76 
77  /// <summary>
78  /// Gets the Python instance property with the specified name
79  /// </summary>
80  /// <param name="propertyName">The name of the property</param>
81  public T GetProperty<T>(string propertyName)
82  {
83  using var _ = Py.GIL();
84  return GetProperty(propertyName).GetAndDispose<T>();
85  }
86 
87  /// <summary>
88  /// Gets the Python instance property with the specified name
89  /// </summary>
90  /// <param name="propertyName">The name of the property</param>
91  public PyObject GetProperty(string propertyName)
92  {
93  using var _ = Py.GIL();
94  return _instance.GetAttr(GetPropertyName(propertyName));
95  }
96 
97  /// <summary>
98  /// Sets the Python instance property with the specified name
99  /// </summary>
100  /// <param name="propertyName">The name of the property</param>
101  /// <param name="value">The property value</param>
102  public void SetProperty(string propertyName, object value)
103  {
104  using var _ = Py.GIL();
105  _instance.SetAttr(GetPropertyName(propertyName), value.ToPython());
106  }
107 
108  /// <summary>
109  /// Gets the Python instance event with the specified name
110  /// </summary>
111  /// <param name="name">The name of the event</param>
112  public dynamic GetEvent(string name)
113  {
114  using var _ = Py.GIL();
115  return _instance.GetAttr(GetPropertyName(name, true));
116  }
117 
118  /// <summary>
119  /// Determines whether the Python instance has the specified attribute
120  /// </summary>
121  /// <param name="name">The attribute name</param>
122  /// <returns>Whether the Python instance has the specified attribute</returns>
123  public bool HasAttr(string name)
124  {
125  using var _ = Py.GIL();
126  return _instance.HasAttr(name) || _instance.HasAttr(name.ToSnakeCase());
127  }
128 
129  /// <summary>
130  /// Gets the Python instances method with the specified name and caches it
131  /// </summary>
132  /// <param name="methodName">The name of the method</param>
133  /// <returns>The matched method</returns>
134  public PyObject GetMethod(string methodName)
135  {
136  if (!_pythonMethods.TryGetValue(methodName, out var method))
137  {
138  method = _instance.GetMethod(methodName);
139  _pythonMethods = AddToDictionary(_pythonMethods, methodName, method);
140  }
141 
142  return method;
143  }
144 
145  /// <summary>
146  /// Invokes the specified method with the specified arguments
147  /// </summary>
148  /// <param name="methodName">The name of the method</param>
149  /// <param name="args">The arguments to call the method with</param>
150  /// <returns>The returned valued converted to the given type</returns>
151  public T InvokeMethod<T>(string methodName, params object[] args)
152  {
153  using var _ = Py.GIL();
154  var method = GetMethod(methodName);
155  return method.Invoke<T>(args);
156  }
157 
158  /// <summary>
159  /// Invokes the specified method with the specified arguments
160  /// </summary>
161  /// <param name="methodName">The name of the method</param>
162  /// <param name="args">The arguments to call the method with</param>
163  public PyObject InvokeMethod(string methodName, params object[] args)
164  {
165  using var _ = Py.GIL();
166  var method = GetMethod(methodName);
167  return method.Invoke(args);
168  }
169 
170  private string GetPropertyName(string propertyName, bool isEvent = false)
171  {
172  if (!_pythonPropertyNames.TryGetValue(propertyName, out var pythonPropertyName))
173  {
174  var snakeCasedPropertyName = propertyName.ToSnakeCase();
175 
176  // If the object is actually a C# object (e.g. a child class of a C# class),
177  // we check which property was defined in the Python class (if any), either the snake-cased or the original name.
178  if (!isEvent && _underlyingClrObject != null)
179  {
180  var underlyingClrObjectType = _underlyingClrObject.GetType();
181  var property = underlyingClrObjectType.GetProperty(propertyName);
182  if (property != null)
183  {
184  var clrPropertyValue = property.GetValue(_underlyingClrObject);
185  var pyObjectSnakeCasePropertyValue = _instance.GetAttr(snakeCasedPropertyName);
186 
187  if (!pyObjectSnakeCasePropertyValue.TryConvert(out object pyObjectSnakeCasePropertyClrValue, true) ||
188  !ReferenceEquals(clrPropertyValue, pyObjectSnakeCasePropertyClrValue))
189  {
190  pythonPropertyName = snakeCasedPropertyName;
191  }
192  else
193  {
194  pythonPropertyName = propertyName;
195  }
196  }
197  }
198 
199  if (pythonPropertyName == null)
200  {
201  pythonPropertyName = snakeCasedPropertyName;
202  if (!_instance.HasAttr(pythonPropertyName))
203  {
204  pythonPropertyName = propertyName;
205  }
206  }
207 
208  _pythonPropertyNames = AddToDictionary(_pythonPropertyNames, propertyName, pythonPropertyName);
209  }
210 
211  return pythonPropertyName;
212  }
213 
214  /// <summary>
215  /// Adds a key-value pair to the dictionary by copying the original one first and returning a new dictionary
216  /// containing the new key-value pair along with the original ones.
217  /// We do this in order to avoid the overhead of using locks or concurrent dictionaries and still be thread-safe.
218  /// </summary>
219  private static Dictionary<string, T> AddToDictionary<T>(Dictionary<string, T> dictionary, string key, T value)
220  {
221  return new Dictionary<string, T>(dictionary)
222  {
223  [key] = value
224  };
225  }
226 
227  /// <summary>
228  /// Determines whether the specified instance wraps the same Python object reference as this instance,
229  /// which would indicate that they are equal.
230  /// </summary>
231  /// <param name="other">The other object to compare this with</param>
232  /// <returns>True if both instances are equal, that is if both wrap the same Python object reference</returns>
233  public virtual bool Equals(BasePythonWrapper<TInterface> other)
234  {
235  return other is not null && (ReferenceEquals(this, other) || Equals(other._instance));
236  }
237 
238  /// <summary>
239  /// Determines whether the specified object is an instance of <see cref="BasePythonWrapper{TInterface}"/>
240  /// and wraps the same Python object reference as this instance, which would indicate that they are equal.
241  /// </summary>
242  /// <param name="obj">The other object to compare this with</param>
243  /// <returns>True if both instances are equal, that is if both wrap the same Python object reference</returns>
244  public override bool Equals(object obj)
245  {
246  return Equals(obj as PyObject) || Equals(obj as BasePythonWrapper<TInterface>);
247  }
248 
249  /// <summary>
250  /// Gets the hash code for the current instance
251  /// </summary>
252  /// <returns>The hash code of the current instance</returns>
253  public override int GetHashCode()
254  {
255  using var _ = Py.GIL();
256  return PythonReferenceComparer.Instance.GetHashCode(_instance);
257  }
258 
259  /// <summary>
260  /// Determines whether the specified <see cref="PyObject"/> is equal to the current instance's underlying Python object.
261  /// </summary>
262  private bool Equals(PyObject other)
263  {
264  if (other is null) return false;
265  if (ReferenceEquals(_instance, other)) return true;
266 
267  using var _ = Py.GIL();
268  // We only care about the Python object reference, not the underlying C# object reference for comparison
269  return PythonReferenceComparer.Instance.Equals(_instance, other);
270  }
271  }
272 }