Python: add PySide installation check on document open

Checks if the document imports PySide and whether the selected python
interpreter can find a PySide installation. If not show a global info
bar that can install PySide via pip like the python lsp server.

Task-number: PYSIDE-1742
Change-Id: I02c0d5f6eb268f3d8826d4fb9d9ec3c7c48b8638
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
David Schulz
2022-03-16 09:35:02 +01:00
parent 9f3941cda3
commit 7cb3a726d4
9 changed files with 272 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ add_qtc_plugin(Python
PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor
SOURCES SOURCES
pipsupport.cpp pipsupport.h pipsupport.cpp pipsupport.h
pyside.cpp pyside.h
python.qrc python.qrc
pythonconstants.h pythonconstants.h
pythoneditor.cpp pythoneditor.h pythoneditor.cpp pythoneditor.h

View File

@@ -0,0 +1,186 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "pyside.h"
#include "pythonplugin.h"
#include "pythonproject.h"
#include "pythonrunconfiguration.h"
#include "pythonsettings.h"
#include "pythonutils.h"
#include <coreplugin/icore.h>
#include <projectexplorer/target.h>
#include <texteditor/textdocument.h>
#include <utils/algorithm.h>
#include <utils/infobar.h>
#include <utils/runextensions.h>
#include <QRegularExpression>
#include <QTextCursor>
using namespace Utils;
namespace Python {
namespace Internal {
static constexpr char installPySideInfoBarId[] = "Python::InstallPySide";
PySideInstaller *PySideInstaller::instance()
{
static PySideInstaller *instance = new PySideInstaller;
return instance;
}
void PySideInstaller::checkPySideInstallation(const Utils::FilePath &python,
TextEditor::TextDocument *document)
{
document->infoBar()->removeInfo(installPySideInfoBarId);
const QString pySide = importedPySide(document->plainText());
if (pySide == "PySide2" || pySide == "PySide6")
runPySideChecker(python, pySide, document);
}
bool PySideInstaller::missingPySideInstallation(const Utils::FilePath &pythonPath,
const QString &pySide)
{
QTC_ASSERT(!pySide.isEmpty(), return false);
static QMap<FilePath, QSet<QString>> pythonWithPyside;
if (pythonWithPyside[pythonPath].contains(pySide))
return false;
QtcProcess pythonProcess;
const CommandLine importPySideCheck(pythonPath, {"-c", "import " + pySide});
pythonProcess.setCommand(importPySideCheck);
pythonProcess.runBlocking();
const bool missing = pythonProcess.result() != ProcessResult::FinishedWithSuccess;
if (!missing)
pythonWithPyside[pythonPath].insert(pySide);
return missing;
}
QString PySideInstaller::importedPySide(const QString &text)
{
static QRegularExpression importScanner("^\\s*(import|from)\\s+(PySide\\d)",
QRegularExpression::MultilineOption);
const QRegularExpressionMatch match = importScanner.match(text);
return match.captured(2);
}
PySideInstaller::PySideInstaller()
: QObject(PythonPlugin::instance())
{}
void PySideInstaller::installPyside(const Utils::FilePath &python,
const QString &pySide,
TextEditor::TextDocument *document)
{
document->infoBar()->removeInfo(installPySideInfoBarId);
auto install = new PipInstallTask(python);
connect(install, &PipInstallTask::finished, install, &QObject::deleteLater);
install->setPackage(PipPackage(pySide));
install->run();
}
void PySideInstaller::changeInterpreter(const QString &interpreterId,
PythonRunConfiguration *runConfig)
{
if (runConfig)
runConfig->setInterpreter(PythonSettings::interpreter(interpreterId));
}
void PySideInstaller::handlePySideMissing(const FilePath &python,
const QString &pySide,
TextEditor::TextDocument *document)
{
if (!document || !document->infoBar()->canInfoBeAdded(installPySideInfoBarId))
return;
const QString message = tr("%1 installation missing for %2 (%3)")
.arg(pySide, pythonName(python), python.toUserOutput());
InfoBarEntry info(installPySideInfoBarId, message, InfoBarEntry::GlobalSuppression::Enabled);
auto installCallback = [=]() { installPyside(python, pySide, document); };
const QString installTooltip = tr("Install %1 for %2 using pip package installer.")
.arg(pySide, python.toUserOutput());
info.addCustomButton(tr("Install"), installCallback, installTooltip);
if (PythonProject *project = pythonProjectForFile(document->filePath())) {
if (ProjectExplorer::Target *target = project->activeTarget()) {
if (auto runConfiguration = qobject_cast<PythonRunConfiguration *>(
target->activeRunConfiguration())) {
const QList<InfoBarEntry::ComboInfo> interpreters = Utils::transform(
PythonSettings::interpreters(), [](const Interpreter &interpreter) {
return InfoBarEntry::ComboInfo{interpreter.name, interpreter.id};
});
auto interpreterChangeCallback =
[=, rc = QPointer<PythonRunConfiguration>(runConfiguration)](
const InfoBarEntry::ComboInfo &info) {
changeInterpreter(info.data.toString(), rc);
};
const auto isCurrentInterpreter
= Utils::equal(&InfoBarEntry::ComboInfo::data,
QVariant(runConfiguration->interpreter().id));
const QString switchTooltip = tr("Switch the Python interpreter for %1")
.arg(runConfiguration->displayName());
info.setComboInfo(interpreters,
interpreterChangeCallback,
switchTooltip,
Utils::indexOf(interpreters, isCurrentInterpreter));
}
}
}
document->infoBar()->addInfo(info);
}
void PySideInstaller::runPySideChecker(const Utils::FilePath &python,
const QString &pySide,
TextEditor::TextDocument *document)
{
using CheckPySideWatcher = QFutureWatcher<bool>;
QPointer<CheckPySideWatcher> watcher = new CheckPySideWatcher();
// cancel and delete watcher after a 10 second timeout
QTimer::singleShot(10000, this, [watcher]() {
if (watcher) {
watcher->cancel();
watcher->deleteLater();
}
});
connect(watcher,
&CheckPySideWatcher::resultReadyAt,
this,
[=, document = QPointer<TextEditor::TextDocument>(document)]() {
if (watcher->result())
handlePySideMissing(python, pySide, document);
watcher->deleteLater();
});
watcher->setFuture(
Utils::runAsync(&missingPySideInstallation, python, pySide));
}
} // namespace Internal
} // namespace Python

View File

@@ -0,0 +1,67 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include "pipsupport.h"
#include <utils/filepath.h>
#include <QTextDocument>
namespace TextEditor { class TextDocument; }
namespace Python {
namespace Internal {
class PythonRunConfiguration;
class PySideInstaller : public QObject
{
Q_DECLARE_TR_FUNCTIONS(Python::Internal::PySideInstaller)
public:
static PySideInstaller *instance();
void checkPySideInstallation(const Utils::FilePath &python, TextEditor::TextDocument *document);
private:
PySideInstaller();
void installPyside(const Utils::FilePath &python,
const QString &pySide, TextEditor::TextDocument *document);
void changeInterpreter(const QString &interpreterId, PythonRunConfiguration *runConfig);
void handlePySideMissing(const Utils::FilePath &python,
const QString &pySide,
TextEditor::TextDocument *document);
void runPySideChecker(const Utils::FilePath &python,
const QString &pySide,
TextEditor::TextDocument *document);
static bool missingPySideInstallation(const Utils::FilePath &python, const QString &pySide);
static QString importedPySide(const QString &text);
QHash<Utils::FilePath, QList<TextEditor::TextDocument *>> m_infoBarEntries;
};
} // namespace Internal
} // namespace Python

View File

@@ -19,6 +19,8 @@ QtcPlugin {
files: [ files: [
"pipsupport.cpp", "pipsupport.cpp",
"pipsupport.h", "pipsupport.h",
"pyside.cpp",
"pyside.h",
"python.qrc", "python.qrc",
"pythonconstants.h", "pythonconstants.h",
"pythoneditor.cpp", "pythoneditor.cpp",

View File

@@ -25,6 +25,7 @@
#include "pythoneditor.h" #include "pythoneditor.h"
#include "pyside.h"
#include "pythonconstants.h" #include "pythonconstants.h"
#include "pythonhighlighter.h" #include "pythonhighlighter.h"
#include "pythonindenter.h" #include "pythonindenter.h"
@@ -118,6 +119,7 @@ public:
return; return;
PyLSConfigureAssistant::instance()->openDocumentWithPython(python, this); PyLSConfigureAssistant::instance()->openDocumentWithPython(python, this);
PySideInstaller::instance()->checkPySideInstallation(python, this);
} }
}; };

