47#include <QRegularExpression>
51#include <QElapsedTimer>
94 m_lineCb = std::move(cb);
101 m_progressCb = std::move(cb);
107 const QStringList& args)
109 QStringList fullArgs;
110 if (m_config.unbuffered) {
111 fullArgs << QStringLiteral(
"-u");
113 fullArgs << scriptPath << args;
115 return execute(fullArgs);
121 const QStringList& args)
123 QStringList fullArgs;
124 if (m_config.unbuffered) {
125 fullArgs << QStringLiteral(
"-u");
127 fullArgs << QStringLiteral(
"-c") << code;
130 return execute(fullArgs);
138 proc.start(m_config.pythonExe, {QStringLiteral(
"--version")});
139 return proc.waitForFinished(5000) &&
140 proc.exitStatus() == QProcess::NormalExit &&
141 proc.exitCode() == 0;
149 proc.start(m_config.pythonExe, {QStringLiteral(
"--version")});
150 if (!proc.waitForFinished(5000) || proc.exitCode() != 0) {
154 QString out = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
155 if (out.startsWith(QStringLiteral(
"Python "))) {
166 static const QRegularExpression validPkg(QStringLiteral(
"^[A-Za-z_][A-Za-z0-9_.]*$"));
167 if (!validPkg.match(packageName).hasMatch()) {
168 qWarning() <<
"[PythonRunner] Invalid package name:" << packageName;
173 proc.start(m_config.pythonExe,
174 {QStringLiteral(
"-c"),
175 QStringLiteral(
"import %1").arg(packageName)});
176 return proc.waitForFinished(10000) &&
177 proc.exitStatus() == QProcess::NormalExit &&
178 proc.exitCode() == 0;
185 if (m_config.venvDir.isEmpty()) {
189 return QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"Scripts/python.exe"));
191 return QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"bin/python"));
201 if (m_config.venvDir.isEmpty()) {
207 bool venvExists = QFileInfo::exists(venvPython);
211 qDebug() <<
"[PythonRunner] Creating virtual environment at:" << m_config.venvDir;
214 venvProc.start(m_config.pythonExe,
215 {QStringLiteral(
"-m"), QStringLiteral(
"venv"), m_config.venvDir});
217 if (!venvProc.waitForStarted(10000)) {
218 result.
stdErr = QStringLiteral(
"Failed to start venv creation: ") + venvProc.errorString();
219 qWarning() <<
"[PythonRunner]" << result.
stdErr;
223 if (!venvProc.waitForFinished(120000)) {
224 result.
stdErr = QStringLiteral(
"Venv creation timed out.");
226 venvProc.waitForFinished(5000);
227 qWarning() <<
"[PythonRunner]" << result.
stdErr;
231 if (venvProc.exitCode() != 0) {
232 result.
exitCode = venvProc.exitCode();
233 result.
stdErr = QString::fromUtf8(venvProc.readAllStandardError());
234 qWarning() <<
"[PythonRunner] venv creation failed:" << result.
stdErr;
238 qDebug() <<
"[PythonRunner] Virtual environment created.";
240 qDebug() <<
"[PythonRunner] Virtual environment already exists at:" << m_config.venvDir;
245 bool needsInstall =
false;
249 QString pipExe = QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"Scripts/pip.exe"));
251 QString pipExe = QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"bin/pip"));
254 if (!m_config.packageDir.isEmpty()) {
256 QString tomlPath = QDir(m_config.packageDir).absoluteFilePath(QStringLiteral(
"pyproject.toml"));
257 if (QFileInfo::exists(tomlPath)) {
258 pipArgs << QStringLiteral(
"install") << m_config.packageDir;
260 qDebug() <<
"[PythonRunner] Installing from pyproject.toml in:" << m_config.packageDir;
262 qWarning() <<
"[PythonRunner] packageDir set but no pyproject.toml found at:" << tomlPath;
266 if (!needsInstall && !m_config.requirementsFile.isEmpty()) {
267 if (QFileInfo::exists(m_config.requirementsFile)) {
268 pipArgs << QStringLiteral(
"install")
269 << QStringLiteral(
"-r") << m_config.requirementsFile;
271 qDebug() <<
"[PythonRunner] Installing from requirements.txt:" << m_config.requirementsFile;
273 qWarning() <<
"[PythonRunner] requirementsFile not found:" << m_config.requirementsFile;
279 pipProc.start(pipExe, pipArgs);
281 if (!pipProc.waitForStarted(10000)) {
282 result.
stdErr = QStringLiteral(
"Failed to start pip: ") + pipProc.errorString();
283 qWarning() <<
"[PythonRunner]" << result.
stdErr;
288 if (!pipProc.waitForFinished(1800000)) {
289 result.
stdErr = QStringLiteral(
"pip install timed out.");
292 pipProc.waitForFinished(5000);
293 qWarning() <<
"[PythonRunner]" << result.
stdErr;
297 result.
stdOut = QString::fromUtf8(pipProc.readAllStandardOutput());
298 result.
stdErr = QString::fromUtf8(pipProc.readAllStandardError());
299 result.
exitCode = pipProc.exitCode();
301 if (pipProc.exitCode() != 0) {
302 qWarning() <<
"[PythonRunner] pip install failed with code" << pipProc.exitCode();
303 qWarning() <<
"[PythonRunner] stderr:" << result.
stdErr;
307 qDebug() <<
"[PythonRunner] Dependencies installed successfully.";
311 m_config.pythonExe = venvPython;
315 qDebug() <<
"[PythonRunner] Using venv Python:" << m_config.pythonExe;
322 const QStringList& args)
331 return run(scriptPath, args);
341 process.setProcessChannelMode(QProcess::SeparateChannels);
345 process.setWorkingDirectory(m_config.
workingDir);
350 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
351 for (
const QString& kv : m_config.
extraEnv) {
352 int idx = kv.indexOf(QLatin1Char(
'='));
354 env.insert(kv.left(idx), kv.mid(idx + 1));
357 process.setProcessEnvironment(env);
360 qDebug() <<
"[PythonRunner] Launching:" << m_config.pythonExe << fullArgs.join(QLatin1Char(
' '));
362 process.start(m_config.pythonExe, fullArgs);
364 if (!process.waitForStarted(10000)) {
365 result.
stdErr = QStringLiteral(
"Failed to start Python process: ") + process.errorString();
366 qWarning() <<
"[PythonRunner]" << result.
stdErr;
371 QByteArray stdOutBuf, stdErrBuf;
372 QStringList stdOutLines, stdErrLines;
374 auto processLines = [&](QByteArray& buffer,
int channel) {
375 while (buffer.contains(
'\n')) {
376 int idx = buffer.indexOf(
'\n');
377 QString line = QString::fromUtf8(buffer.left(idx));
378 buffer.remove(0, idx + 1);
381 if (channel == 0) stdOutLines << line;
382 else stdErrLines << line;
386 m_lineCb(channel, line);
393 if (parseProgressLine(line, pct, msg)) {
396 m_progressCb(pct, msg);
403 qDebug() <<
"[Python stdout]" << line;
405 qWarning() <<
"[Python stderr]" << line;
411 QElapsedTimer elapsed;
414 while (!process.waitForFinished(100)) {
415 stdOutBuf.append(process.readAllStandardOutput());
416 stdErrBuf.append(process.readAllStandardError());
417 processLines(stdOutBuf, 0);
418 processLines(stdErrBuf, 1);
421 if (m_config.timeoutMsec > 0 && elapsed.elapsed() >= m_config.timeoutMsec) {
423 stdOutBuf.append(process.readAllStandardOutput());
424 stdErrBuf.append(process.readAllStandardError());
425 processLines(stdOutBuf, 0);
426 processLines(stdErrBuf, 1);
429 result.
stdErr += QStringLiteral(
"\nProcess timed out after %1 ms.").arg(m_config.timeoutMsec);
431 process.waitForFinished(5000);
432 qWarning() <<
"[PythonRunner] Process timed out after" << m_config.timeoutMsec <<
"ms.";
438 stdOutBuf.append(process.readAllStandardOutput());
439 stdErrBuf.append(process.readAllStandardError());
441 processLines(stdOutBuf, 0);
442 processLines(stdErrBuf, 1);
444 if (!stdOutBuf.isEmpty()) {
445 QString line = QString::fromUtf8(stdOutBuf);
447 if (m_lineCb) m_lineCb(0, line);
450 if (!stdErrBuf.isEmpty()) {
451 QString line = QString::fromUtf8(stdErrBuf);
453 if (m_lineCb) m_lineCb(1, line);
458 result.
stdOut = stdOutLines.join(QLatin1Char(
'\n'));
459 result.
stdErr += stdErrLines.join(QLatin1Char(
'\n'));
461 result.
exitCode = process.exitCode();
463 process.exitStatus() == QProcess::NormalExit &&
467 qDebug() <<
"[PythonRunner] Script finished successfully.";
469 qWarning() <<
"[PythonRunner] Script exited with code" << result.
exitCode;
479bool PythonRunner::parseProgressLine(
const QString& line,
484 static const QRegularExpression re(
485 QStringLiteral(R
"(^\[progress\]\s+(\d+(?:\.\d+)?)\s*%\s*(.*)?$)"),
486 QRegularExpression::CaseInsensitiveOption);
488 QRegularExpressionMatch match = re.match(line.trimmed());
489 if (!match.hasMatch()) {
494 pct = match.captured(1).toFloat(&ok);
498 msg = match.captured(2).trimmed();
PythonRunner class declaration — standardized interface for calling Python scripts.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
std::function< void(float pct, const QString &msg)> PythonProgressCallback
std::function< void(int channel, const QString &line)> PythonLineCallback
Script execution result container.
Script execution configuration.
const PythonRunnerConfig & config() const
void setLineCallback(PythonLineCallback cb)
void setConfig(const PythonRunnerConfig &config)
QString pythonVersion() const
QString venvPythonPath() const
bool isPythonAvailable() const
void progressUpdated(float pct, const QString &msg)
bool isPackageAvailable(const QString &packageName) const
PythonRunnerResult runCode(const QString &code, const QStringList &args={})
PythonRunnerResult ensureVenv()
PythonRunnerResult run(const QString &scriptPath, const QStringList &args={})
void setProgressCallback(PythonProgressCallback cb)
void finished(const UTILSLIB::PythonRunnerResult &result)
PythonRunnerResult runInVenv(const QString &scriptPath, const QStringList &args={})
PythonRunner(QObject *pParent=nullptr)
void lineReceived(int channel, const QString &line)