File: PythonManager.h

package info (click to toggle)
camitk 6.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 389,496 kB
  • sloc: cpp: 103,476; sh: 2,448; python: 1,618; xml: 984; makefile: 128; perl: 84; sed: 20
file content (291 lines) | stat: -rw-r--r-- 13,251 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

#ifdef PYTHON_BINDING

#ifndef __PYTHON_MANAGER__
#define __PYTHON_MANAGER__

#include "CamiTKAPI.h"

#include <QDir>
#include <QProcess>

#pragma push_macro("slots")
#undef slots
#include <pybind11/embed.h>
#pragma pop_macro("slots")

namespace py = pybind11;

namespace camitk {

/**
 * @brief PythonManager manages the interaction with the python language from CamiTK using pybind11.
 *
 * This manager is in charge of
 * - starting and interacting with the python interpreter
 * - switching between python context (virtual environment and script).
 *
 * Note that in CamiTK, using python virtual environment is mandatory.
 *
 * A python context is made of:
 * - a virtual environment (the directory that contains the venv created by "python -m venv")
 * - a script to load as a module in the python interpreter
 *
 * Only one python context can be loaded at the same time. A locking mechanism
 * is used to avoid discrepancy: when a python context is initialized using lockContext(...)
 * the python manager will not allow another call to lockContext(..) until unlock() is called.
 * Note that if lockContext(..) can be called many times with the same context, only one final
 * call to unlock() is required.
 *
 * Most usage involved:
 * - initPython() initialize the python interpreter (does nothing if it was already initialized
 *   during the application life time)
 * - lockContext(..) load the given script and set the python interpreter in the given virtual env.
 *   This also locks the python manager until unlock() is called.
 * - unlock() release the lock on the current context
 *
 * For instance:
 * \code
 *
 * PythonManager::initPython(); // typically done in the client class constructor
 * ...
 *
 * // load the script as a pybind11 python module
 * // (typically done just before python function must be called)
 * mod = Python::lockContext(myVenvPath, myScript);
 *
 * // test that method foo exists and is a callable function
 * if (py::hasattr(mod, "foo") && py::isinstance<py::function>(mod.attr("foo"))) {
 *     // call the python function "foo(param)" in the python script defined in file myScript
 *     // with the python interpreter configured inside the virtual env defined in myVenvPath
 *     mod.attr("foo")(param);
 *     ...
 * }
 *
 * \endcode
 *
 * \note Python manager is not aware of who is using the python interpreter (that is, it is not
 * aware of PythonHotPlug classes).
 *
 * \note If you need to get more debug information from python itself, use PYTHONVERBOSE=1 bin/camitk-imp
 */
class CAMITK_API PythonManager {

public:
    /// @brief Initialize the python interpreter if possible.
    ///
    /// The interpreter can only be started if
    /// - CamiTK core was build using PYTHON_BINDING
    /// - python is installed on the machine
    /// - the camitk python module is available in python system paths or camitk install
    ///   library paths, see findPythonModule()
    /// This will determine:
    /// - the python version (and load python symbols on Linux as the python executable is
    ///   not linked to the python binary)
    /// - the path to the camitk python module
    /// And will then initialize the global interpreter.
    ///
    /// see pythonEnabled()
    ///
    /// @return true if the interpreter was initialized
    static bool initPython();

    /// @brief Returns the python status
    /// Python status specifies if python interpreter has been initialized and ready to run scripts
    /// and if it is the python version and camitk python module being used
    /// \note: PythonManager::getPythonStatus().contains("Python disabled") returns true if
    /// Python support is currently disabled
    static QString getPythonStatus();

    /// load the python user-script from the given venv config and return the pybind11 module object.
    /// If the switch to the virtual env went well and the module was loaded without any problem,
    /// it "locks" the PythonManager until unlock() is called.
    ///
    /// If called more than once for the same context, it will return the valid current module.
    ///
    /// @see PythonHotPlugActionExtension::callPython() for an usage example
    ///
    /// @return the pybind11 module object (might be uninitialized if something went wrong or if
    /// a different context is already locked)
    static py::module_ lockContext(QString virtualEnvPath, QString scriptPath);

    /// Run the given script in the given virtualEnvPath and returns a map of symbols created during the script execution.
    /// The returned QMap contains the variables of the local dictionary.
    /// You can then check the dictionary for created symbols.
    /// @see fromPython(..) for the list of supported types
    ///
    /// @see TestPythonScript::scriptVariable() for a usage example
    ///
    /// @param virtualEnvPath the virtual environment to use to run the script string
    /// @param pythonScript a QString that contains python instruction to run (beware this is NOT a path to a python script file)
    /// @param pythonError a QString that contains the python exception if any was generated
    /// @return the local dictionary created during the script execution as a QMap
    static QMap<QString, QVariant> runScript(QString virtualEnvPath, const QString& pythonScript, QString& pythonError);

    /// release the lock on the current context. After that, lockContext will be possible again
    static void unlock();

    /// get the detected python version (major.minor)
    static QString getPythonVersion();

    /// return a multiline string showing python environment debug information, including information
    /// about the current virtual environment.
    static QString pythonEnvironmentDebugInfo();

    /// Print the content of the given python dictionary
    static void dump(py::dict dict);

