Krita Source Code Documentation
Loading...
Searching...
No Matches
PythonPluginManager.cpp
Go to the documentation of this file.
1/*
2 * This file is part of PyKrita, Krita' Python scripting plugin.
3 *
4 * SPDX-FileCopyrightText: 2013 Alex Turbov <i.zaufi@gmail.com>
5 * SPDX-FileCopyrightText: 2014-2016 Boudewijn Rempt <boud@valdyas.org>
6 * SPDX-FileCopyrightText: 2017 Jouni Pentikäinen (joupent@gmail.com)
7 *
8 * SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10
11#include "PythonPluginManager.h"
12
13#include <QFile>
14#include <QFileInfo>
15#include <KoResourcePaths.h>
16#include <KConfig>
17#include <KDesktopFile>
18#include <KLocalizedString>
19#include <KSharedConfig>
20#include <KConfigGroup>
21
22#include <KisUsageLogger.h>
23
24#include "config.h"
25#include "version_checker.h"
26
27static QString currentLocale()
28{
29 const QStringList languages = KLocalizedString::languages();
30 if (languages.isEmpty()) {
31 return QLocale().name();
32 } else {
33 return languages.first();
34 }
35}
36
38
39// PythonPlugin implementation
40
42{
43 QString filePath = m_moduleName;
44 return filePath.replace(".", "/");
45}
46
48{
49 if (m_manual.isNull() && !m_manualPath.isEmpty()) {
50 QFile f(m_manualPath);
51 if (f.open(QFile::ReadOnly)) {
52 QByteArray ba = f.readAll();
53 f.close();
54 m_manual = QString::fromUtf8(ba);
55 }
56 }
57 return m_manual;
58}
59
61{
62 dbgScript << "Got Krita/PythonPlugin: " << name()
63 << ", module-path=" << moduleName()
64 ;
65 // Make sure mandatory properties are here
66 if (m_name.isEmpty()) {
67 dbgScript << "Ignore desktop file w/o a name";
68 return false;
69 }
70 if (m_moduleName.isEmpty()) {
71 dbgScript << "Ignore desktop file w/o a module to import";
72 return false;
73 }
74
75 return true;
76}
77
79{
80 if (isBroken()) {
81 return i18nc("@item:intable plugin status", "Broken");
82 } else if (isUnstable()) {
83 return i18nc("@item:intable plugin status", "Unstable");
84 } else if (isEnabled()) {
85 return i18nc("@item:intable plugin status", "Loaded");
86 } else {
87 return i18nc("@item:intable plugin status", "Not loaded");
88 }
89}
90
91// PythonPluginManager implementation
92
94 : QObject(0)
95 , m_model(0, this)
96{}
97
102
104 if (index >= 0 && index < m_plugins.count()) {
105 return &m_plugins[index];
106 }
107
108 return nullptr;
109}
110
115
117{
118 Q_FOREACH(PythonPlugin plugin, m_plugins) {
119 if (plugin.m_loaded) {
121 }
122 }
123}
124
126{
127 // Find the module:
128 // 0) try to locate directory based plugin first
129 QString rel_path = plugin.moduleFilePathPart();
130 rel_path = rel_path + "/" + "__init__.py";
131 dbgScript << "Finding Python module with rel_path:" << rel_path;
132
133 QString module_path = KoResourcePaths::findAsset("pythonscripts", rel_path);
134
135 dbgScript << "module_path:" << module_path;
136
137 if (module_path.isEmpty()) {
138 // 1) Nothing found, then try file based plugin
139 rel_path = plugin.moduleFilePathPart() + ".py";
140 dbgScript << "Finding Python module with rel_path:" << rel_path;
141 module_path = KoResourcePaths::findAsset("pythonscripts", rel_path);
142 dbgScript << "module_path:" << module_path;
143 }
144
145 // Is anything found at all?
146 if (module_path.isEmpty()) {
147 plugin.m_broken = true;
148 plugin.m_errorReason = i18nc(
149 "@info:tooltip"
150 , "Unable to find the module specified <application>%1</application>"
152 );
153 dbgScript << "Cannot load module:" << plugin.m_errorReason;
154 return false;
155 }
156 dbgScript << "Found module path:" << module_path;
157 return true;
158}
159
160QPair<QString, PyKrita::version_checker> PythonPluginManager::parseDependency(const QString& d)
161{
162 // Check if dependency has package info attached
163 const int pnfo = d.indexOf('(');
164 if (pnfo != -1) {
165 QString dependency = d.mid(0, pnfo);
166 QString version_str = d.mid(pnfo + 1, d.size() - pnfo - 2).trimmed();
167 dbgScript << "Desired version spec [" << dependency << "]:" << version_str;
169 if (!(checker.isValid() && d.endsWith(')'))) {
170 dbgScript << "Invalid version spec " << d;
171 QString reason = i18nc(
172 "@info:tooltip"
173 , "<p>Specified version has invalid format for dependency <application>%1</application>: "
174 "<icode>%2</icode>. Skipped</p>"
175 , dependency
176 , version_str
177 );
178 return qMakePair(reason, PyKrita::version_checker());
179 }
180 return qMakePair(dependency, checker);
181 }
183}
184
196{
197 QStringList dependencies = plugin.property("X-Python-Dependencies").toStringList();
198
200 QString reason = i18nc("@info:tooltip", "<title>Dependency check</title>");
201 Q_FOREACH(const QString & d, dependencies) {
202 QPair<QString, PyKrita::version_checker> info_pair = parseDependency(d);
203 PyKrita::version_checker& checker = info_pair.second;
204 if (!checker.isValid()) {
205 plugin.m_broken = true;
206 reason += info_pair.first;
207 continue;
208 }
209
210 dbgScript << "Try to import dependency module/package:" << d;
211
212 // Try to import a module
213 const QString& dependency = info_pair.first;
214 PyObject* module = py.moduleImport(PQ(dependency));
215 if (module) {
216 if (checker.isEmpty()) { // Need to check smth?
217 dbgScript << "No version to check, just make sure it's loaded:" << dependency;
218 Py_DECREF(module);
219 continue;
220 }
221 // Try to get __version__ from module
222 // See PEP396: https://www.python.org/dev/peps/pep-0396/
223 PyObject* version_obj = py.itemString("__version__", PQ(dependency));
224 if (!version_obj) {
225 dbgScript << "No __version__ for " << dependency
226 << "[" << plugin.name() << "]:\n" << py.lastTraceback()
227 ;
228 plugin.m_unstable = true;
229 reason += i18nc(
230 "@info:tooltip"
231 , "<p>Failed to check version of dependency <application>%1</application>: "
232 "Module do not have PEP396 <code>__version__</code> attribute. "
233 "It is not disabled, but behaviour is unpredictable...</p>"
234 , dependency
235 );
236 }
237 PyKrita::version dep_version = PyKrita::version::fromPythonObject(version_obj);
238
239 if (!dep_version.isValid()) {
240 // Dunno what is this... Giving up!
241 dbgScript << "***: Can't parse module version for" << dependency;
242 plugin.m_unstable = true;
243 reason += i18nc(
244 "@info:tooltip"
245 , "<p><application>%1</application>: Unexpected module's version format"
246 , dependency
247 );
248 } else if (!checker(dep_version)) {
249 dbgScript << "Version requirement check failed ["
250 << plugin.name() << "] for "
251 << dependency << ": wanted " << checker.operationToString()
252 << QString(checker.required())
253 << ", but found" << QString(dep_version)
254 ;
255 plugin.m_broken = true;
256 reason += i18nc(
257 "@info:tooltip"
258 , "<p><application>%1</application>: No suitable version found. "
259 "Required version %2 %3, but found %4</p>"
260 , dependency
261 , checker.operationToString()
262 , QString(checker.required())
263 , QString(dep_version)
264 );
265 }
266 // Do not need this module anymore...
267 Py_DECREF(module);
268 } else {
269 dbgScript << "Load failure [" << plugin.name() << "]:\n" << py.lastTraceback();
270 plugin.m_broken = true;
271 reason += i18nc(
272 "@info:tooltip"
273 , "<p>Failure on module load <application>%1</application>:</p><pre>%2</pre>"
274 , dependency
275 , py.lastTraceback()
276 );
277 }
278 }
279
280 if (plugin.isBroken() || plugin.isUnstable()) {
281 plugin.m_errorReason = reason;
282 }
283}
284
286{
287 m_plugins.clear();
288
289 KConfigGroup pluginSettings(KSharedConfig::openConfig(), "python");
290
291 QStringList desktopFiles = KoResourcePaths::findAllAssets("data", "pykrita/*desktop");
292
293 Q_FOREACH(const QString &desktopFile, desktopFiles) {
294
295 KDesktopFile df(desktopFile);
296 df.setLocale(currentLocale());
297 const KConfigGroup dg = df.desktopGroup();
298 if (dg.readEntry("ServiceTypes") == "Krita/PythonPlugin") {
300 plugin.m_comment = df.readComment();
301 plugin.m_name = df.readName();
302 plugin.m_moduleName = dg.readEntry("X-KDE-Library");
303 plugin.m_desktopFilePath = QFileInfo(desktopFile).filePath();
304
305 QString manual = dg.readEntry("X-Krita-Manual");
306 if (!manual.isEmpty()) {
307 plugin.m_manualPath = QFileInfo(desktopFile).path() + "/" + plugin.m_moduleName + "/" + manual;
308 }
309 if (!plugin.isValid()) {
310 dbgScript << plugin.name() << "is not usable";
311 continue;
312 }
313
315 dbgScript << "Cannot load" << plugin.name() << ": broken"
316 << plugin.isBroken()
317 << "because:" << plugin.errorReason();
318 continue;
319 }
320
322
323 plugin.m_enabled = pluginSettings.readEntry(QString("enable_") + plugin.moduleName(), false);
324
325 m_plugins.append(plugin);
326 }
327 }
328}
329
331{
332 KisUsageLogger::writeSysInfo("Loaded Python Plugins");
333 for (PythonPlugin &plugin : m_plugins) {
334 dbgScript << "Trying to load plugin" << plugin.moduleName()
335 << ". Enabled:" << plugin.isEnabled()
336 << ". Broken: " << plugin.isBroken();
337
338 if (plugin.m_enabled && !plugin.isBroken()) {
340 }
341 }
343}
344
346{
348
349 // We can't know if a plugin is still broken until we try to load it
350 plugin.m_broken = false;
351 plugin.m_errorReason.clear();
352
353 QString module_name = plugin.moduleName();
354 KisUsageLogger::writeSysInfo("\t" + module_name);
355 dbgScript << "Loading module: " << module_name;
356
358
359 // Get 'plugins' key from 'pykrita' module dictionary.
360 // Every entry has a module name as a key and 2 elements tuple as a value
361 PyObject* plugins = py.itemString("plugins");
363
364 PyObject* module = py.moduleImport(PQ(module_name));
365 if (module) {
366 // Move just loaded module to the dict
367 const int ins_result = PyDict_SetItemString(plugins, PQ(module_name), module);
368 KIS_SAFE_ASSERT_RECOVER_NOOP(ins_result == 0);
369 Py_DECREF(module);
370 // Handle failure in release mode.
371 if (ins_result == 0) {
372 // Initialize the module from Python's side
373 PyObject* const args = Py_BuildValue("(s)", PQ(module_name));
374 PyObject* result = py.functionCall("_pluginLoaded", PyKrita::Python::PYKRITA_ENGINE, args);
375 Py_DECREF(args);
376 if (result) {
377 dbgScript << "\t" << "success!";
378 plugin.m_loaded = true;
379 return;
380 }
381 }
382 plugin.m_errorReason = i18nc("@info:tooltip", "Internal engine failure");
383 } else {
384 plugin.m_errorReason = i18nc(
385 "@info:tooltip"
386 , "Module not loaded:<br/><code>%1</code>"
387 , py.lastTraceback().replace("\n", "<br/>")
388 );
389 }
390 plugin.m_broken = true;
391 warnScript << "Error loading plugin" << module_name;
392}
393
395{
398
399 dbgScript << "Unloading module: " << plugin.moduleName();
400
402
403 // Get 'plugins' key from 'pykrita' module dictionary
404 PyObject* plugins = py.itemString("plugins");
406
407 PyObject* const args = Py_BuildValue("(s)", PQ(plugin.moduleName()));
408 py.functionCall("_pluginUnloading", PyKrita::Python::PYKRITA_ENGINE, args);
409 Py_DECREF(args);
410
411 // This will just decrement a reference count for module instance
412 PyDict_DelItemString(plugins, PQ(plugin.moduleName()));
413
414 // Remove the module also from 'sys.modules' dict to really unload it,
415 // so if reloaded all @init actions will work again!
416 PyObject* sys_modules = py.itemString("modules", "sys");
418 PyDict_DelItemString(sys_modules, PQ(plugin.moduleName()));
419
420 plugin.m_loaded = false;
421}
422
424{
425 bool wasEnabled = plugin.isEnabled();
426
427 if (wasEnabled && !enabled && plugin.m_loaded) {
429 }
430
431 plugin.m_enabled = enabled;
432 KConfigGroup pluginSettings(KSharedConfig::openConfig(), "python");
433 pluginSettings.writeEntry(QString("enable_") + plugin.moduleName(), enabled);
434
435 if (!wasEnabled && enabled) {
437 }
438}
static QString currentLocale()
static QString currentLocale()
PythonPluginManager * instance
static void writeSysInfo(const QString &message)
Writes to the system information file and Krita log.
static QStringList findAllAssets(const QString &type, const QString &filter=QString(), SearchOptions options=NoSearchOptions)
static QString findAsset(const QString &type, const QString &fileName)
bool functionCall(const char *functionName, const char *moduleName=PYKRITA_ENGINE)
static const char * PYKRITA_ENGINE
Definition utilities.h:219
PyObject * itemString(const char *item, const char *moduleName=PYKRITA_ENGINE)
QString lastTraceback(void) const
Class version_checker.
QString operationToString() const
static version_checker fromString(const QString &version_info)
Class version.
bool isValid() const
static version fromPythonObject(PyObject *version_obj)
QList< PythonPlugin > m_plugins
PythonPluginsModel * model()
void setPluginEnabled(PythonPlugin &plugin, bool enabled)
PythonPlugin * plugin(int index)
PythonPluginsModel m_model
static bool verifyModuleExists(PythonPlugin &)
const QList< PythonPlugin > & plugins() const
void loadModule(PythonPlugin &plugin)
static void verifyDependenciesSetStatus(PythonPlugin &)
static QPair< QString, PyKrita::version_checker > parseDependency(const QString &)
void unloadModule(PythonPlugin &plugin)
QString name() const
bool isBroken() const
bool isEnabled() const
bool isUnstable() const
QString moduleName() const
const QString & errorReason() const
QString moduleFilePathPart() const
QVariant property(const QString &name) const
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define dbgScript
Definition kis_debug.h:60
#define warnScript
Definition kis_debug.h:105
#define PQ(x)
Save us some ruddy time when printing out QStrings with UTF-8.
Definition utilities.h:21