Skip to content

Commit cfb9435

Browse files
[python:1.10] Improve UX while installing dependencies
* dont detach pip * track success * report useful, nonredundant error to pluginmanager * show users the pip output while installing
1 parent ddd86b5 commit cfb9435

File tree

5 files changed

+106
-54
lines changed

5 files changed

+106
-54
lines changed

python/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
project(python VERSION 1.9)
1+
project(python VERSION 1.10)
22
albert_plugin(
33
NAME "Python"
44
DESCRIPTION "Load Python plugins"

python/src/plugin.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,3 @@ QWidget *Plugin::buildConfigWidget()
123123

124124
return w;
125125
}
126-
127-
void Plugin::installPackages(const QStringList &package_names) const
128-
{
129-
auto script = QString(R"R(python3 -m pip install --disable-pip-version-check --target "%1" %2; cd "%1")R")
130-
.arg(dataDir()->filePath("site-packages"), package_names.join(" "));
131-
runTerminal(script);
132-
}

python/src/plugin.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ class Plugin : public albert::plugin::ExtensionPlugin<albert::PluginProvider>
1818
bool watchSources() const;
1919
void setWatchSources(bool);
2020

21-
void installPackages(const QStringList&) const;
22-
2321
protected:
2422
QWidget* buildConfigWidget() override;
2523
std::vector<albert::PluginLoader*> plugins() override;

python/src/pypluginloader.cpp