View File

@@ -25,6 +25,7 @@
#include "pythonrunconfiguration.h" #include "pythonrunconfiguration.h"
#include "pyside.h"
#include "pythonconstants.h" #include "pythonconstants.h"
#include "pythonlanguageclient.h" #include "pythonlanguageclient.h"
#include "pythonproject.h" #include "pythonproject.h"
@@ -244,7 +245,7 @@ PythonRunConfiguration::PythonRunConfiguration(Target *target, Utils::Id id)
auto interpreterAspect = addAspect<InterpreterAspect>(); auto interpreterAspect = addAspect<InterpreterAspect>();
interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter"); interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter");
connect(interpreterAspect, &InterpreterAspect::changed, connect(interpreterAspect, &InterpreterAspect::changed,
this, &PythonRunConfiguration::updateLanguageServer); this, &PythonRunConfiguration::interpreterChanged);
connect(PythonSettings::instance(), &PythonSettings::interpretersChanged, connect(PythonSettings::instance(), &PythonSettings::interpretersChanged,
interpreterAspect, &InterpreterAspect::updateInterpreters); interpreterAspect, &InterpreterAspect::updateInterpreters);
@@ -292,7 +293,7 @@ PythonRunConfiguration::PythonRunConfiguration(Target *target, Utils::Id id)
connect(target, &Target::buildSystemUpdated, this, &RunConfiguration::update); connect(target, &Target::buildSystemUpdated, this, &RunConfiguration::update);
} }
void PythonRunConfiguration::updateLanguageServer() void PythonRunConfiguration::interpreterChanged()
{ {
using namespace LanguageClient; using namespace LanguageClient;
@@ -300,8 +301,10 @@ void PythonRunConfiguration::updateLanguageServer()
for (FilePath &file : project()->files(Project::AllFiles)) { for (FilePath &file : project()->files(Project::AllFiles)) {
if (auto document = TextEditor::TextDocument::textDocumentForFilePath(file)) { if (auto document = TextEditor::TextDocument::textDocumentForFilePath(file)) {
if (document->mimeType() == Constants::C_PY_MIMETYPE) if (document->mimeType() == Constants::C_PY_MIMETYPE) {
PyLSConfigureAssistant::instance()->openDocumentWithPython(python, document); PyLSConfigureAssistant::instance()->openDocumentWithPython(python, document);
PySideInstaller::instance()->checkPySideInstallation(python, document);
}
} }
} }
} }

