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 dbgScript << "Got Krita/PythonPlugin: " << name()
50 << ", module-path=" << moduleName()
51 ;
52 // Make sure mandatory properties are here
53 if (m_name.isEmpty()) {
54 dbgScript << "Ignore desktop file w/o a name";
55 return false;
56 }
57 if (m_moduleName.isEmpty()) {
58 dbgScript << "Ignore desktop file w/o a module to import";
59 return false;
60 }
61
62 return true;
63}
64
65// PythonPluginManager implementation
66
68 : QObject(0)
69 , m_model(0, this)
70{}
71
76
78 if (index >= 0 && index < m_plugins.count()) {
79 return &m_plugins[index];
80 }
81
82 return nullptr;
83}
84
89
98
100{
101 // Find the module:
102 // 0) try to locate directory based plugin first
103 QString rel_path = plugin.moduleFilePathPart();
104 rel_path = rel_path + "/" + "__init__.py";
105 dbgScript << "Finding Python module with rel_path:" << rel_path;
106
107 QString module_path = KoResourcePaths::findAsset("pythonscripts", rel_path);
108
109 dbgScript << "module_path:" << module_path;
110
111 if (module_path.isEmpty()) {
112 // 1) Nothing found, then try file based plugin
113 rel_path = plugin.moduleFilePathPart() + ".py";
114 dbgScript << "Finding Python module with rel_path:" << rel_path;
115 module_path = KoResourcePaths::findAsset("pythonscripts", rel_path);
116 dbgScript << "module_path:" << module_path;
117 }
118
119 // Is anything found at all?
120 if (module_path.isEmpty()) {
121 plugin.m_broken = true;
122 plugin.m_errorReason = i18nc(
123 "@info:tooltip"
124 , "Unable to find the module specified <application>%1</application>"
126 );
127 dbgScript << "Cannot load module:" << plugin.m_errorReason;
128 return false;
129 }
130 dbgScript << "Found module path:" << module_path;
131 return true;
132}
133
134QPair<QString, PyKrita::version_checker> PythonPluginManager::parseDependency(const QString& d)
135{
136 // Check if dependency has package info attached
137 const int pnfo = d.indexOf('(');
138 if (pnfo != -1) {
139 QString dependency = d.mid(0, pnfo);
140 QString version_str = d.mid(pnfo + 1, d.size() - pnfo - 2).trimmed();
141 dbgScript << "Desired version spec [" << dependency << "]:" << version_str;
143 if (!(checker.isValid() && d.endsWith(')'))) {
144 dbgScript << "Invalid version spec " << d;
145 QString reason = i18nc(
146 "@info:tooltip"
147 , "<p>Specified version has invalid format for dependency <application>%1</application>: "
148 "<icode>%2</icode>. Skipped</p>"
149 , dependency
150 , version_str
151 );
152 return qMakePair(reason, PyKrita::version_checker());
153 }
154 return qMakePair(dependency, checker);
155 }
157}
158
170{
171 QStringList dependencies = plugin.property("X-Python-Dependencies").toStringList();
172
174 QString reason = i18nc("@info:tooltip", "<title>Dependency check</title>");
175 Q_FOREACH(const QString & d, dependencies) {
176 QPair<QString, PyKrita::version_checker> info_pair = parseDependency(d);
177 PyKrita::version_checker& checker = info_pair.second;
178 if (!checker.isValid()) {
179 plugin.m_broken = true;
180 reason += info_pair.first;
181 continue;
182 }
183
184 dbgScript << "Try to import dependency module/package:" << d;
185
186 // Try to import a module
187 const QString& dependency = info_pair.first;
188 PyObject* module = py.moduleImport(PQ(dependency));
189 if (module) {
190 if (checker.isEmpty()) { // Need to check smth?
191 dbgScript << "No version to check, just make sure it's loaded:" << dependency;
192 Py_DECREF(module);
193 continue;
194 }
195 // Try to get __version__ from module
196 // See PEP396: https://www.python.org/dev/peps/pep-0396/
197 PyObject* version_obj = py.itemString("__version__", PQ(dependency));
198 if (!version_obj) {
199 dbgScript << "No __version__ for " << dependency
200 << "[" << plugin.name() << "]:\n" << py.lastTraceback()
201 ;
202 plugin.m_unstable = true;
203 reason += i18nc(
204 "@info:tooltip"
205 , "<p>Failed to check version of dependency <application>%1</application>: "
206 "Module do not have PEP396 <code>__version__</code> attribute. "
207 "It is not disabled, but behaviour is unpredictable...</p>"
208 , dependency
209 );
210 }
211 PyKrita::version dep_version = PyKrita::version::fromPythonObject(version_obj);
212
213 if (!dep_version.isValid()) {
214 // Dunno what is this... Giving up!
215 dbgScript << "***: Can't parse module version for" << dependency;
216 plugin.m_unstable = true;
217 reason += i18nc(
218 "@info:tooltip"
219 , "<p><application>%1</application>: Unexpected module's version format"
220 , dependency
221 );
222 } else if (!checker(dep_version)) {
223 dbgScript << "Version requirement check failed ["
224 << plugin.name() << "] for "
225 << dependency << ": wanted " << checker.operationToString()
226 << QString(checker.required())
227 << ", but found" << QString(dep_version)
228 ;
229 plugin.m_broken = true;
230 reason += i18nc(
231 "@info:tooltip"
232 , "<p><application>%1</application>: No suitable version found. "
233 "Required version %2 %3, but found %4</p>"
234 , dependency
235 , checker.operationToString()
236 , QString(checker.required())
237 , QString(dep_version)
238 );
239 }
240 // Do not need this module anymore...
241 Py_DECREF(module);
242 } else {
243 dbgScript << "Load failure [" << plugin.name() << "]:\n" << py.lastTraceback();
244 plugin.m_broken = true;
245 reason += i18nc(
246 "@info:tooltip"
247 , "<p>Failure on module load <application>%1</application>:</p><pre>%2</pre>"
248 , dependency
249 , py.lastTraceback()
250 );
251 }
252 }
253
254 if (plugin.isBroken() || plugin.isUnstable()) {
255 plugin.m_errorReason = reason;
256 }
257}
258
260{
261 m_plugins.clear();
262
263 KConfigGroup pluginSettings(KSharedConfig::openConfig(), "python");
264
265 QStringList desktopFiles = KoResourcePaths::findAllAssets("data", "pykrita/*desktop");
266
267 Q_FOREACH(const QString &desktopFile, desktopFiles) {
268
269 KDesktopFile df(desktopFile);
270 df.setLocale(currentLocale());
271 const KConfigGroup dg = df.desktopGroup();
272 if (dg.readEntry("ServiceTypes") == "Krita/PythonPlugin") {
274 plugin.m_comment = df.readComment();
275 plugin.m_name = df.readName();
276 plugin.m_moduleName = dg.readEntry("X-KDE-Library");
277
278 QString manual = dg.readEntry("X-Krita-Manual");
279 if (!manual.isEmpty()) {
280 QFile f(QFileInfo(desktopFile).path() + "/" + plugin.m_moduleName + "/" + manual);
281 if (f.exists()) {
282 f.open(QFile::ReadOnly);
283 QByteArray ba = f.readAll();
284 f.close();
285 plugin.m_manual = QString::fromUtf8(ba);
286 }
287 }
288 if (!plugin.isValid()) {
289 dbgScript << plugin.name() << "is not usable";
290 continue;
291 }
292
294 dbgScript << "Cannot load" << plugin.name() << ": broken"
295 << plugin.isBroken()
296 << "because:" << plugin.errorReason();
297 continue;
298 }
299
301
302 plugin.m_enabled = pluginSettings.readEntry(QString("enable_") + plugin.moduleName(), false);
303
304 m_plugins.append(plugin);
305 }
306 }
307}
308
310{
311 KisUsageLogger::writeSysInfo("Loaded Python Plugins");
312 for (PythonPlugin &plugin : m_plugins) {
313 dbgScript << "Trying to load plugin" << plugin.moduleName()
314 << ". Enabled:" << plugin.isEnabled()
315 << ". Broken: " << plugin.isBroken();
316
317 if (plugin.m_enabled && !plugin.isBroken()) {
319 }
320 }
322}
323
325{
327
328 QString module_name = plugin.moduleName();
329 KisUsageLogger::writeSysInfo("\t" + module_name);
330 dbgScript << "Loading module: " << module_name;
331
333
334 // Get 'plugins' key from 'pykrita' module dictionary.
335 // Every entry has a module name as a key and 2 elements tuple as a value
336 PyObject* plugins = py.itemString("plugins");
338
339 PyObject* module = py.moduleImport(PQ(module_name));
340 if (module) {
341 // Move just loaded module to the dict
342 const int ins_result = PyDict_SetItemString(plugins, PQ(module_name), module);
343 KIS_SAFE_ASSERT_RECOVER_NOOP(ins_result == 0);
344 Py_DECREF(module);
345 // Handle failure in release mode.
346 if (ins_result == 0) {
347 // Initialize the module from Python's side
348 PyObject* const args = Py_BuildValue("(s)", PQ(module_name));
349 PyObject* result = py.functionCall("_pluginLoaded", PyKrita::Python::PYKRITA_ENGINE, args);
350 Py_DECREF(args);
351 if (result) {
352 dbgScript << "\t" << "success!";
353 plugin.m_loaded = true;
354 return;
355 }
356 }
357 plugin.m_errorReason = i18nc("@info:tooltip", "Internal engine failure");
358 } else {
359 plugin.m_errorReason = i18nc(
360 "@info:tooltip"
361 , "Module not loaded:<br/>%1"
362 , py.lastTraceback().replace("\n", "<br/>")
363 );
364 }
365 plugin.m_broken = true;
366 warnScript << "Error loading plugin" << module_name;
367}
368
370{
373
374 dbgScript << "Unloading module: " << plugin.moduleName();
375
377
378 // Get 'plugins' key from 'pykrita' module dictionary
379 PyObject* plugins = py.itemString("plugins");
381
382 PyObject* const args = Py_BuildValue("(s)", PQ(plugin.moduleName()));
383 py.functionCall("_pluginUnloading", PyKrita::Python::PYKRITA_ENGINE, args);
384 Py_DECREF(args);
385
386 // This will just decrement a reference count for module instance
387 PyDict_DelItemString(plugins, PQ(plugin.moduleName()));
388
389 // Remove the module also from 'sys.modules' dict to really unload it,
390 // so if reloaded all @init actions will work again!
391 PyObject* sys_modules = py.itemString("modules", "sys");
393 PyDict_DelItemString(sys_modules, PQ(plugin.moduleName()));
394
395 plugin.m_loaded = false;
396}
397
399{
400 bool wasEnabled = plugin.isEnabled();
401
402 if (wasEnabled && !enabled) {
404 }
405
406 plugin.m_enabled = enabled;
407 KConfigGroup pluginSettings(KSharedConfig::openConfig(), "python");
408 pluginSettings.writeEntry(QString("enable_") + plugin.moduleName(), enabled);
409
410 if (!wasEnabled && enabled) {
412 }
413}
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:56
#define warnScript
Definition kis_debug.h:98
#define PQ(x)
Save us some ruddy time when printing out QStrings with UTF-8.
Definition utilities.h:21