Lines changed: 103 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
#include "plugin.h"
55
#include "pypluginloader.h"
66
#include <QDir>
7+
#include <QEventLoop>
78
#include <QFileInfo>
9+
#include <QFontDatabase>
810
#include <QMessageBox>
11+
#include <QProcess>
912
#include <QRegularExpression>
1013
#include <QStandardPaths>
14+
#include <QTextEdit>
1115
using namespace albert;
1216
using namespace std;
1317
namespace py = pybind11;
@@ -200,55 +204,110 @@ const albert::PluginMetaData &PyPluginLoader::metaData() const { return metadata
200204

201205
albert::PluginInstance *PyPluginLoader::instance() const { return cpp_plugin_instance_; }
202206

203-
QString PyPluginLoader::load()
207+
QString PyPluginLoader::load_()
204208
{
205209
py::gil_scoped_acquire acquire;
210+
211+
// Import as __name__ = albert.package_name
212+
py::module importlib_util = py::module::import("importlib.util");
213+
py::object pyspec = importlib_util.attr("spec_from_file_location")(QString("albert.%1").arg(metadata_.id), source_path_); // Prefix to avoid conflicts
214+
module_ = importlib_util.attr("module_from_spec")(pyspec);
215+
216+
// Set default md_id
217+
if (!py::hasattr(module_, ATTR_MD_ID))
218+
module_.attr("md_id") = metadata_.id;
219+
220+
// Attach logcat functions
221+
// https://bugreports.qt.io/browse/QTBUG-117153
222+
// https://code.qt.io/cgit/pyside/pyside-setup.git/commit/?h=6.5&id=2823763072ce3a2da0210dbc014c6ad3195fbeff
223+
py::setattr(module_,"debug", py::cpp_function([this](const QString &s){ qCDebug((*logging_category)) << s; }));
224+
py::setattr(module_,"info", py::cpp_function([this](const QString &s){ qCInfo((*logging_category)) << s; }));
225+
py::setattr(module_,"warning", py::cpp_function([this](const QString &s){ qCWarning((*logging_category)) << s; }));
226+
py::setattr(module_,"critical", py::cpp_function([this](const QString &s){ qCCritical((*logging_category)) << s; }));
227+
228+
// Execute module
229+
pyspec.attr("loader").attr("exec_module")(module_);
230+
231+
// Instanciate plugin
232+
py_plugin_instance_ = module_.attr(ATTR_PLUGIN_CLASS)(); // may throw
233+
cpp_plugin_instance_ = py_plugin_instance_.cast<PluginInstance*>(); // may throw
234+
235+
for (const auto& exec : metadata_.binary_dependencies)
236+
if (QStandardPaths::findExecutable(exec).isNull())
237+
throw runtime_error(QString("No '%1' in $PATH.").arg(exec).toStdString());
238+
239+
return {};
240+
}
241+
242+
QString PyPluginLoader::load()
243+
{
206244
QString err;
207245
try
208246
{
209-
// Import as __name__ = albert.package_name
210-
py::module importlib_util = py::module::import("importlib.util");
211-
py::object pyspec = importlib_util.attr("spec_from_file_location")(QString("albert.%1").arg(metadata_.id), source_path_); // Prefix to avoid conflicts
212-
module_ = importlib_util.attr("module_from_spec")(pyspec);
213-
214-
// Set default md_id
215-
if (!py::hasattr(module_, ATTR_MD_ID))
216-
module_.attr("md_id") = metadata_.id;
217-
218-
// Attach logcat functions
219-
// https://bugreports.qt.io/browse/QTBUG-117153
220-
// https://code.qt.io/cgit/pyside/pyside-setup.git/commit/?h=6.5&id=2823763072ce3a2da0210dbc014c6ad3195fbeff
221-
py::setattr(module_,"debug", py::cpp_function([this](const QString &s){ qCDebug((*logging_category)) << s; }));
222-
py::setattr(module_,"info", py::cpp_function([this](const QString &s){ qCInfo((*logging_category)) << s; }));
223-
py::setattr(module_,"warning", py::cpp_function([this](const QString &s){ qCWarning((*logging_category)) << s; }));
224-
py::setattr(module_,"critical", py::cpp_function([this](const QString &s){ qCCritical((*logging_category)) << s; }));
225-
226-
// Execute module
227-
pyspec.attr("loader").attr("exec_module")(module_);
228-
229-
// Instanciate plugin
230-
py_plugin_instance_ = module_.attr(ATTR_PLUGIN_CLASS)(); // may throw
231-
cpp_plugin_instance_ = py_plugin_instance_.cast<PluginInstance*>(); // may throw
232-
233-
for (const auto& exec : metadata_.binary_dependencies)
234-
if (QStandardPaths::findExecutable(exec).isNull())
235-
throw runtime_error(QString("No '%1' in $PATH.").arg(exec).toStdString());
236-
237-
return {};
238-
239-
} catch (py::error_already_set &e) {
240-
if (e.matches(PyExc_ModuleNotFoundError)) {
241-
auto text = QString::fromStdString(e.what()).section("\n",0,0);
242-
text.append(QString("\n\nTry installing missing dependencies (%1) into albert site-packages?\n\n"
243-
"Note that you have to reload the plugin afterwards.").arg(metadata_.runtime_dependencies.join(", ")));
244-
auto b = QMessageBox::warning(nullptr, "Module not found", text,
245-
QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes);
246-
if (b==QMessageBox::Yes)
247-
provider_.installPackages(metadata_.runtime_dependencies);
248-
249-
err = text;
250-
} else
251-
err = e.what();
247+
try {
248+
return load_();
249+
} catch (py::error_already_set &e) {
250+
251+
// Catch only import errors, rethrow anything else
252+
if (e.matches(PyExc_ModuleNotFoundError)) {
253+
254+
// ask user if dependencies should be installed
255+
QMessageBox mb;
256+
mb.setIcon(QMessageBox::Information);
257+
mb.setWindowTitle("Module not found");
258+
mb.setText("Some modules were not found. Probably this plugin has mandatory "
259+
"dependencies which are not installed on this system.\n\n"
260+
"Install dependencies into the Albert virtual environment?");
261+
mb.setStandardButtons(QMessageBox::Yes|QMessageBox::No);
262+
mb.setDefaultButton(QMessageBox::Yes);
263+
mb.setDetailedText(e.what());
264+
if (mb.exec() == QMessageBox::Yes){
265+
266+
// install dependencies
267+
268+
QProcess proc;
269+
proc.start(
270+
"python3",
271+
{
272+
"-m",
273+
"pip",
274+
"install",
275+
"--disable-pip-version-check",
276+
"--target",
277+
provider_.dataDir()->filePath("site-packages"),
278+
metadata_.runtime_dependencies.join(" ")
279+
}
280+
);
281+
282+
auto *te = new QTextEdit;
283+
te->setCurrentFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
284+
te->setReadOnly(true);
285+
te->setWindowTitle(QString("Installing '%1' dependencies").arg(metadata_.name));
286+
te->resize(600, 480);
287+
te->setAttribute(Qt::WA_DeleteOnClose);
288+
te->show();
289+
290+
QObject::connect(&proc, &QProcess::readyReadStandardOutput, te,
291+
[te, &proc](){ te->append(QString::fromUtf8(proc.readAllStandardOutput())); });
292+
QObject::connect(&proc, &QProcess::readyReadStandardError, te,
293+
[te, &proc](){ te->append(QString::fromUtf8(proc.readAllStandardError())); });
294+
295+
QEventLoop loop;
296+
QObject::connect(&proc, &QProcess::finished, &loop, &QEventLoop::quit);
297+
loop.exec();
298+
299+
if (proc.exitStatus() == QProcess::ExitStatus::NormalExit && proc.exitCode() == EXIT_SUCCESS){
300+
301+
te->deleteLater(); // auto-close on success only
302+
return load_(); // On success try again
303+
304+
} else
305+
err = "Installing dependencies failed.";
306+
} else
307+
err = "Dependencies are missing.";
308+
} else
309+
throw e;
310+
}
252311
} catch(const std::exception &e) {
253312
err = e.what();
254313
} catch(...) {

python/src/pypluginloader.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class PyPluginLoader : public albert::PluginLoader
3131
const QString &source_path() const;
3232

3333
private:
34+
QString load_();
35+
3436
QString source_path_;
3537
pybind11::module module_;
3638
Plugin &provider_;

0 commit comments

Comments
 (0)