Lean  $LEAN_TAG$
PythonInitializer.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 
17 using System;
18 using System.IO;
19 using System.Linq;
20 using Python.Runtime;
21 using QuantConnect.Util;
22 using QuantConnect.Logging;
23 using System.Collections.Generic;
25 
26 namespace QuantConnect.Python
27 {
28  /// <summary>
29  /// Helper class for Python initialization
30  /// </summary>
31  public static class PythonInitializer
32  {
33  private static bool IncludeSystemPackages;
34  private static string PathToVirtualEnv;
35 
36  // Used to allow multiple Python unit and regression tests to be run in the same test run
37  private static bool _isInitialized;
38 
39  // Used to hold pending path additions before Initialize is called
40  private static List<string> _pendingPathAdditions = new List<string>();
41 
42  private static string _algorithmLocation;
43 
44  /// <summary>
45  /// Initialize python.
46  ///
47  /// In some cases, we might not need to call BeginAllowThreads, like when we're running
48  /// in a python or non-threaded environment.
49  /// In those cases, we can set the beginAllowThreads parameter to false.
50  /// </summary>
51  public static void Initialize(bool beginAllowThreads = true)
52  {
53  if (!_isInitialized)
54  {
55  Log.Trace($"PythonInitializer.Initialize(): {Messages.PythonInitializer.Start}...");
56  PythonEngine.Initialize();
57 
58  if (beginAllowThreads)
59  {
60  // required for multi-threading usage
61  PythonEngine.BeginAllowThreads();
62  }
63 
64  _isInitialized = true;
65 
66  ConfigurePythonPaths();
67 
68  TryInitPythonVirtualEnvironment();
69  Log.Trace($"PythonInitializer.Initialize(): {Messages.PythonInitializer.Ended}");
70  }
71  }
72 
73  /// <summary>
74  /// Shutdown python
75  /// </summary>
76  public static void Shutdown()
77  {
78  if (_isInitialized)
79  {
80  Log.Trace($"PythonInitializer.Shutdown(): {Messages.PythonInitializer.Start}");
81  _isInitialized = false;
82 
83  try
84  {
85  var pyLock = Py.GIL();
86  PythonEngine.Shutdown();
87  }
88  catch (Exception ex)
89  {
90  Log.Error(ex);
91  }
92 
93  Log.Trace($"PythonInitializer.Shutdown(): {Messages.PythonInitializer.Ended}");
94  }
95  }
96 
97  /// <summary>
98  /// Adds directories to the python path at runtime
99  /// </summary>
100  public static bool AddPythonPaths(IEnumerable<string> paths)
101  {
102  // Filter out any paths that are already on our Python path
103  if (paths.IsNullOrEmpty())
104  {
105  return false;
106  }
107 
108  // Add these paths to our pending additions
109  _pendingPathAdditions.AddRange(paths.Where(x => !_pendingPathAdditions.Contains(x)));
110 
111  if (_isInitialized)
112  {
113  using (Py.GIL())
114  {
115  using dynamic sys = Py.Import("sys");
116  using var locals = new PyDict();
117  locals.SetItem("sys", sys);
118 
119  // Filter out any already paths that already exist on our current PythonPath
120  using var pythonCurrentPath = PythonEngine.Eval("sys.path", locals: locals);
121  var currentPath = pythonCurrentPath.As<List<string>>();
122  _pendingPathAdditions = _pendingPathAdditions.Where(x => !currentPath.Contains(x.Replace('\\', '/'))).ToList();
123 
124  // Algorithm location most always be before any other path added through this method
125  var insertionIndex = 0;
126  if (!_algorithmLocation.IsNullOrEmpty())
127  {
128  insertionIndex = currentPath.IndexOf(_algorithmLocation.Replace('\\', '/')) + 1;
129 
130  if (insertionIndex == 0)
131  {
132  // The algorithm location is not in the current path so it must be in the pending additions list.
133  // Let's move it to the back so it ends up added at the beginning of the path list
134  _pendingPathAdditions.Remove(_algorithmLocation);
135  _pendingPathAdditions.Add(_algorithmLocation);
136  }
137  }
138 
139  // Insert any pending path additions
140  if (!_pendingPathAdditions.IsNullOrEmpty())
141  {
142  var code = string.Join(";", _pendingPathAdditions
143  .Select(s => $"sys.path.insert({insertionIndex}, '{s}')")).Replace('\\', '/');
144  PythonEngine.Exec(code, locals: locals);
145 
146  _pendingPathAdditions.Clear();
147  }
148  }
149  }
150 
151  return true;
152  }
153 
154  /// <summary>
155  /// Adds the algorithm location to the python path.
156  /// This will make sure that <see cref="AddPythonPaths" /> keeps the algorithm location path
157  /// at the beginning of the pythonpath.
158  /// </summary>
159  public static void AddAlgorithmLocationPath(string algorithmLocation)
160  {
161  if (!_algorithmLocation.IsNullOrEmpty())
162  {
163  return;
164  }
165 
166  if (!Directory.Exists(algorithmLocation))
167  {
168  Log.Error($@"PythonInitializer.AddAlgorithmLocationPath(): {
169  Messages.PythonInitializer.UnableToLocateAlgorithm(algorithmLocation)}");
170  return;
171  }
172 
173  _algorithmLocation = algorithmLocation;
174  AddPythonPaths(new[] { _algorithmLocation });
175  }
176 
177  /// <summary>
178  /// Resets the algorithm location path so another can be set
179  /// </summary>
180  public static void ResetAlgorithmLocationPath()
181  {
182  _algorithmLocation = null;
183  }
184 
185  /// <summary>
186  /// "Activate" a virtual Python environment by prepending its library storage to Pythons
187  /// path. This allows the libraries in this venv to be selected prior to our base install.
188  /// Requires PYTHONNET_PYDLL to be set to base install.
189  /// </summary>
190  /// <remarks>If a module is already loaded, Python will use its cached version first
191  /// these modules must be reloaded by reload() from importlib library</remarks>
192  public static bool ActivatePythonVirtualEnvironment(string pathToVirtualEnv)
193  {
194  if (string.IsNullOrEmpty(pathToVirtualEnv))
195  {
196  return false;
197  }
198 
199  if(!Directory.Exists(pathToVirtualEnv))
200  {
201  Log.Error($@"PythonIntializer.ActivatePythonVirtualEnvironment(): {
202  Messages.PythonInitializer.VirutalEnvironmentNotFound(pathToVirtualEnv)}");
203  return false;
204  }
205 
206  PathToVirtualEnv = pathToVirtualEnv;
207 
208  bool? includeSystemPackages = null;
209  var configFile = new FileInfo(Path.Combine(PathToVirtualEnv, "pyvenv.cfg"));
210  if(configFile.Exists)
211  {
212  foreach (var line in File.ReadAllLines(configFile.FullName))
213  {
214  if (line.Contains("include-system-site-packages", StringComparison.InvariantCultureIgnoreCase))
215  {
216  // format: include-system-site-packages = false (or true)
217  var equalsIndex = line.IndexOf('=', StringComparison.InvariantCultureIgnoreCase);
218  if(equalsIndex != -1 && line.Length > (equalsIndex + 1) && bool.TryParse(line.Substring(equalsIndex + 1).Trim(), out var result))
219  {
220  includeSystemPackages = result;
221  break;
222  }
223  }
224  }
225  }
226 
227  if(!includeSystemPackages.HasValue)
228  {
229  includeSystemPackages = true;
230  Log.Error($@"PythonIntializer.ActivatePythonVirtualEnvironment(): {
231  Messages.PythonInitializer.FailedToFindSystemPackagesConfiguration(pathToVirtualEnv, configFile)}");
232  }
233  else
234  {
235  Log.Trace($@"PythonIntializer.ActivatePythonVirtualEnvironment(): {
236  Messages.PythonInitializer.SystemPackagesConfigurationFound(pathToVirtualEnv, includeSystemPackages.Value)}");
237  }
238 
239  if (!includeSystemPackages.Value)
240  {
241  PythonEngine.SetNoSiteFlag();
242  }
243 
244  IncludeSystemPackages = includeSystemPackages.Value;
245 
246  TryInitPythonVirtualEnvironment();
247  return true;
248  }
249 
250  private static void TryInitPythonVirtualEnvironment()
251  {
252  if (!_isInitialized || string.IsNullOrEmpty(PathToVirtualEnv))
253  {
254  return;
255  }
256 
257  using (Py.GIL())
258  {
259  using dynamic sys = Py.Import("sys");
260  using var locals = new PyDict();
261  locals.SetItem("sys", sys);
262 
263  if (!IncludeSystemPackages)
264  {
265  var currentPath = (List<string>)sys.path.As<List<string>>();
266  var toRemove = new List<string>(currentPath.Where(s => s.Contains("site-packages", StringComparison.InvariantCultureIgnoreCase)));
267  if (toRemove.Count > 0)
268  {
269  var code = string.Join(";", toRemove.Select(s => $"sys.path.remove('{s}')"));
270  PythonEngine.Exec(code, locals: locals);
271  }
272  }
273 
274  // fix the prefixes to point to our venv
275  sys.prefix = PathToVirtualEnv;
276  sys.exec_prefix = PathToVirtualEnv;
277 
278  using dynamic site = Py.Import("site");
279  // This has to be overwritten because site module may already have been loaded by the interpreter (but not run yet)
280  site.PREFIXES = new List<PyObject> { sys.prefix, sys.exec_prefix };
281  // Run site path modification with tweaked prefixes
282  site.main();
283 
284  if (IncludeSystemPackages)
285  {
286  // let's make sure our site packages is at the start so that we support overriding system libraries with a version in the env
287  PythonEngine.Exec(@$"if sys.path[-1].startswith('{PathToVirtualEnv}'):
288  sys.path.insert(0, sys.path.pop())", locals: locals);
289  }
290 
291  if (Log.DebuggingEnabled)
292  {
293  using dynamic os = Py.Import("os");
294  var path = new List<string>();
295  foreach (var p in sys.path)
296  {
297  path.Add((string)p);
298  }
299 
300  Log.Debug($"PythonIntializer.InitPythonVirtualEnvironment(): PYTHONHOME: {os.getenv("PYTHONHOME")}." +
301  $" PYTHONPATH: {os.getenv("PYTHONPATH")}." +
302  $" sys.executable: {sys.executable}." +
303  $" sys.prefix: {sys.prefix}." +
304  $" sys.base_prefix: {sys.base_prefix}." +
305  $" sys.exec_prefix: {sys.exec_prefix}." +
306  $" sys.base_exec_prefix: {sys.base_exec_prefix}." +
307  $" sys.path: [{string.Join(",", path)}]");
308  }
309  }
310  }
311 
312  /// <summary>
313  /// Gets the python additional paths from the config and adds them to Python using the PythonInitializer
314  /// </summary>
315  private static void ConfigurePythonPaths()
316  {
317  var pythonAdditionalPaths = new List<string> { Environment.CurrentDirectory };
318  pythonAdditionalPaths.AddRange(Config.GetValue("python-additional-paths", Enumerable.Empty<string>()));
319  AddPythonPaths(pythonAdditionalPaths.Where(path =>
320  {
321  var pathExists = Directory.Exists(path);
322  if (!pathExists)
323  {
324  Log.Error($"PythonInitializer.ConfigurePythonPaths(): {Messages.PythonInitializer.PythonPathNotFound(path)}");
325  }
326 
327  return pathExists;
328  }));
329  }
330  }
331 }