From fde9c16cbf9963c50ce865a80839a5cd296497c3 Mon Sep 17 00:00:00 2001 From: tmontaigu Date: Sun, 14 Jun 2026 12:45:11 +0200 Subject: [PATCH 1/4] format --- wrapper/cccorelib/src/MeshSamplingTools.cpp | 35 +++++++++------------ wrapper/cccorelib/src/ScalarField.cpp | 4 +-- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/wrapper/cccorelib/src/MeshSamplingTools.cpp b/wrapper/cccorelib/src/MeshSamplingTools.cpp index 8e4d4c3..a511dcd 100644 --- a/wrapper/cccorelib/src/MeshSamplingTools.cpp +++ b/wrapper/cccorelib/src/MeshSamplingTools.cpp @@ -44,9 +44,8 @@ An edge used by exactly one triangle signals a border/hole; an edge shared by more than two triangles signals a non-manifold edge. )doc") .def(py::init<>()) - .def_readonly("edgesCount", - &MeshSamplingTools::EdgeConnectivityStats::edgesCount, - "Total number of edges.") + .def_readonly( + "edgesCount", &MeshSamplingTools::EdgeConnectivityStats::edgesCount, "Total number of edges.") .def_readonly("edgesNotShared", &MeshSamplingTools::EdgeConnectivityStats::edgesNotShared, "Edges used by only one triangle (border/hole).") @@ -86,9 +85,8 @@ check — any non-zero ``edgesNotShared`` means the mesh has holes. MeshSamplingTools::EdgeConnectivityStats stats; if (!MeshSamplingTools::computeMeshEdgesConnectivity(mesh, stats)) { - throw std::runtime_error( - "MeshSamplingTools.computeMeshEdgesConnectivity failed " - "(invalid mesh or not enough memory)."); + throw std::runtime_error("MeshSamplingTools.computeMeshEdgesConnectivity failed " + "(invalid mesh or not enough memory)."); } return stats; }, @@ -108,9 +106,8 @@ computeMeshVolume. MeshSamplingTools::EdgeConnectivityStats stats; if (!MeshSamplingTools::flagMeshVerticesByType(mesh, flags, &stats)) { - throw std::runtime_error( - "MeshSamplingTools.flagMeshVerticesByType failed " - "(invalid mesh or not enough memory)."); + throw std::runtime_error("MeshSamplingTools.flagMeshVerticesByType failed " + "(invalid mesh or not enough memory)."); } return stats; }, @@ -132,17 +129,15 @@ the way. CCCoreLib::GenericProgressCallback *progressCb) { std::vector triIndices; - CCCoreLib::PointCloud *pc = MeshSamplingTools::samplePointsOnMesh( - mesh, samplingDensity, progressCb, &triIndices); + CCCoreLib::PointCloud *pc = + MeshSamplingTools::samplePointsOnMesh(mesh, samplingDensity, progressCb, &triIndices); if (!pc) { - throw std::runtime_error( - "MeshSamplingTools.samplePointsOnMesh (by density) failed."); + throw std::runtime_error("MeshSamplingTools.samplePointsOnMesh (by density) failed."); } // take_ownership: CCCoreLib new's a fresh PointCloud; transfer // ownership to Python so it's deleted when the handle is GC'd. - return py::make_tuple(py::cast(pc, py::return_value_policy::take_ownership), - triIndices); + return py::make_tuple(py::cast(pc, py::return_value_policy::take_ownership), triIndices); }, "mesh"_a, "samplingDensity"_a, @@ -177,15 +172,13 @@ RuntimeError CCCoreLib::GenericProgressCallback *progressCb) { std::vector triIndices; - CCCoreLib::PointCloud *pc = MeshSamplingTools::samplePointsOnMesh( - mesh, numberOfPoints, progressCb, &triIndices); + CCCoreLib::PointCloud *pc = + MeshSamplingTools::samplePointsOnMesh(mesh, numberOfPoints, progressCb, &triIndices); if (!pc) { - throw std::runtime_error( - "MeshSamplingTools.samplePointsOnMesh (by count) failed."); + throw std::runtime_error("MeshSamplingTools.samplePointsOnMesh (by count) failed."); } - return py::make_tuple(py::cast(pc, py::return_value_policy::take_ownership), - triIndices); + return py::make_tuple(py::cast(pc, py::return_value_policy::take_ownership), triIndices); }, "mesh"_a, "numberOfPoints"_a, diff --git a/wrapper/cccorelib/src/ScalarField.cpp b/wrapper/cccorelib/src/ScalarField.cpp index 15920d2..0f1b0da 100644 --- a/wrapper/cccorelib/src/ScalarField.cpp +++ b/wrapper/cccorelib/src/ScalarField.cpp @@ -299,9 +299,7 @@ at the float level. .def("resetOffset", &CCCoreLib::ScalarField::resetOffset, "Resets the offset to 0.0 and marks it as 'not explicitly set'.") - .def("clear", - &CCCoreLib::ScalarField::clear, - "Empties the scalar field and resets the offset state.") + .def("clear", &CCCoreLib::ScalarField::clear, "Empties the scalar field and resets the offset state.") .def("invert", &CCCoreLib::ScalarField::invert, R"doc( From 05fb24f571c0e626f32d5ad6cee0be791413aa9f Mon Sep 17 00:00:00 2001 From: tmontaigu Date: Sun, 14 Jun 2026 12:41:27 +0200 Subject: [PATCH 2/4] Fix Venv support by pulling info from python Call the python executable to get all the info we need --- src/PythonConfig.cpp | 236 ++++++++------------------------------ src/PythonConfig.h | 84 +++++--------- src/PythonInterpreter.cpp | 72 +++++++----- src/PythonInterpreter.h | 5 - src/PythonPlugin.cpp | 4 +- 5 files changed, 123 insertions(+), 278 deletions(-) diff --git a/src/PythonConfig.cpp b/src/PythonConfig.cpp index 022f5e7..0019e57 100644 --- a/src/PythonConfig.cpp +++ b/src/PythonConfig.cpp @@ -18,22 +18,11 @@ #include "Utilities.h" #include -#include #include #include #include -#include -#include #include -#if !defined(USE_EMBEDDED_MODULES) && defined(Q_OS_WINDOWS) -static QString WindowsBundledSitePackagesPath() -{ - return QDir::listSeparator() + QApplication::applicationDirPath() + - "/plugins/Python/Lib/site-packages"; -} -#endif - //================================================================================ Version::Version(const QString &versionStr) : Version() @@ -72,71 +61,6 @@ static Version GetPythonExeVersion(QProcess &pythonProcess) } return Version{}; } -//================================================================================ - -struct PyVenvCfg -{ - PyVenvCfg() = default; - - static PyVenvCfg FromFile(const QString &path); - - QString home{}; - bool includeSystemSitesPackages{}; - Version version; -}; - -PyVenvCfg PyVenvCfg::FromFile(const QString &path) -{ - PyVenvCfg cfg{}; - - QFile cfgFile(path); - if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) - { - while (!cfgFile.atEnd()) - { - QString line = cfgFile.readLine(); - QStringList v = line.split("="); - - if (v.size() == 2) - { - QString name = v[0].simplified(); - QString value = v[1].simplified(); - - if (name == "home") - { - cfg.home = value; - } - else if (name == "include-system-site-packages") - { - cfg.includeSystemSitesPackages = (value == "true"); - } - else if (name == "version") - { - cfg.version = Version(value); - } - } - } - } - - return cfg; -} - -//================================================================================ - -bool PythonConfigPaths::isSet() const -{ - return m_pythonHome != nullptr && m_pythonPath != nullptr; -} - -const wchar_t *PythonConfigPaths::pythonHome() const -{ - return m_pythonHome.get(); -} - -const wchar_t *PythonConfigPaths::pythonPath() const -{ - return m_pythonPath.get(); -} //================================================================================ @@ -198,102 +122,80 @@ void PythonConfig::initFromLocation(const QString &prefix) if (!envRoot.exists()) { m_pythonHome = QString(); - m_pythonPath = QString(); m_type = Type::Unknown; return; } + m_pythonHome = envRoot.path(); + if (envRoot.exists("pyvenv.cfg")) { - QString pythonExePath = PathToPythonExecutableInEnv(Type::Venv, prefix); - initFromPythonExecutable(pythonExePath); - if (m_pythonHome.isEmpty() && m_pythonPath.isEmpty()) - { - qDebug() << "Failed to get paths info from python executable at (venv)" - << pythonExePath; - initVenv(envRoot.path()); - } - else - { - m_type = Type::Venv; - } + m_type = Type::Venv; } else if (envRoot.exists("conda-meta")) { - QString pythonExePath = PathToPythonExecutableInEnv(Type::Conda, prefix); - initFromPythonExecutable(pythonExePath); - if (m_pythonHome.isEmpty() && m_pythonPath.isEmpty()) - { - qDebug() << "Failed to get paths info from python executable at (conda)" - << pythonExePath; - initCondaEnv(envRoot.path()); - } - else - { - m_type = Type::Conda; - } + m_type = Type::Conda; } else -#if defined(Q_OS_MACOS) - { - QString pythonExePath = PathToPythonExecutableInEnv(Type::Unknown, prefix); - initFromPythonExecutable(pythonExePath); - if (m_pythonHome.isEmpty() && m_pythonPath.isEmpty()) - { - qDebug() << "Failed to get paths info from python executable at (bundled)" - << pythonExePath; - initVenv(envRoot.path()); - } - else - { - m_type = Type::Unknown; - } - } -#else { - m_pythonHome = envRoot.path(); - m_pythonPath = QString("%1/DLLs;%1/lib;%1/Lib/site-packages;").arg(m_pythonHome); m_type = Type::Unknown; - -#if !defined(USE_EMBEDDED_MODULES) && defined(Q_OS_WINDOWS) - m_pythonPath.append(WindowsBundledSitePackagesPath()); -#endif } -#endif } -void PythonConfig::initCondaEnv(const QString &condaPrefix) +QString PythonConfig::pythonExecutable() const { - m_type = Type::Conda; - m_pythonHome = condaPrefix; - m_pythonPath = QString("%1/DLLs;%1/lib;%1/Lib/site-packages;").arg(condaPrefix); - -#if !defined(USE_EMBEDDED_MODULES) && defined(Q_OS_WINDOWS) - m_pythonPath.append(WindowsBundledSitePackagesPath()); -#endif + return PathToPythonExecutableInEnv(m_type, m_pythonHome); } -void PythonConfig::initVenv(const QString &venvPrefix) +ResolvedPythonPaths PythonConfig::resolvePaths() const { - PyVenvCfg cfg = PyVenvCfg::FromFile(QString("%1/pyvenv.cfg").arg(venvPrefix)); + QProcess pythonProcess; + preparePythonProcess(pythonProcess); + // -I runs the interpreter in isolated mode so the reported values match what + // the embedded (also isolated) interpreter should use: standard library + + // the environment's site-packages, without user-site or ambient PYTHON*. + // The four prefixes are printed first (one per line), then the search paths. + pythonProcess.setArguments({"-I", + "-c", + "import sys; " + "print(sys.prefix); print(sys.exec_prefix); " + "print(sys.base_prefix); print(sys.base_exec_prefix); " + "print(chr(10).join(sys.path))"}); + pythonProcess.start(QIODevice::ReadOnly); + pythonProcess.waitForFinished(); + + const QString output = QString::fromUtf8(pythonProcess.readAllStandardOutput()); + + QStringList lines; + for (const QString &line : output.split('\n')) + { + const QString trimmed = line.trimmed(); + if (!trimmed.isEmpty()) + { + lines.append(trimmed); + } + } - m_type = Type::Venv; - m_pythonHome = venvPrefix; - m_pythonPath = QString("%1/Lib/site-packages;%3/DLLS;%3/lib").arg(venvPrefix, cfg.home); - if (cfg.includeSystemSitesPackages) + // 4 prefixes + at least one search path. + if (lines.size() < 5) { - m_pythonPath.append(QString("%1/Lib/site-packages").arg(cfg.home)); + plgWarning() << "Could not query Python paths from " << pythonExecutable() << ": got '" + << output << "'"; + return {}; } -#if !defined(USE_EMBEDDED_MODULES) && defined(Q_OS_WINDOWS) - m_pythonPath.append(WindowsBundledSitePackagesPath()); -#endif + ResolvedPythonPaths paths; + paths.prefix = lines.takeFirst(); + paths.execPrefix = lines.takeFirst(); + paths.basePrefix = lines.takeFirst(); + paths.baseExecPrefix = lines.takeFirst(); + paths.moduleSearchPaths = lines; + return paths; } void PythonConfig::preparePythonProcess(QProcess &pythonProcess) const { - const QString pythonExePath = PathToPythonExecutableInEnv(type(), m_pythonHome); - pythonProcess.setProgram(pythonExePath); + pythonProcess.setProgram(pythonExecutable()); // Conda env have SSL related libraries stored in a part that is not // in the path of the python exe, we have to add it ourselves. @@ -307,14 +209,6 @@ void PythonConfig::preparePythonProcess(QProcess &pythonProcess) const } } -PythonConfigPaths PythonConfig::pythonCompatiblePaths() const -{ - PythonConfigPaths paths; - paths.m_pythonHome.reset(QStringToWcharArray(m_pythonHome)); - paths.m_pythonPath.reset(QStringToWcharArray(m_pythonPath)); - return paths; -} - Version PythonConfig::getVersion() const { QProcess pythonProcess; @@ -366,8 +260,7 @@ PythonConfig PythonConfig::fromContainingEnvironment() QString root = qEnvironmentVariable("CONDA_PREFIX"); if (!root.isEmpty()) { - const QString pythonExePath = PathToPythonExecutableInEnv(Type::Conda, root); - config.initFromPythonExecutable(pythonExePath); + config.m_pythonHome = root; config.m_type = Type::Conda; return config; } @@ -375,43 +268,10 @@ PythonConfig PythonConfig::fromContainingEnvironment() root = qEnvironmentVariable("VIRTUAL_ENV"); if (!root.isEmpty()) { - const QString pythonExePath = PathToPythonExecutableInEnv(Type::Venv, root); - config.initFromPythonExecutable(pythonExePath); + config.m_pythonHome = root; config.m_type = Type::Venv; return config; } return config; } - -void PythonConfig::initFromPythonExecutable(const QString &pythonExecutable) -{ - m_type = Type::Unknown; - - const QString pythonPathScript = QStringLiteral( - "import os;import sys;print(os.pathsep.join(sys.path[1:]));print(sys.prefix, end='')"); - - QProcess pythonProcess; - pythonProcess.setProgram(pythonExecutable); - pythonProcess.setArguments({"-c", pythonPathScript}); - pythonProcess.start(QIODevice::ReadOnly); - pythonProcess.waitForFinished(); - - const QString result = QString::fromUtf8(pythonProcess.readAllStandardOutput()); - - QStringList pathsAndHome = result.split('\n'); - - if (pathsAndHome.size() != 2) - { - plgWarning() << "'" << result << "' could not be parsed as a list if paths and a home path." - << "Expected 2 strings found " << pathsAndHome.size(); - return; - } - - m_pythonPath = pathsAndHome.takeFirst(); - m_pythonHome = pathsAndHome.takeFirst(); - -#if !defined(USE_EMBEDDED_MODULES) && defined(Q_OS_WINDOWS) - m_pythonPath.append(WindowsBundledSitePackagesPath()); -#endif -} diff --git a/src/PythonConfig.h b/src/PythonConfig.h index e02decb..1257a53 100644 --- a/src/PythonConfig.h +++ b/src/PythonConfig.h @@ -19,16 +19,13 @@ #define PYTHON_PLUGIN_PYTHON_CONFIG_H #include +#include #include -#include - #undef slots #include -struct PyVenvCfg; class QProcess; -class PythonConfigPaths; class QWidget; /// Simple representation of a SemVer version @@ -71,11 +68,22 @@ struct Version /// Python Version the plugin was compiled against constexpr Version PythonVersion(PY_MAJOR_VERSION, PY_MINOR_VERSION, PY_MICRO_VERSION); -/// This class infers the right python home and python path -/// for the python environment to be used. -/// -/// Its only used for Windows (on other platform it doesn't do much) as -/// on Windows we can't rely on the system's python. +/// Path-configuration values resolved by querying an environment's interpreter. +struct ResolvedPythonPaths +{ + QString prefix{}; + QString execPrefix{}; + QString basePrefix{}; + QString baseExecPrefix{}; + QStringList moduleSearchPaths{}; + + /// True if the environment's interpreter could be queried. + bool isValid() const + { + return !moduleSearchPaths.isEmpty(); + } +}; + class PythonConfig final { public: @@ -119,14 +127,22 @@ class PythonConfig final return m_pythonHome; } + /// Returns the path to the Python interpreter executable of this + /// environment (e.g. `/bin/python` or `/Scripts/python.exe`). + QString pythonExecutable() const; + + /// Queries this environment's interpreter for the path-configuration values + /// (prefixes and `sys.path`) needed to initialize the embedded interpreter. + /// + /// \return The resolved paths, or an invalid result (see + /// ResolvedPythonPaths::isValid) if the interpreter could not be + /// queried. + ResolvedPythonPaths resolvePaths() const; + /// Sets the necessary settings of the QProcess so that /// it uses the correct Python exe. void preparePythonProcess(QProcess &pythonProcess) const; - /// Returns the python home & path stored in - /// types that the CPython API can use. - PythonConfigPaths pythonCompatiblePaths() const; - /// Calls the python.exe of this environment / config /// to get its version. /// @@ -163,55 +179,15 @@ class PythonConfig final /// Will try to guess if the environment is a conda env /// or a python venv void initFromLocation(const QString &prefix); - /// Initialize the paths to use the conda environment stored at condaPrefix - void initCondaEnv(const QString &condaPrefix); - /// Initialize the paths to use the python venv stored at venvPrefix. - void initVenv(const QString &venvPrefix); - - void initFromPythonExecutable(const QString &pythonExecutable); template friend ostream &operator<<(ostream &o, const PythonConfig &config) { - o << "PythonConfig { type: " << config.m_type << ", home: '" << config.m_pythonHome - << "', path: '" << config.m_pythonPath << "'}"; + o << "PythonConfig { type: " << config.m_type << ", home: '" << config.m_pythonHome << "'}"; return o; } private: QString m_pythonHome{}; - QString m_pythonPath{}; Type m_type{Type::Unknown}; }; - -/// Holds strings of the PythonHome & PythonPath, -/// in types that are compatible with CPython API. -/// -/// They are meant to be used for `Py_SetPythonHome` and `Py_SetPath`. -/// See: -/// - https://docs.python.org/3/c-api/init.html#c.Py_SetPythonHome -/// - https://docs.python.org/3/c-api/init.html#c.Py_SetPath -class PythonConfigPaths final -{ - friend PythonConfig; - - public: - /// Default ctor, does not initialize pythonHome & pythonPath - PythonConfigPaths() = default; - - /// returns true if both paths are non empty - bool isSet() const; - - /// Returns the pythonHome - const wchar_t *pythonHome() const; - - /// Returns the pythonPath - const wchar_t *pythonPath() const; - - private: - /// Once Py_SetPythonHome is used, the value of m_pythonHome must never change - /// and must not be freed until the interpreter is uninitialized. - std::unique_ptr m_pythonHome{}; - /// m_pythonPath can however be freed after Py_SetPath was used - std::unique_ptr m_pythonPath{}; -}; #endif // PYTHON_PLUGIN_PYTHON_CONFIG_H diff --git a/src/PythonInterpreter.cpp b/src/PythonInterpreter.cpp index ea3c69f..19596d6 100644 --- a/src/PythonInterpreter.cpp +++ b/src/PythonInterpreter.cpp @@ -22,6 +22,8 @@ #include +#include + #include #include #include @@ -222,40 +224,48 @@ void PythonInterpreter::initialize(const PythonConfig &config) // // https://www.python.org/dev/peps/pep-0587/ // https://docs.python.org/3/c-api/init_config.html#init-python-config - m_config = config.pythonCompatiblePaths(); - PyStatus status; + const ResolvedPythonPaths paths = config.resolvePaths(); + if (!paths.isValid()) + { + throw std::runtime_error( + "Failed to query the environment's Python interpreter for its configuration"); + } PyConfig pyConfig; PyConfig_InitPythonConfig(&pyConfig); pyConfig.isolated = 1; - status = PyConfig_SetString(&pyConfig, &pyConfig.home, m_config.pythonHome()); - if (PyStatus_Exception(status)) + const auto check = [&pyConfig](PyStatus status) { - PyConfig_Clear(&pyConfig); - throw std::runtime_error(status.err_msg); - } - - status = PyConfig_SetString(&pyConfig, &pyConfig.pythonpath_env, m_config.pythonPath()); - if (PyStatus_Exception(status)) + if (PyStatus_Exception(status)) + { + const std::string message = status.err_msg ? status.err_msg : "unknown error"; + PyConfig_Clear(&pyConfig); + throw std::runtime_error(message); + } + }; + + const auto setString = [&](wchar_t **field, const QString &value) { - PyConfig_Clear(&pyConfig); - throw std::runtime_error(status.err_msg); - } - - status = PyConfig_Read(&pyConfig); - if (PyStatus_Exception(status)) + const std::unique_ptr wide(QStringToWcharArray(value)); + check(PyConfig_SetString(&pyConfig, field, wide.get())); + }; + + setString(&pyConfig.prefix, paths.prefix); + setString(&pyConfig.exec_prefix, paths.execPrefix); + setString(&pyConfig.base_prefix, paths.basePrefix); + setString(&pyConfig.base_exec_prefix, paths.baseExecPrefix); + setString(&pyConfig.executable, config.pythonExecutable()); + + pyConfig.module_search_paths_set = 1; + for (const QString &path : paths.moduleSearchPaths) { - PyConfig_Clear(&pyConfig); - throw std::runtime_error(status.err_msg); + const std::unique_ptr wide(QStringToWcharArray(path)); + check(PyWideStringList_Append(&pyConfig.module_search_paths, wide.get())); } - status = Py_InitializeFromConfig(&pyConfig); - if (PyStatus_Exception(status)) - { - PyConfig_Clear(&pyConfig); - throw std::runtime_error(status.err_msg); - } + check(PyConfig_Read(&pyConfig)); + check(Py_InitializeFromConfig(&pyConfig)); } else { @@ -265,6 +275,15 @@ void PythonInterpreter::initialize(const PythonConfig &config) // Make sure this module is imported // so that we can later easily construct our consoles. py::module::import("ccinternals"); + +#if !defined(USE_EMBEDDED_MODULES) && defined(Q_OS_WINDOWS) + // In non-embedded-modules builds the CloudCompare Python wrappers (pycc, + // cccorelib) are installed next to the application rather than inside the + // selected environment, so make that location importable. + const QString bundledSitePackages = + QApplication::applicationDirPath() + "/plugins/Python/Lib/site-packages"; + py::module::import("sys").attr("path").attr("append")(bundledSitePackages.toStdString()); +#endif } bool PythonInterpreter::IsInitialized() @@ -293,8 +312,3 @@ bool PythonInterpreter::isExecuting() const { return m_isExecuting; } - -const PythonConfigPaths &PythonInterpreter::config() const -{ - return m_config; -} diff --git a/src/PythonInterpreter.h b/src/PythonInterpreter.h index d29a795..6c10e91 100644 --- a/src/PythonInterpreter.h +++ b/src/PythonInterpreter.h @@ -21,8 +21,6 @@ #include "PythonConfig.h" #include -#include - #undef slots #include #include @@ -59,7 +57,6 @@ class PythonInterpreter final : public QObject void initialize(const PythonConfig &config); void finalize(); static bool IsInitialized(); - const PythonConfigPaths &config() const; /// Execution functions (and slots) public Q_SLOTS: @@ -86,8 +83,6 @@ class PythonInterpreter final : public QObject private: bool m_isExecuting{false}; - PythonConfigPaths m_config; - #ifdef Q_OS_UNIX void *m_libPythonHandle{nullptr}; #endif diff --git a/src/PythonPlugin.cpp b/src/PythonPlugin.cpp index b321161..29dffa3 100644 --- a/src/PythonPlugin.cpp +++ b/src/PythonPlugin.cpp @@ -92,8 +92,8 @@ PythonPlugin::PythonPlugin(QObject *parent) { config = PythonConfig::fromContainingEnvironment(); isDefaultPythonEnv = false; - plgPrint() << "CloudCompare was loaded from within a " << config.type() << "env. " - << "Will try to use it"; + plgPrint() << "CloudCompare was loaded from within a " << config.type() + << " environment. Will try to use it"; } else { From b22d92a1c8169d0fe56078a8fac3fc5e5e83e48c Mon Sep 17 00:00:00 2001 From: tmontaigu Date: Sun, 14 Jun 2026 17:27:16 +0200 Subject: [PATCH 3/4] Improve printing of exception, and do a numpy check --- src/PythonInterpreter.cpp | 77 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/src/PythonInterpreter.cpp b/src/PythonInterpreter.cpp index 19596d6..c897e1f 100644 --- a/src/PythonInterpreter.cpp +++ b/src/PythonInterpreter.cpp @@ -42,6 +42,72 @@ namespace py = pybind11; +namespace +{ +/// Formats a Python exception with its full cause/context chain, the way Python +/// prints it to stderr. pybind11's `error_already_set::what()` only reports the +/// leaf exception, which hides the original cause - for example a missing +/// dependency behind a generic "ImportError: initialization failed". +QString FormatPythonException(const py::error_already_set &error) +{ + try + { + const py::gil_scoped_acquire gil; + const py::module_ traceback = py::module_::import("traceback"); + const py::list lines = + traceback.attr("format_exception")(error.type(), error.value(), error.trace()); + + QString formatted; + for (const py::handle line : lines) + { + formatted += QString::fromStdString(line.cast()); + } + return formatted.trimmed(); + } + catch (const std::exception &) + { + // Fall back to the leaf message if formatting itself fails. + return QString::fromUtf8(error.what()); + } +} + +/// Returns the message to display for a caught exception, expanding Python +/// exceptions into their full traceback. +QString ExceptionText(const std::exception &error) +{ + if (const auto *pythonError = dynamic_cast(&error)) + { + return FormatPythonException(*pythonError); + } + return QString::fromUtf8(error.what()); +} + +/// cccorelib and pycc bind NumPy and import it while initializing their own +/// modules. When the selected environment lacks NumPy those imports fail with an +/// opaque "initialization failed" error, so detect it up front and tell the user +/// how to fix it. +void WarnIfNumPyIsMissing(const PythonConfig &config) +{ + try + { + const py::module_ importlibUtil = py::module_::import("importlib.util"); + if (!importlibUtil.attr("find_spec")("numpy").is_none()) + { + return; + } + } + catch (const py::error_already_set &) + { + // Fall through and warn: if NumPy cannot even be looked up, importing the + // wrappers will not work either. + } + + plgWarning() << "NumPy was not found in the selected Python environment. The 'cccorelib' " + "and 'pycc' modules require it and will fail to import. Install it with: " + << config.pythonExecutable() << " -m pip install numpy"; +} +} // namespace + static py::dict CreateGlobals() { py::dict globals; @@ -76,7 +142,7 @@ bool PythonInterpreter::executeFile(const std::string &filepath) } catch (const std::exception &e) { - ccLog::Warning(e.what()); + ccLog::Warning(ExceptionText(e)); success = false; } @@ -116,15 +182,16 @@ void PythonInterpreter::executeCodeString(const std::string &code, } catch (const std::exception &e) { + const QString text = ExceptionText(e); if (output) { - auto message = new QListWidgetItem(e.what()); + auto message = new QListWidgetItem(text); message->setForeground(Qt::red); output->addItem(message); } else { - ccLog::Error(e.what()); + ccLog::Error(text); } } @@ -173,7 +240,7 @@ void PythonInterpreter::executeFunction(const pybind11::object &function) } catch (const std::exception &e) { - ccLog::Error("Failed to start Python actions: %s", e.what()); + ccLog::Error(QStringLiteral("Failed to start Python actions: %1").arg(ExceptionText(e))); } m_isExecuting = false; Q_EMIT executionFinished(); @@ -284,6 +351,8 @@ void PythonInterpreter::initialize(const PythonConfig &config) QApplication::applicationDirPath() + "/plugins/Python/Lib/site-packages"; py::module::import("sys").attr("path").attr("append")(bundledSitePackages.toStdString()); #endif + + WarnIfNumPyIsMissing(config); } bool PythonInterpreter::IsInitialized() From b2d2b15f4c5426777c4487e322389668246eff03 Mon Sep 17 00:00:00 2001 From: tmontaigu Date: Sun, 14 Jun 2026 17:30:12 +0200 Subject: [PATCH 4/4] Add button to create a venv Add in the settings a button to create a new venv and automatically select it to be used on the next startup --- src/CMakeLists.txt | 6 +- src/CreateVenvForm.cpp | 28 +++++++++ src/CreateVenvForm.h | 26 ++++++++ src/PythonRuntimeSettings.cpp | 33 +++++++++++ src/PythonRuntimeSettings.h | 1 + src/Ui/CMakeLists.txt | 3 +- src/Ui/CreateVenvForm.ui | 101 ++++++++++++++++++++++++++++++++ src/Ui/PythonRuntimeSettings.ui | 9 ++- 8 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 src/CreateVenvForm.cpp create mode 100644 src/CreateVenvForm.h create mode 100644 src/Ui/CreateVenvForm.ui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 338d7ab..d8cff63 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,6 +9,8 @@ target_sources( ${CMAKE_CURRENT_LIST_DIR}/AboutDialog.h ${CMAKE_CURRENT_LIST_DIR}/ColorScheme.cpp ${CMAKE_CURRENT_LIST_DIR}/ColorScheme.h + ${CMAKE_CURRENT_LIST_DIR}/CreateVenvForm.cpp + ${CMAKE_CURRENT_LIST_DIR}/CreateVenvForm.h ${CMAKE_CURRENT_LIST_DIR}/FileRunner.cpp ${CMAKE_CURRENT_LIST_DIR}/FileRunner.h ${CMAKE_CURRENT_LIST_DIR}/PackageManager.cpp @@ -25,8 +27,8 @@ target_sources( ${CMAKE_CURRENT_LIST_DIR}/PythonPlugin.h ${CMAKE_CURRENT_LIST_DIR}/PythonPluginManager.cpp ${CMAKE_CURRENT_LIST_DIR}/PythonPluginManager.h - ${CMAKE_CURRENT_LIST_DIR}/PythonRuntimeSettings.cpp - ${CMAKE_CURRENT_LIST_DIR}/PythonRuntimeSettings.h + ${CMAKE_CURRENT_LIST_DIR}/PythonRuntimeSettings.cpp + ${CMAKE_CURRENT_LIST_DIR}/PythonRuntimeSettings.h ${CMAKE_CURRENT_LIST_DIR}/PythonRepl.cpp ${CMAKE_CURRENT_LIST_DIR}/PythonRepl.h ${CMAKE_CURRENT_LIST_DIR}/Resources.h diff --git a/src/CreateVenvForm.cpp b/src/CreateVenvForm.cpp new file mode 100644 index 0000000..8115de5 --- /dev/null +++ b/src/CreateVenvForm.cpp @@ -0,0 +1,28 @@ +#include "CreateVenvForm.h" +#include "ui_CreateVenvForm.h" + +#include + +CreateVenvForm::CreateVenvForm(QWidget *parent) : QDialog(parent), ui(new Ui::CreateVenvForm) +{ + ui->setupUi(this); + + connect(ui->locationBtn, &QPushButton::clicked, this, &CreateVenvForm::promptForLocation); +} + +CreateVenvForm::~CreateVenvForm() +{ + delete ui; +} + +QString CreateVenvForm::path() const +{ + return QString("%1/%2").arg(ui->locationEdit->text(), ui->nameEdit->text()); +} + +void CreateVenvForm::promptForLocation() +{ + QString selectedDir = QFileDialog::getExistingDirectory( + this, "Python Environment Root", ui->locationEdit->text()); + ui->locationEdit->setText(selectedDir); +} diff --git a/src/CreateVenvForm.h b/src/CreateVenvForm.h new file mode 100644 index 0000000..4abc23c --- /dev/null +++ b/src/CreateVenvForm.h @@ -0,0 +1,26 @@ +#ifndef CREATEVENVFORM_H +#define CREATEVENVFORM_H + +#include + +namespace Ui +{ +class CreateVenvForm; +} + +class CreateVenvForm : public QDialog +{ + Q_OBJECT + + public: + explicit CreateVenvForm(QWidget *parent = nullptr); + ~CreateVenvForm(); + + QString path() const; + + private: + void promptForLocation(); + Ui::CreateVenvForm *ui; +}; + +#endif // CREATEVENVFORM_H diff --git a/src/PythonRuntimeSettings.cpp b/src/PythonRuntimeSettings.cpp index 791bda4..3e8d401 100644 --- a/src/PythonRuntimeSettings.cpp +++ b/src/PythonRuntimeSettings.cpp @@ -15,7 +15,9 @@ // # # // ########################################################################## #include "PythonRuntimeSettings.h" +#include "CreateVenvForm.h" #include "Resources.h" +#include "Utilities.h" #include #include @@ -30,6 +32,8 @@ #include #include +#include + #include /// Simple Dialog that displays a Line Edit with a button next to it @@ -168,6 +172,8 @@ PythonRuntimeSettings::PythonRuntimeSettings(QWidget *parent) &QPushButton::clicked, this, &PythonRuntimeSettings::handleSelectLocalEnv); + connect( + m_ui->createVenvBtn, &QPushButton::clicked, this, &PythonRuntimeSettings::handleCreateVenv); restoreSettings(); m_ui->informationLabel->hide(); } @@ -256,6 +262,33 @@ void PythonRuntimeSettings::handleSelectLocalEnv() } } +void PythonRuntimeSettings::handleCreateVenv() +{ + CreateVenvForm form(this); + + if (form.exec() != QDialog::Accepted) + { + return; + } + + const QString path = form.path(); + + try + { + const auto createVenvFn = pybind11::module_::import("venv").attr("create"); + createVenvFn(path); + } + catch (const std::exception &e) + { + plgError() << "Failed to create venv: " << e.what(); + return; + } + + m_ui->localEnvPathLabel->setText(path); + const int idx = m_ui->envTypeComboBox->findText("Local"); + m_ui->envTypeComboBox->setCurrentIndex(idx); +} + QStringList PythonRuntimeSettings::pluginsPaths() const { return m_pluginsPaths; diff --git a/src/PythonRuntimeSettings.h b/src/PythonRuntimeSettings.h index ad75e1a..0f2a9cd 100644 --- a/src/PythonRuntimeSettings.h +++ b/src/PythonRuntimeSettings.h @@ -43,6 +43,7 @@ class PythonRuntimeSettings final : public QDialog void handleEditPluginsPaths(); void handleEnvComboBoxChange(const QString &envTypeName); void handleSelectLocalEnv(); + void handleCreateVenv(); private: Ui_PythonRuntimeSettings *m_ui; diff --git a/src/Ui/CMakeLists.txt b/src/Ui/CMakeLists.txt index 2ac06a2..3fe7e58 100644 --- a/src/Ui/CMakeLists.txt +++ b/src/Ui/CMakeLists.txt @@ -2,12 +2,13 @@ target_sources( ${PROJECT_NAME} PRIVATE # cmake-format: sortable ${CMAKE_CURRENT_LIST_DIR}/AboutDialog.ui + ${CMAKE_CURRENT_LIST_DIR}/CreateVenvForm.ui ${CMAKE_CURRENT_LIST_DIR}/EditorSettings.ui ${CMAKE_CURRENT_LIST_DIR}/InstallDialog.ui ${CMAKE_CURRENT_LIST_DIR}/PackageManager.ui ${CMAKE_CURRENT_LIST_DIR}/PathVariableEditor.ui ${CMAKE_CURRENT_LIST_DIR}/PythonEditor.ui - ${CMAKE_CURRENT_LIST_DIR}/PythonRuntimeSettings.ui + ${CMAKE_CURRENT_LIST_DIR}/PythonRuntimeSettings.ui ${CMAKE_CURRENT_LIST_DIR}/PythonREPL.ui ) diff --git a/src/Ui/CreateVenvForm.ui b/src/Ui/CreateVenvForm.ui new file mode 100644 index 0000000..8e493d9 --- /dev/null +++ b/src/Ui/CreateVenvForm.ui @@ -0,0 +1,101 @@ + + + CreateVenvForm + + + + 0 + 0 + 437 + 151 + + + + Dialog + + + + + + + + + + + + 10 + 0 + + + + ... + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + Name + + + + + + + Location + + + + + + + + + + + + buttonBox + accepted() + CreateVenvForm + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CreateVenvForm + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Ui/PythonRuntimeSettings.ui b/src/Ui/PythonRuntimeSettings.ui index 314fb7f..f8facae 100644 --- a/src/Ui/PythonRuntimeSettings.ui +++ b/src/Ui/PythonRuntimeSettings.ui @@ -100,7 +100,7 @@ - + When started from the command line, the runtime will try to pick up and use any current python virtual environment, this however may not work. When checked, any virtual env when launched from the command line will be ignored. @@ -110,6 +110,13 @@ + + + + Create virtual environment + + +