View File

@@ -45,7 +45,7 @@ public:
QString interpreter() const; QString interpreter() const;
private: private:
void updateLanguageServer(); void interpreterChanged();
bool supportsDebugger() const; bool supportsDebugger() const;
QString mainScript() const; QString mainScript() const;

View File

@@ -549,6 +549,12 @@ Interpreter PythonSettings::defaultInterpreter()
return interpreterOptionsPage().defaultInterpreter(); return interpreterOptionsPage().defaultInterpreter();
} }
Interpreter PythonSettings::interpreter(const QString &interpreterId)
{
const QList<Interpreter> interpreters = PythonSettings::interpreters();
return Utils::findOrDefault(interpreters, Utils::equal(&Interpreter::id, interpreterId));
}
} // namespace Internal } // namespace Internal
} // namespace Python } // namespace Python

View File

@@ -62,6 +62,7 @@ public:
static QList<Interpreter> interpreters(); static QList<Interpreter> interpreters();
static Interpreter defaultInterpreter(); static Interpreter defaultInterpreter();
static Interpreter interpreter(const QString &interpreterId);
static void setInterpreter(const QList<Interpreter> &interpreters, const QString &defaultId); static void setInterpreter(const QList<Interpreter> &interpreters, const QString &defaultId);
static void addInterpreter(const Interpreter &interpreter, bool isDefault = false); static void addInterpreter(const Interpreter &interpreter, bool isDefault = false);
static PythonSettings *instance(); static PythonSettings *instance();