    /// Convert a py::handle to QVariant.
    /// Supported types are:
    /// - None → invalid QVariant (i.e. QVariant())
    /// - boolean py::bool_ → QVariant(bool)
    /// - int py::int_ → QVariant(int)
    /// - py::bytes_ → QByteArray
    /// - py::float_ → QVariant(double)
    /// - py::str → QVariant(QString)
    /// - list/tuple → QVariantList
    /// - dict → QVariantMap
    ///
    /// If the handle is of unsupported type, the method returns a QVariant(QString) with
    /// the value "<unsupported Python type 'type name'>" (if it can deduce the type name)
    static QVariant fromPython(const py::handle& value);

    /// Check that a .venv virtual venv path exists in the given path.
    /// Checks that:
    /// - .venv, .venv/bin, .venv/lib/pythonx.y/site-packages path exists
    /// - .venv/bin has a pip executable
    /// @param silent if true no warning are emitted
    /// @return true if everything is OK
    static bool checkVirtualEnvPath(QString virtualEnvRootPath, bool silent = true);

    /// create a virtual environment ".venv" as a subdirectory of the given path if does not
    /// have a valid virtual env yet (this calls checkVirtualEnvPath(..) first)
    /// @return true if everything went OK
    static bool createVirtualEnv(QString virtualEnvRootPath);

    /// install the given list of packages inside the given virtual env
    /// (i.e. using the virtual env pip command)
    /// @param progressMinimum is the current initial progress value
    /// @param progressMaximum is the maximum progress bar value to use when all packages are installed
    static bool installPackages(QString virtualEnvPath, QStringList packages, int progressMinimum = 0, int progressMaximum = 100);

    /// Associate the given QObject to the __dict__ of the given python object.
    ///
    /// @note only the first call associate the qObject to the pointer
    ///
    /// @see PythonHotPlugActionExtension for a usage example, where the pythonPointer is
    /// the pointer to a PythonHotPlugAction in the python world. setPythonPointer() used
    /// in conjunction with backupPythonState() and backupPythonState() to store
    /// the python __dict__ of the action between two calls (therefore storing any
    /// python symbols created/updated with "self.something = ...")
    ///
    /// Once called, backupPythonState() and restorePythonState() can be called any number of times
    static void setPythonPointer(QObject* qObject, py::object pythonPointer);

    /// backup the current state of the __dict__ of the python object associated with the given qObject.
    /// The __dict__ can then be restore any time using restorePythonState()
    static void backupPythonState(QObject* qObject);

    /// restore the __dict__ of the python object associated with the given qObject.
    /// @note backupPythonState() must be called at least once before calling restorePythonState()
    static void restorePythonState(QObject* qObject);

private:
    /// check which shared lib need to be preloaded and determine version
    static void resolvePythonSharedLibPathAndVersion();

    /// "manually" load python shared object
    static bool loadPythonSharedLibrary();

    /// find the filename (path) to the given python module searching in the python system paths
    /// and installed CamiTK library directories
    /// This method checks for a file that starts with "moduleName"
    /// followed by ".cpython" (or ".cp" on windows) and ending with .so (or .pyd on windows)
    static QString findPythonModule(const QString& moduleName);

    /// check if value is already in given list and if not insert it at the beginning of the list.
    /// This is used to modify sys.path and avoid multiple setup of the same path.
    /// @return true if value was inserted, false if it was already there
    static bool insertIfNotAlreadyInList(py::list* list, const QString& value);

    /// check if there are more than one package accessible
    /// from the python interpreter that has the same name but
    /// a different version
    static void checkPythonPackageConflicts();

    /// check that the python command is found in the system path
    /// @return the path to the command or null QString if not found
    static QString findPythonExecutable();

    /// import or load the given module name in the current python context.
    /// \warning you have to ensure that the interpreter is already configured in a valid
    /// current context or virtual env before calling this method
    ///
    /// @param clearAll if true the method will remove all previous symbols from the
    /// given module and call the garbage collector to effectively remove the symbols.
    ///
    /// Removing all symbols from the module is required for instance when a method
    /// is removed from the python script between to calls.
    static py::module_ importOrReload(const QString& moduleName, bool clearAll = false);

    /// clear dict, setup path to camitk module, enter virtual env and set up the console redirection.
    /// Setup the current python context to the given virtual env, then importOrReload the python user-script (if given)
    /// @return true if everything went well
    static bool setupPython(QString virtualEnvPath, QString scriptPath = QString());

    /// The current python status (is interpreter initialized and ready, which python version, camitk python module location)
    static QString pythonStatusString;

    /// path to where the camitk.cpython-312-x86_64-linux-gnu.so / camitk.dll resides
    static QDir pythonCamitkModulePath;

    /// filename of system python.so to load (contains the version number)
    static QString pythonSoLib;

    /// python major.minor version
    static QString pythonVersion;

    /// path to the system python executable (or null QString if not found)
    static QString systemPythonExecutable;

    /// lock status
    static bool isLocked;

    /// Store the given py:object (can then be use to backup/restore __dict__ value)
    static QMap<QObject*, QPair<py::object, py::dict>> pythonStateMap;

    /// current python context and loaded module
    static QString currentVirtualEnvPath;
    static QString currentScriptPath;
    static py::module_ currentModule;

};

} // namespace camitk

#endif // __PYTHON_MANAGER__
#endif // PYTHON_BINDING