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
|