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 PythonRuntimeChecker.ConvertAndDispose<T>(GetProperty(propertyName), propertyName, isMethod: false);
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  var method = GetMethod(methodName);
154  return PythonRuntimeChecker.InvokeMethod<T>(method, methodName, args);
155  }
156 
157  /// <summary>
158  /// Invokes the specified method with the specified arguments
159  /// </summary>
160  /// <param name="methodName">The name of the method</param>
161  /// <param name="args">The arguments to call the method with</param>
162  public PyObject InvokeMethod(string methodName, params object[] args)
163  {
164  using var _ = Py.GIL();
165  var method = GetMethod(methodName);
166  return method.Invoke(args);
167  }
168 
169  /// <summary>
170  /// Invokes the specified method with the specified arguments without returning a value
171  /// </summary>
172  /// <param name="methodName">The name of the method</param>
173  /// <param name="args">The arguments to call the method with</param>
174  public void InvokeVoidMethod(string methodName, params object[] args)
175  {
176  InvokeMethod(methodName, args).Dispose();
177  }
178 
179  /// <summary>
180  /// Invokes the specified method with the specified arguments and iterates over the returned values
181  /// </summary>
182  /// <param name="methodName">The name of the method</param>
183  /// <param name="args">The arguments to call the method with</param>
184  /// <returns>The returned valued converted to the given type</returns>
185  public IEnumerable<T> InvokeMethodAndEnumerate<T>(string methodName, params object[] args)
186  {
187  var method = GetMethod(methodName);
188  return PythonRuntimeChecker.InvokeMethodAndEnumerate<T>(method, methodName, args);
189  }
190 
191  /// <summary>
192  /// Invokes the specified method with the specified arguments and iterates over the returned values
193  /// </summary>
194  /// <param name="methodName">The name of the method</param>
195  /// <param name="args">The arguments to call the method with</param>
196  /// <returns>The returned valued converted to the given type</returns>
197  public Dictionary<TKey, TValue> InvokeMethodAndGetDictionary<TKey, TValue>(string methodName, params object[] args)
198  {
199  var method = GetMethod(methodName);
200  return PythonRuntimeChecker.InvokeMethodAndGetDictionary<TKey, TValue>(method, methodName, args);
201  }
202 
203  /// <summary>
204  /// Invokes the specified method with the specified arguments and out parameters
205  /// </summary>
206  /// <param name="methodName">The name of the method</param>
207  /// <param name="outParametersTypes">The types of the out parameters</param>
208  /// <param name="outParameters">The out parameters values</param>
209  /// <param name="args">The arguments to call the method with</param>
210  /// <returns>The returned valued converted to the given type</returns>
211  public T InvokeMethodWithOutParameters<T>(string methodName, Type[] outParametersTypes, out object[] outParameters, params object[] args)
212  {
213  var method = GetMethod(methodName);
214  return PythonRuntimeChecker.InvokeMethodAndGetOutParameters<T>(method, methodName, outParametersTypes, out outParameters, args);
215  }
216 
217  /// <summary>
218  /// Invokes the specified method with the specified arguments and wraps the result
219  /// by calling the given function if the result is not a C# object
220  /// </summary>
221  /// <param name="methodName">The name of the method</param>
222  /// <param name="wrapResult">Method that wraps a Python object in the corresponding Python Wrapper</param>
223  /// <param name="args">The arguments to call the method with</param>
224  /// <returns>The returned value wrapped using the given method if the result is not a C# object</returns>
225  public T InvokeMethodAndWrapResult<T>(string methodName, Func<PyObject, T> wrapResult, params object[] args)
226  {
227  var method = GetMethod(methodName);
228  return PythonRuntimeChecker.InvokeMethodAndWrapResult(method, methodName, wrapResult, args);
229  }
230 
231  private string GetPropertyName(string propertyName, bool isEvent = false)
232  {
233  if (!_pythonPropertyNames.TryGetValue(propertyName, out var pythonPropertyName))
234  {
235  var snakeCasedPropertyName = propertyName.ToSnakeCase();
236 
237  // If the object is actually a C# object (e.g. a child class of a C# class),
238  // we check which property was defined in the Python class (if any), either the snake-cased or the original name.
239  if (!isEvent && _underlyingClrObject != null)
240  {
241  var underlyingClrObjectType = _underlyingClrObject.GetType();
242  var property = underlyingClrObjectType.GetProperty(propertyName);
243  if (property != null)
244  {
245  var clrPropertyValue = property.GetValue(_underlyingClrObject);
246  var pyObjectSnakeCasePropertyValue = _instance.GetAttr(snakeCasedPropertyName);
247 
248  if (!pyObjectSnakeCasePropertyValue.TryConvert(out object pyObjectSnakeCasePropertyClrValue, true) ||
249  !ReferenceEquals(clrPropertyValue, pyObjectSnakeCasePropertyClrValue))
250  {
251  pythonPropertyName = snakeCasedPropertyName;
252  }
253  else
254  {
255  pythonPropertyName = propertyName;
256  }
257  }
258  }
259 
260  if (pythonPropertyName == null)
261  {
262  pythonPropertyName = snakeCasedPropertyName;
263  if (!_instance.HasAttr(pythonPropertyName))
264  {
265  pythonPropertyName = propertyName;
266  }
267  }
268 
269  _pythonPropertyNames = AddToDictionary(_pythonPropertyNames, propertyName, pythonPropertyName);
270  }
271 
272  return pythonPropertyName;
273  }
274 
275  /// <summary>
276  /// Adds a key-value pair to the dictionary by copying the original one first and returning a new dictionary
277  /// containing the new key-value pair along with the original ones.
278  /// We do this in order to avoid the overhead of using locks or concurrent dictionaries and still be thread-safe.
279  /// </summary>
280  private static Dictionary<string, T> AddToDictionary<T>(Dictionary<string, T> dictionary, string key, T value)
281  {
282  return new Dictionary<string, T>(dictionary)
283  {
284  [key] = value
285  };
286  }
287 
288  /// <summary>
289  /// Determines whether the specified instance wraps the same Python object reference as this instance,
290  /// which would indicate that they are equal.
291  /// </summary>
292  /// <param name="other">The other object to compare this with</param>
293  /// <returns>True if both instances are equal, that is if both wrap the same Python object reference</returns>
294  public virtual bool Equals(BasePythonWrapper<TInterface> other)
295  {
296  return other is not null && (ReferenceEquals(this, other) || Equals(other._instance));
297  }
298 
299  /// <summary>
300  /// Determines whether the specified object is an instance of <see cref="BasePythonWrapper{TInterface}"/>
301  /// and wraps the same Python object reference as this instance, which would indicate that they are equal.
302  /// </summary>
303  /// <param name="obj">The other object to compare this with</param>
304  /// <returns>True if both instances are equal, that is if both wrap the same Python object reference</returns>
305  public override bool Equals(object obj)
306  {
307  return Equals(obj as PyObject) || Equals(obj as BasePythonWrapper<TInterface>);
308  }
309 
310  /// <summary>
311  /// Gets the hash code for the current instance
312  /// </summary>
313  /// <returns>The hash code of the current instance</returns>
314  public override int GetHashCode()
315  {
316  using var _ = Py.GIL();
317  return PythonReferenceComparer.Instance.GetHashCode(_instance);
318  }
319 
320  /// <summary>
321  /// Determines whether the specified <see cref="PyObject"/> is equal to the current instance's underlying Python object.
322  /// </summary>
323  private bool Equals(PyObject other)
324  {
325  if (other is null) return false;
326  if (ReferenceEquals(_instance, other)) return true;
327 
328  using var _ = Py.GIL();
329  // We only care about the Python object reference, not the underlying C# object reference for comparison
330  return PythonReferenceComparer.Instance.Equals(_instance, other);
331  }
332 
333  /// <summary>
334  /// Set of helper methods to invoke Python methods with runtime checks for return values and out parameter's conversions.
335  /// </summary>
336  public class PythonRuntimeChecker
337  {
338  /// <summary>
339  /// Invokes method <paramref name="method"/> and converts the returned value to type <typeparamref name="TResult"/>
340  /// </summary>
341  public static TResult InvokeMethod<TResult>(PyObject method, string pythonMethodName, params object[] args)
342  {
343  using var _ = Py.GIL();
344  using var result = method.Invoke(args);
345 
346  return Convert<TResult>(result, pythonMethodName);
347  }
348 
349  /// <summary>
350  /// Invokes method <paramref name="method"/>, expecting an enumerable or generator as return value,
351  /// converting each item to type <typeparamref name="TItem"/> on demand.
352  /// </summary>
353  public static IEnumerable<TItem> InvokeMethodAndEnumerate<TItem>(PyObject method, string pythonMethodName, params object[] args)
354  {
355  using var _ = Py.GIL();
356  var result = method.Invoke(args);
357 
358  foreach (var item in EnumerateAndDisposeItems<TItem>(result, pythonMethodName))
359  {
360  yield return item;
361  }
362 
363  result.Dispose();
364  }
365 
366  /// <summary>
367  /// Invokes method <paramref name="method"/>, expecting a dictionary as return value,
368  /// which then will be converted to a managed dictionary, with type checking on each item conversion.
369  /// </summary>
370  public static Dictionary<TKey, TValue> InvokeMethodAndGetDictionary<TKey, TValue>(PyObject method, string pythonMethodName, params object[] args)
371  {
372  using var _ = Py.GIL();
373  using var result = method.Invoke(args);
374 
375  Dictionary<TKey, TValue> dict;
376  if (result.TryConvert(out dict))
377  {
378  // this is required if the python implementation is actually returning a C# dict, not common,
379  // but could happen if its actually calling a base C# implementation
380  return dict;
381  }
382 
383  dict = new();
384  Func<PyObject, string> keyErrorMessageFunc =
385  (pyItem) => Messages.BasePythonWrapper.InvalidDictionaryKeyType(pythonMethodName, typeof(TKey), pyItem.GetPythonType());
386  foreach (var (managedKey, pyKey) in Enumerate<TKey>(result, pythonMethodName, keyErrorMessageFunc))
387  {
388  var pyValue = result.GetItem(pyKey);
389  try
390  {
391  dict[managedKey] = pyValue.GetAndDispose<TValue>();
392  }
393  catch (InvalidCastException ex)
394  {
395  throw new InvalidCastException(
396  Messages.BasePythonWrapper.InvalidDictionaryValueType(pythonMethodName, typeof(TValue), pyValue.GetPythonType()),
397  ex);
398  }
399  }
400 
401  return dict;
402  }
403 
404  /// <summary>
405  /// Invokes method <paramref name="method"/> and tries to convert the returned value to type <typeparamref name="TResult"/>.
406  /// If conversion is not possible, the returned PyObject is passed to the provided <paramref name="wrapResult"/> method,
407  /// which should try to do the proper conversion, wrapping or handling of the PyObject.
408  /// </summary>
409  public static TResult InvokeMethodAndWrapResult<TResult>(PyObject method, string pythonMethodName, Func<PyObject, TResult> wrapResult,
410  params object[] args)
411  {
412  using var _ = Py.GIL();
413  var result = method.Invoke(args);
414 
415  if (!result.TryConvert<TResult>(out var managedResult))
416  {
417  return wrapResult(result);
418  }
419 
420  result.Dispose();
421  return managedResult;
422  }
423 
424  /// <summary>
425  /// Invokes method <paramref name="method"/> and converts the returned value to type <typeparamref name="TResult"/>.
426  /// It also makes sure the Python method returns values for the out parameters, converting them into the expected types
427  /// in <paramref name="outParametersTypes"/> and placing them in the <paramref name="outParameters"/> array.
428  /// </summary>
429  public static TResult InvokeMethodAndGetOutParameters<TResult>(PyObject method, string pythonMethodName, Type[] outParametersTypes,
430  out object[] outParameters, params object[] args)
431  {
432  using var _ = Py.GIL();
433  using var result = method.Invoke(args);
434 
435  // Since pythonnet does not support out parameters, the methods return
436  // a tuple where the out parameter come after the other returned values
437  if (!PyTuple.IsTupleType(result))
438  {
439  throw new ArgumentException(
440  Messages.BasePythonWrapper.InvalidReturnTypeForMethodWithOutParameters(pythonMethodName, result.GetPythonType()));
441  }
442 
443  if (result.Length() < outParametersTypes.Length + 1)
444  {
445  throw new ArgumentException(Messages.BasePythonWrapper.InvalidReturnTypeTupleSizeForMethodWithOutParameters(
446  pythonMethodName, outParametersTypes.Length + 1, result.Length()));
447  }
448 
449  var managedResult = Convert<TResult>(result[0], pythonMethodName);
450 
451  outParameters = new object[outParametersTypes.Length];
452  var i = 0;
453  try
454  {
455  for (; i < outParametersTypes.Length; i++)
456  {
457  outParameters[i] = result[i + 1].AsManagedObject(outParametersTypes[i]);
458  }
459  }
460  catch (InvalidCastException exception)
461  {
462  throw new InvalidCastException(
463  Messages.BasePythonWrapper.InvalidOutParameterType(pythonMethodName, i, outParametersTypes[i], result[i + 1].GetPythonType()),
464  exception);
465  }
466 
467  return managedResult;
468  }
469 
470  /// <summary>
471  /// Converts the given PyObject into the provided <typeparamref name="T"/> type,
472  /// generating an exception with a user-friendly message if conversion is not possible.
473  /// </summary>
474  public static T Convert<T>(PyObject pyObject, string pythonName, bool isMethod = true)
475  {
476  var type = typeof(T);
477  try
478  {
479  if (type == typeof(void))
480  {
481  return default;
482  }
483 
484  if (type == typeof(PyObject))
485  {
486  return (T)(object)pyObject;
487  }
488 
489  return (T)pyObject.AsManagedObject(type);
490  }
491  catch (InvalidCastException e)
492  {
493  throw new InvalidCastException(Messages.BasePythonWrapper.InvalidReturnType(pythonName, type, pyObject.GetPythonType(), isMethod), e);
494  }
495  }
496 
497  /// <summary>
498  /// Converts the given PyObject into the provided <typeparamref name="T"/> type,
499  /// generating an exception with a user-friendly message if conversion is not possible.
500  /// It will dispose of the source PyObject.
501  /// </summary>
502  public static T ConvertAndDispose<T>(PyObject pyObject, string pythonName, bool isMethod = true)
503  {
504  try
505  {
506  return Convert<T>(pyObject, pythonName, isMethod);
507  }
508  finally
509  {
510  pyObject.Dispose();
511  }
512  }
513 
514  /// <summary>
515  /// Verifies that the <paramref name="result"/> value is iterable and converts each item into the <typeparamref name="TItem"/> type,
516  /// returning also the corresponding source PyObject for each one of them.
517  /// </summary>
518  private static IEnumerable<(TItem, PyObject)> Enumerate<TItem>(PyObject result, string pythonMethodName,
519  Func<PyObject, string> getInvalidCastExceptionMessage = null)
520  {
521  if (!result.IsIterable())
522  {
523  throw new InvalidCastException(Messages.BasePythonWrapper.InvalidIterable(pythonMethodName, typeof(TItem), result.GetPythonType()));
524  }
525 
526  using var iterator = result.GetIterator();
527  foreach (PyObject item in iterator)
528  {
529  TItem managedItem;
530 
531  try
532  {
533  managedItem = item.As<TItem>();
534  }
535  catch (InvalidCastException ex)
536  {
537  var message = getInvalidCastExceptionMessage?.Invoke(item) ??
538  Messages.BasePythonWrapper.InvalidMethodIterableItemType(pythonMethodName, typeof(TItem), item.GetPythonType());
539  throw new InvalidCastException(message, ex);
540  }
541 
542  yield return (managedItem, item);
543  }
544  }
545 
546  /// <summary>
547  /// Verifies that the <paramref name="result"/> value is iterable and converts each item into the <typeparamref name="TItem"/> type.
548  /// </summary>
549  private static IEnumerable<TItem> EnumerateAndDisposeItems<TItem>(PyObject result, string pythonMethodName)
550  {
551  foreach (var (managedItem, pyItem) in Enumerate<TItem>(result, pythonMethodName))
552  {
553  pyItem.Dispose();
554  yield return managedItem;
555  }
556  }
557  }
558  }
559 }