aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Schulz <[email protected]>2025-06-19 14:58:36 +0200
committerDavid Schulz <[email protected]>2025-06-30 10:26:46 +0000
commit975eaa91eae8f8fc5789a3db82fad36749dd8a4e (patch)
tree85646ec8601424edc3389d5eb31fbf8c6d78dc26
parentc1f917ee490b6e504c31e23da4b95efcf734cae6 (diff)
Python: improve missing pyside handlingHEADmaster
Fixes: QTCREATORBUG-33057 Change-Id: I41fca7728c10fbf512b383cf04c4b3dacdb536ac Reviewed-by: Christian Stenger <[email protected]>
-rw-r--r--src/plugins/projectexplorer/runcontrol.cpp15
-rw-r--r--src/plugins/projectexplorer/runcontrol.h4
-rw-r--r--src/plugins/python/pyside.cpp31
-rw-r--r--src/plugins/python/pyside.h6
-rw-r--r--src/plugins/python/pythonrunconfiguration.cpp139
5 files changed, 139 insertions, 56 deletions
diff --git a/src/plugins/projectexplorer/runcontrol.cpp b/src/plugins/projectexplorer/runcontrol.cpp
index 275360b3885..c006a79b418 100644
--- a/src/plugins/projectexplorer/runcontrol.cpp
+++ b/src/plugins/projectexplorer/runcontrol.cpp
@@ -973,7 +973,7 @@ void RunControlPrivate::showError(const QString &msg)
void RunControl::setupFormatter(OutputFormatter *formatter) const
{
- QList<Utils::OutputLineParser *> parsers = createOutputParsers(target());
+ QList<Utils::OutputLineParser *> parsers = createOutputParsers(buildConfiguration());
if (const auto customParsersAspect = aspectData<CustomParsersAspect>()) {
for (const Id id : std::as_const(customParsersAspect->parsers)) {
if (auto parser = createCustomParserFromId(id))
@@ -1581,13 +1581,13 @@ void RunWorker::addStopDependency(RunWorker *dependency)
// Output parser factories
-static QList<std::function<OutputLineParser *(Target *)>> g_outputParserFactories;
+static QList<std::function<OutputLineParser *(BuildConfiguration *)>> g_outputParserFactories;
-QList<OutputLineParser *> createOutputParsers(Target *target)
+QList<OutputLineParser *> createOutputParsers(BuildConfiguration *bc)
{
QList<OutputLineParser *> formatters;
for (auto factory : std::as_const(g_outputParserFactories)) {
- if (OutputLineParser *parser = factory(target))
+ if (OutputLineParser *parser = factory(bc))
formatters << parser;
}
return formatters;
@@ -1595,6 +1595,13 @@ QList<OutputLineParser *> createOutputParsers(Target *target)
void addOutputParserFactory(const std::function<Utils::OutputLineParser *(Target *)> &factory)
{
+ g_outputParserFactories.append(
+ [factory](BuildConfiguration *bc) { return factory(bc ? bc->target() : nullptr); });
+}
+
+void addOutputParserFactory(
+ const std::function<Utils::OutputLineParser *(BuildConfiguration *)> &factory)
+{
g_outputParserFactories.append(factory);
}
diff --git a/src/plugins/projectexplorer/runcontrol.h b/src/plugins/projectexplorer/runcontrol.h
index fef9204dadc..04e3a9749b6 100644
--- a/src/plugins/projectexplorer/runcontrol.h
+++ b/src/plugins/projectexplorer/runcontrol.h
@@ -264,8 +264,10 @@ public:
PROJECTEXPLORER_EXPORT
void addOutputParserFactory(const std::function<Utils::OutputLineParser *(Target *)> &);
+PROJECTEXPLORER_EXPORT
+void addOutputParserFactory(const std::function<Utils::OutputLineParser *(BuildConfiguration *)> &);
-PROJECTEXPLORER_EXPORT QList<Utils::OutputLineParser *> createOutputParsers(Target *target);
+PROJECTEXPLORER_EXPORT QList<Utils::OutputLineParser *> createOutputParsers(BuildConfiguration *bc);
class PROJECTEXPLORER_EXPORT RunInterface : public QObject
{
diff --git a/src/plugins/python/pyside.cpp b/src/plugins/python/pyside.cpp
index b30b764664c..d338623d14d 100644
--- a/src/plugins/python/pyside.cpp
+++ b/src/plugins/python/pyside.cpp
@@ -22,6 +22,7 @@
#include <utils/algorithm.h>
#include <utils/async.h>
+#include <utils/checkablemessagebox.h>
#include <utils/infobar.h>
#include <utils/mimeconstants.h>
#include <utils/qtcassert.h>
@@ -29,6 +30,7 @@
#include <QBoxLayout>
#include <QComboBox>
+#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QRegularExpression>
#include <QTextCursor>
@@ -84,13 +86,27 @@ QString PySideInstaller::usedPySide(const QString &text, const QString &mimeType
return {};
}
+void PySideInstaller::installPySide(const QUrl &url)
+{
+ FilePath python = FilePath::fromUserInput(QUrl::fromPercentEncoding(url.path().toLatin1()));
+ QTC_ASSERT(python.isExecutableFile(), return);
+ installPySide(python, "PySide6");
+}
+
PySideInstaller::PySideInstaller()
{
+ QDesktopServices::setUrlHandler("pysideinstall", this, "installPySide");
+
connect(Core::EditorManager::instance(), &Core::EditorManager::documentOpened,
this, &PySideInstaller::handleDocumentOpened);
}
-void PySideInstaller::installPyside(const FilePath &python, const QString &pySide)
+PySideInstaller::~PySideInstaller()
+{
+ QDesktopServices::unsetUrlHandler("pysideinstall");
+}
+
+void PySideInstaller::installPySide(const FilePath &python, const QString &pySide, bool quiet)
{
QMap<QVersionNumber, Utils::FilePath> availablePySides;
@@ -132,6 +148,17 @@ void PySideInstaller::installPyside(const FilePath &python, const QString &pySid
emit pySideInstalled(python, pySide);
});
if (availablePySides.isEmpty()) {
+ if (!quiet) {
+ QMessageBox::StandardButton selected = CheckableMessageBox::question(
+ Tr::tr("Missing PySide6 installation"),
+ Tr::tr("Install PySide6 via pip for %1?").arg(python.shortNativePath()),
+ {},
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes,
+ QMessageBox::Yes);
+ if (selected == QMessageBox::No)
+ return;
+ }
install->setPackages({PipPackage(pySide)});
} else {
QDialog dialog;
@@ -198,7 +225,7 @@ void PySideInstaller::handlePySideMissing(const FilePath &python,
const QString message = Tr::tr("%1 installation missing for %2 (%3)")
.arg(pySide, pythonName(python), python.toUserOutput());
InfoBarEntry info(installPySideInfoBarId, message, InfoBarEntry::GlobalSuppression::Enabled);
- auto installCallback = [this, python, pySide] { installPyside(python, pySide); };
+ auto installCallback = [this, python, pySide] { installPySide(python, pySide, true); };
const QString installTooltip = Tr::tr("Install %1 for %2 using pip package installer.")
.arg(pySide, python.toUserOutput());
info.addCustomButton(
diff --git a/src/plugins/python/pyside.h b/src/plugins/python/pyside.h
index b2e1a65343a..f6f8f25f1e3 100644
--- a/src/plugins/python/pyside.h
+++ b/src/plugins/python/pyside.h
@@ -30,14 +30,18 @@ class PySideInstaller : public QObject
public:
void checkPySideInstallation(const Utils::FilePath &python, TextEditor::TextDocument *document);
static PySideInstaller &instance();
+ void installPySide(const Utils::FilePath &python, const QString &pySide, bool quiet = false);
+
+public slots:
+ void installPySide(const QUrl &url);
signals:
void pySideInstalled(const Utils::FilePath &python, const QString &pySide);
private:
PySideInstaller();
+ ~PySideInstaller();
- void installPyside(const Utils::FilePath &python, const QString &pySide);
void handlePySideMissing(const Utils::FilePath &python,
const QString &pySide,
TextEditor::TextDocument *document);
diff --git a/src/plugins/python/pythonrunconfiguration.cpp b/src/plugins/python/pythonrunconfiguration.cpp
index d1dbac54de2..3b7f8742f6e 100644
--- a/src/plugins/python/pythonrunconfiguration.cpp
+++ b/src/plugins/python/pythonrunconfiguration.cpp
@@ -3,6 +3,8 @@
#include "pythonrunconfiguration.h"
+#include "pyside.h"
+#include "pythonbuildconfiguration.h"
#include "pythonconstants.h"
#include "pythonproject.h"
#include "pythontr.h"
@@ -23,6 +25,8 @@
#include <utils/fileutils.h>
#include <utils/outputformatter.h>
+#include <QUrl>
+
using namespace ProjectExplorer;
using namespace Utils;
@@ -34,10 +38,18 @@ static const QRegularExpression &tracebackFilePattern()
return s_filePattern;
}
+static const QRegularExpression &moduleNotFoundPattern()
+{
+ static const QRegularExpression s_functionPattern(
+ "^ModuleNotFoundError: No module named '([_a-zA-Z][_a-zA-Z0-9]*)'$");
+ return s_functionPattern;
+}
+
class PythonOutputLineParser : public OutputLineParser
{
public:
- PythonOutputLineParser()
+ PythonOutputLineParser(const FilePath &python)
+ : m_python(python)
{
TaskHub::clearTasks(PythonErrorTaskCategory);
}
@@ -45,62 +57,87 @@ public:
private:
Result handleLine(const QString &text, OutputFormat format) final
{
- if (!m_inTraceBack) {
- m_inTraceBack = format == StdErrFormat
- && text.startsWith("Traceback (most recent call last):");
- if (m_inTraceBack)
- return Status::InProgress;
- return Status::NotHandled;
- }
-
const Id category(PythonErrorTaskCategory);
- const QRegularExpressionMatch match = tracebackFilePattern().match(text);
- if (match.hasMatch()) {
- const LinkSpec link(match.capturedStart(2), match.capturedLength(2), match.captured(2));
- const auto fileName = FilePath::fromString(match.captured(3));
- const int lineNumber = match.captured(4).toInt();
- m_tasks.append({Task::Warning, QString(), fileName, lineNumber, category});
- return {Status::InProgress, {link}};
- }
- Status status = Status::InProgress;
- if (text.startsWith(' ')) {
- // Neither traceback start, nor file, nor error message line.
- // Not sure if that can actually happen.
- if (m_tasks.isEmpty()) {
- m_tasks.append({Task::Warning, text.trimmed(), {}, -1, category});
+ if (m_inTraceBack) {
+ const QRegularExpressionMatch match = tracebackFilePattern().match(text);
+ if (match.hasMatch()) {
+ const LinkSpec link(match.capturedStart(2), match.capturedLength(2), match.captured(2));
+ const auto fileName = FilePath::fromUserInput(match.captured(3));
+ const int lineNumber = match.captured(4).toInt();
+ m_tasks.append({Task::Warning, QString(), fileName, lineNumber, category});
+ return {Status::InProgress, {link}};
+ }
+
+ if (text.startsWith(' ')) {
+ // Neither traceback start, nor file, nor error message line.
+ // Not sure if that can actually happen.
+ if (m_tasks.isEmpty()) {
+ m_tasks.append({Task::Warning, text.trimmed(), {}, -1, category});
+ } else {
+ Task &task = m_tasks.back();
+ if (!task.summary().isEmpty())
+ task.addToSummary(QChar(' '));
+ task.addToSummary(text.trimmed());
+ }
} else {
- Task &task = m_tasks.back();
- QString t = text.trimmed();
- if (!task.summary().isEmpty())
- t.prepend(' ');
- task.addToSummary(t);
+ // The actual exception. This ends the traceback.
+ Task exception{Task::Error, text, {}, -1, category};
+ const QString detail = Tr::tr("Install %1 (requires pip)");
+ const QString pySide6Text = Tr::tr("PySide6");
+ const QString link = QString("pysideinstall:")
+ + QUrl::toPercentEncoding(m_python.toFSPathString());
+ exception.addToDetails(detail.arg(pySide6Text));
+ QTextCharFormat format;
+ format.setAnchor(true);
+ format.setAnchorHref(link);
+ const int offset = exception.summary().length() + detail.indexOf("%1") + 1;
+ exception.setFormats(
+ {QTextLayout::FormatRange{offset, int(pySide6Text.length()), format}});
+ TaskHub::addTask(exception);
+ for (auto rit = m_tasks.crbegin(), rend = m_tasks.crend(); rit != rend; ++rit)
+ TaskHub::addTask(*rit);
+ m_inTraceBack = false;
+ const QRegularExpressionMatch match = moduleNotFoundPattern().match(text);
+ if (match.hasMatch()) {
+ const QString moduleName = match.captured(1);
+ if (moduleName == "PySide6") {
+ const LinkSpec
+ link(match.capturedStart(1), match.capturedLength(1), moduleName);
+ return {Status::Done, {link}};
+ }
+ }
+ return Status::Done;
}
- } else {
- // The actual exception. This ends the traceback.
- TaskHub::addTask({Task::Error, text, {}, -1, category});
- for (auto rit = m_tasks.crbegin(), rend = m_tasks.crend(); rit != rend; ++rit)
- TaskHub::addTask(*rit);
- m_tasks.clear();
- m_inTraceBack = false;
- status = Status::Done;
+ return Status::InProgress;
+ }
+
+ if (format == StdErrFormat) {
+ m_inTraceBack = text.startsWith("Traceback (most recent call last):");
+ if (m_inTraceBack)
+ return Status::InProgress;
}
- return status;
+
+ return Status::NotHandled;
}
bool handleLink(const QString &href) final
{
- const QRegularExpressionMatch match = tracebackFilePattern().match(href);
- if (!match.hasMatch())
- return false;
- const QString fileName = match.captured(3);
- const int lineNumber = match.captured(4).toInt();
- Core::EditorManager::openEditorAt({FilePath::fromString(fileName), lineNumber});
- return true;
+ if (const QRegularExpressionMatch match = tracebackFilePattern().match(href);
+ match.hasMatch()) {
+ const QString fileName = match.captured(3);
+ const int lineNumber = match.captured(4).toInt();
+ Core::EditorManager::openEditorAt({FilePath::fromString(fileName), lineNumber});
+ return true;
+ }
+ if (href == "PySide6")
+ PySideInstaller::instance().installPySide(m_python, href);
+ return false;
}
QList<Task> m_tasks;
bool m_inTraceBack = false;
+ const FilePath m_python;
};
// RunConfiguration
@@ -195,12 +232,18 @@ void setupPythonDebugWorker()
void setupPythonOutputParser()
{
- addOutputParserFactory([](Target *t) -> OutputLineParser * {
- if (!t)
+ addOutputParserFactory([](BuildConfiguration *bc) -> OutputLineParser * {
+ auto *pythonBuildConfig = qobject_cast<PythonBuildConfiguration *>(bc);
+ if (!pythonBuildConfig)
return nullptr;
+
+ Target *t = pythonBuildConfig->target();
+ QTC_ASSERT(t, return nullptr);
+
if (t->project()->mimeType() == Constants::C_PY_PROJECT_MIME_TYPE
- || t->project()->mimeType() == Constants::C_PY_PROJECT_MIME_TYPE_TOML)
- return new PythonOutputLineParser;
+ || t->project()->mimeType() == Constants::C_PY_PROJECT_MIME_TYPE_TOML) {
+ return new PythonOutputLineParser(pythonBuildConfig->python());
+ }
return nullptr;
});
}