Krita Source Code Documentation
Loading...
Searching...
No Matches
utilities.cpp
Go to the documentation of this file.
1// This file is part of PyKrita, Krita' Python scripting plugin.
2//
3// SPDX-FileCopyrightText: 2006 Paul Giannaros <paul@giannaros.org>
4// SPDX-FileCopyrightText: 2012, 2013 Shaheed Haque <srhaque@theiet.org>
5// SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
6//
7// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
8//
9
10// config.h defines PYKRITA_PYTHON_LIBRARY, the path to libpython.so
11// on the build system
12
13#include "config.h"
14#include "utilities.h"
15#include "PythonPluginManager.h"
16
17#include <algorithm>
18
19#include <cmath>
20#include <Python.h>
21
22#include <QDir>
23#include <QLibrary>
24#include <QString>
25#include <QStringList>
26#include <QFileInfo>
27
28#include <KoResourcePaths.h>
29#include <kconfigbase.h>
30#include <kconfiggroup.h>
31#include <klocalizedstring.h>
32
33#include <kis_debug.h>
34
35#include "PykritaModule.h"
36
37#define THREADED 1
38
39namespace PyKrita
40{
42 static QScopedPointer<PythonPluginManager> pluginManagerInstance;
43
45 {
46 // Already initialized?
47 if (initStatus == INIT_OK) return INIT_OK;
48
49 dbgScript << "Initializing Python plugin for Python" << PY_MAJOR_VERSION << "," << PY_MINOR_VERSION;
50
51 if (!Python::libraryLoad()) {
53 }
54
55 // Update PYTHONPATH
56 // 0) custom plugin directories (prefer local dir over systems')
57 // 1) shipped krita module's dir
58 QStringList pluginDirectories = KoResourcePaths::findDirs("pythonscripts");
59 dbgScript << "Plugin Directories: " << pluginDirectories;
60 if (!Python::setPath(pluginDirectories)) {
62 return initStatus;
63 }
64
65 if (0 != PyImport_AppendInittab(Python::PYKRITA_ENGINE, PYKRITA_INIT)) {
67 return initStatus;
68 }
69
71 Python py = Python();
72
73 // Initialize 'plugins' dict of module 'pykrita'
74 PyObject* plugins = PyDict_New();
75 py.itemStringSet("plugins", plugins);
76
78
79 // Initialize our built-in module.
80 auto pykritaModule = PYKRITA_INIT();
81
82 if (!pykritaModule) {
84 return initStatus;
85 //return i18nc("@info:tooltip ", "No <icode>pykrita</icode> built-in module");
86 }
87
89 return initStatus;
90 }
91
98
99 void finalize() {
100 dbgScript << "Going to destroy the Python engine";
103
106
107 pluginManagerInstance.reset();
109 }
110 }
111
112 namespace
113{
114#ifndef Q_OS_WIN
115QLibrary* s_pythonLibrary = 0;
116#endif
117PyThreadState* s_pythonThreadState = 0;
118bool isPythonPathSet = false;
119} // anonymous namespace
120
121const char* Python::PYKRITA_ENGINE = "pykrita";
122
124{
125#if THREADED
126 m_state = PyGILState_Ensure();
127#endif
128}
129
131{
132#if THREADED
133 PyGILState_Release(m_state);
134#endif
135}
136
137bool Python::prependStringToList(PyObject* const list, const QString& value)
138{
139 PyObject* const u = unicode(value);
140 bool result = !PyList_Insert(list, 0, u);
141 Py_DECREF(u);
142 if (!result)
143 traceback(QString("Failed to prepend %1").arg(value));
144 return result;
145}
146
147bool Python::functionCall(const char* const functionName, const char* const moduleName)
148{
149 PyObject* const result = functionCall(functionName, moduleName, PyTuple_New(0));
150 if (result)
151 Py_DECREF(result);
152 return bool(result);
153}
154
156 const char* const functionName
157 , const char* const moduleName
158 , PyObject* const arguments
159)
160{
161 if (!arguments) {
162 errScript << "Missing arguments for" << moduleName << functionName;
163 return 0;
164 }
165 PyObject* const func = itemString(functionName, moduleName);
166 if (!func) {
167 errScript << "Failed to resolve" << moduleName << functionName;
168 return 0;
169 }
170 if (!PyCallable_Check(func)) {
171 traceback(QString("Not callable %1.%2").arg(moduleName).arg(functionName));
172 return 0;
173 }
174 PyObject* const result = PyObject_CallObject(func, arguments);
175 Py_DECREF(arguments);
176 if (!result)
177 traceback(QString("No result from %1.%2").arg(moduleName).arg(functionName));
178
179 return result;
180}
181
182bool Python::itemStringDel(const char* const item, const char* const moduleName)
183{
184 PyObject* const dict = moduleDict(moduleName);
185 const bool result = dict && PyDict_DelItemString(dict, item);
186 if (!result)
187 traceback(QString("Could not delete item string %1.%2").arg(moduleName).arg(item));
188 return result;
189}
190
191PyObject* Python::itemString(const char* const item, const char* const moduleName)
192{
193 if (PyObject* const value = itemString(item, moduleDict(moduleName)))
194 return value;
195
196 errScript << "Could not get item string" << moduleName << item;
197 return 0;
198}
199
200PyObject* Python::itemString(const char* item, PyObject* dict)
201{
202 if (dict)
203 if (PyObject* const value = PyDict_GetItemString(dict, item))
204 return value;
205 traceback(QString("Could not get item string %1").arg(item));
206 return 0;
207}
208
209bool Python::itemStringSet(const char* const item, PyObject* const value, const char* const moduleName)
210{
211 PyObject* const dict = moduleDict(moduleName);
212 const bool result = dict && !PyDict_SetItemString(dict, item, value);
213 if (!result)
214 traceback(QString("Could not set item string %1.%2").arg(moduleName).arg(item));
215 return result;
216}
217
218PyObject* Python::kritaHandler(const char* const moduleName, const char* const handler)
219{
220 if (PyObject* const module = moduleImport(moduleName))
221 return functionCall(handler, "krita", Py_BuildValue("(O)", module));
222 return 0;
223}
224
226{
227 QString result;
228 result.swap(m_traceback);
229 return result;
230}
231
233{
234 // no-op on Windows
235#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
236 if (!s_pythonLibrary) {
237
238 QFileInfo fi(PYKRITA_PYTHON_LIBRARY);
239 // get the filename of the configured Python library, without the .so suffix
240 const QString libraryName = fi.completeBaseName();
241 // 1.0 is the SONAME of the shared Python library
242 s_pythonLibrary = new QLibrary(libraryName, "1.0");
243 s_pythonLibrary->setLoadHints(QLibrary::ExportExternalSymbolsHint);
244 if (!s_pythonLibrary->load()) {
245 dbgScript << QString("Could not load %1 -- Reason: %2").arg(s_pythonLibrary->fileName()).arg(s_pythonLibrary->errorString());
246 delete s_pythonLibrary;
247 s_pythonLibrary = 0;
248 return false;
249 }
250 dbgScript << QString("Loaded %1").arg(s_pythonLibrary->fileName());
251 }
252#endif
253 return true;
254}
255
256namespace
257{
258
259QString findKritaPythonLibsPath(const QString &libdir)
260{
261 QString rootPath(KoResourcePaths::getApplicationRoot());
262
263 QDir rootDir(rootPath);
264 QDir frameworkDir(rootPath + "Frameworks/Python.framework/Versions/Current");
265
266 QFileInfoList candidates =
267 frameworkDir.entryInfoList(QStringList() << "lib", QDir::Dirs | QDir::NoDotAndDotDot) +
268 rootDir.entryInfoList(QStringList() << "lib*", QDir::Dirs | QDir::NoDotAndDotDot) +
269 rootDir.entryInfoList(QStringList() << "Frameworks", QDir::Dirs | QDir::NoDotAndDotDot) +
270 rootDir.entryInfoList(QStringList() << "share", QDir::Dirs | QDir::NoDotAndDotDot);
271 Q_FOREACH (const QFileInfo &entry, candidates) {
272 QDir libDir(entry.absoluteFilePath());
273 if (libDir.cd(libdir)) {
274 return libDir.absolutePath();
275 } else {
276 // Handle cases like Linux where libs are placed in a sub-dir
277 // with the ABI name
278 Q_FOREACH (const QFileInfo &subEntry, libDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
279 QDir subDir(subEntry.absoluteFilePath());
280 if (subDir.cd(libdir)) {
281 return subDir.absolutePath();
282 }
283 }
284 }
285 }
286 return QString();
287}
288
289} // namespace
290
291bool Python::setPath(const QStringList& scriptPaths)
292{
293
294 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!Py_IsInitialized(), false);
295 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!isPythonPathSet, false);
296
297// qDebug() << ">>>>>>>>>>>" << qgetenv("APPDIR")
298// << KoResourcePaths::getApplicationRoot()
299// << (!qgetenv("APPDIR").isNull() && KoResourcePaths::getApplicationRoot().contains(qgetenv("APPDIR")));
300
301
302#if defined Q_OS_WIN
303 bool runningInBundle = false;
304#elif defined Q_OS_MAC
305 bool runningInBundle = KoResourcePaths::getApplicationRoot().toLower().contains("krita.app");
306#else
307 bool runningInBundle = (!qgetenv("APPDIR").isNull() &&
308 KoResourcePaths::getApplicationRoot().contains(qgetenv("APPDIR")));
309#endif
310 dbgScript << "Python::setPath. Script paths:" << scriptPaths << runningInBundle;
311
312#ifdef Q_OS_WIN
313 constexpr char pathSeparator = ';';
314#else
315 constexpr char pathSeparator = ':';
316#endif
317
318 QString originalPath;
319 // Start with the script paths
320 QStringList paths(scriptPaths);
321
322 // Append the Krita libraries path
323 QString pythonLibsPath = findKritaPythonLibsPath("krita-python-libs");
324 dbgScript << "pythonLibsPath (krita-python-libs)" << pythonLibsPath;
325 if (pythonLibsPath.isEmpty()) {
326 dbgScript << "Cannot find krita-python-libs";
327 return false;
328 }
329 dbgScript << "Found krita-python-libs at" << pythonLibsPath;
330 paths.append(pythonLibsPath);
331
332#ifndef Q_OS_WIN
333 // Append the sip libraries path
334 pythonLibsPath = findKritaPythonLibsPath("sip");
335 dbgScript << "pythonLibsPath (sip)" << pythonLibsPath;
336 if (!pythonLibsPath.isEmpty()) {
337 dbgScript << "Found sip at" << pythonLibsPath;
338 paths.append(pythonLibsPath);
339 }
340#endif
341
342#ifdef Q_OS_WIN
343 // Find embeddable Python at <root>/python
344 QDir pythonDir(KoResourcePaths::getApplicationRoot());
345 if (pythonDir.cd("python")) {
346 const QString pythonHome = pythonDir.absolutePath();
347 dbgScript << "Found bundled Python at" << pythonHome;
348 // The default paths for Windows embeddable Python is
349 // ./python[0-9][0-9]+.zip;./
350 // Ordinarily, Python would find its own bundle here.
351 // But because we set sys.path manually afterwards,
352 // the prefix gets zeroed out. See
353 // https://docs.python.org/3/c-api/init.html#c.Py_SetPath
354 paths.append(pythonDir.absoluteFilePath(QStringLiteral("python%1%2.zip")
355 .arg(PY_MAJOR_VERSION)
356 .arg(PY_MINOR_VERSION)));
357 paths.append(pythonDir.absolutePath());
358 } else {
359 errScript << "Bundled Python not found, cannot set Python library paths";
360 return false;
361 }
362
363 // Add stock Python libs folder at <root>/lib/site-packages
364 QString distToolsPath = findKritaPythonLibsPath("site-packages");
365 dbgScript << "distToolsPath (site-packages)" << distToolsPath;
366 if (distToolsPath.isEmpty()) {
367 dbgScript << "Cannot find site-packages";
368 return false;
369 }
370 dbgScript << "Found site-packages at" << distToolsPath;
371 paths.append(distToolsPath);
372#else
373 // If using a system Python install, respect the current PYTHONPATH
374 if (runningInBundle) {
375 // We're running from an appimage, so we need our local python
376 QString p = QFileInfo(PYKRITA_PYTHON_LIBRARY).fileName();
377#ifdef Q_OS_MAC
378 QString p2 = p.remove("lib").remove("m.dy").remove(".dy");
379#else
380 QString p2 = p.remove("lib").remove("m.so").remove(".so");
381#endif
382 dbgScript << "\t" << p << p2;
383 originalPath = findKritaPythonLibsPath(p);
384#ifdef Q_OS_MAC
385 // Are we running with a system Python library instead?
386 if (originalPath.isEmpty()) {
387 // Keep the original Python search path.
388 originalPath = QString::fromWCharArray(Py_GetPath());
389 QString d = QFileInfo(PYKRITA_PYTHON_LIBRARY).absolutePath();
390
391 paths.append(d + "/" + p2 + "/site-packages");
392#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
393 paths.append(d + "/" + p2 + "/site-packages/PyQt6");
394#else
395 paths.append(d + "/" + p2 + "/site-packages/PyQt5");
396#endif
397
398 }
399 else {
400#endif
401 paths.append(originalPath + "/lib-dynload");
402 paths.append(originalPath + "/site-packages");
403#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
404 paths.append(originalPath + "/site-packages/PyQt6");
405#else
406 paths.append(originalPath + "/site-packages/PyQt5");
407#endif
408#ifdef Q_OS_MAC
409 }
410#endif
411 }
412 else {
413 // Use the system path
414 originalPath = QString::fromLocal8Bit(qgetenv("PYTHONPATH"));
415 }
416#endif
417
418 QString joinedPaths = paths.join(pathSeparator);
419 if (!originalPath.isEmpty()) {
420 joinedPaths = joinedPaths + pathSeparator + originalPath;
421 }
422 dbgScript << "Setting python paths:" << joinedPaths;
423 qputenv("PYTHONPATH", joinedPaths.toLocal8Bit());
424
425 isPythonPathSet = true;
426 return true;
427}
428
430{
431 if (Py_IsInitialized()) {
432 warnScript << "Python interpreter is already initialized, not initializing again";
433 } else {
434 dbgScript << "Initializing Python interpreter";
435 Py_InitializeEx(0);
436 if (!Py_IsInitialized()) {
437 errScript << "Could not initialize Python interpreter";
438 }
439#if THREADED
440 s_pythonThreadState = PyGILState_GetThisThreadState();
441 PyEval_ReleaseThread(s_pythonThreadState);
442#endif
443 }
444}
445
447{
448 if (!Py_IsInitialized()) {
449 warnScript << "Python interpreter not initialized, no need to finalize";
450 } else {
451#if THREADED
452 PyEval_AcquireThread(s_pythonThreadState);
453#endif
454 Py_Finalize();
455 }
456}
457
459{
460 // no-op on Windows
461#ifndef Q_OS_WIN
462 if (s_pythonLibrary) {
463 // Shut the interpreter down if it has been started.
464 if (s_pythonLibrary->isLoaded()) {
465 s_pythonLibrary->unload();
466 }
467 delete s_pythonLibrary;
468 s_pythonLibrary = 0;
469 }
470#endif
471}
472
473PyObject* Python::moduleActions(const char* moduleName)
474{
475 return kritaHandler(moduleName, "moduleGetActions");
476}
477
478PyObject* Python::moduleConfigPages(const char* const moduleName)
479{
480 return kritaHandler(moduleName, "moduleGetConfigPages");
481}
482
483QString Python::moduleHelp(const char* moduleName)
484{
485 QString r;
486 PyObject* const result = kritaHandler(moduleName, "moduleGetHelp");
487 if (result) {
488 r = unicode(result);
489 Py_DECREF(result);
490 }
491 return r;
492}
493
494PyObject* Python::moduleDict(const char* const moduleName)
495{
496 PyObject* const module = moduleImport(moduleName);
497 if (module)
498 if (PyObject* const dictionary = PyModule_GetDict(module))
499 return dictionary;
500
501 traceback(QString("Could not get dict %1").arg(moduleName));
502 return 0;
503}
504
505PyObject* Python::moduleImport(const char* const moduleName)
506{
507 PyObject* const module = PyImport_ImportModule(moduleName);
508 if (module)
509 return module;
510
511 traceback(QString("Could not import %1").arg(moduleName));
512 return 0;
513}
514
515// Inspired by https://lists.gt.net/python/python/150924.
516void Python::traceback(const QString& description)
517{
518 m_traceback.clear();
519 if (!PyErr_Occurred())
520 // Return an empty string on no error.
521 // NOTE "Return a string?" really??
522 return;
523
524 PyObject* exc_typ;
525 PyObject* exc_val;
526 PyObject* exc_tb;
527 PyErr_Fetch(&exc_typ, &exc_val, &exc_tb);
528 PyErr_NormalizeException(&exc_typ, &exc_val, &exc_tb);
529
530 // Include the traceback.
531 if (exc_tb) {
532 m_traceback = "Traceback (most recent call last):\n";
533 PyObject* const arguments = PyTuple_New(1);
534 PyTuple_SetItem(arguments, 0, exc_tb);
535 PyObject* const result = functionCall("format_tb", "traceback", arguments);
536 if (result) {
537 for (int i = 0, j = PyList_Size(result); i < j; i++) {
538 PyObject* const tt = PyList_GetItem(result, i);
539 PyObject* const t = Py_BuildValue("(O)", tt);
540 char* buffer;
541 if (!PyArg_ParseTuple(t, "s", &buffer))
542 break;
543 m_traceback += buffer;
544 }
545 Py_DECREF(result);
546 }
547 Py_DECREF(exc_tb);
548 }
549
550 // Include the exception type and value.
551 if (exc_typ) {
552 PyObject* const temp = PyObject_GetAttrString(exc_typ, "__name__");
553 if (temp) {
554 m_traceback += unicode(temp);
555 m_traceback += ": ";
556 }
557 Py_DECREF(exc_typ);
558 }
559
560 if (exc_val) {
561 PyObject* const temp = PyObject_Str(exc_val);
562 if (temp) {
563 m_traceback += unicode(temp);
564 m_traceback += "\n";
565 }
566 Py_DECREF(exc_val);
567 }
568 m_traceback += description;
569
570 QStringList l = m_traceback.split("\n");
571 Q_FOREACH(const QString &s, l) {
572 errScript << s;
573 }
575}
576
577PyObject* Python::unicode(const QString& string)
578{
579 return PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND,
580 string.constData(),
581 string.length());
582}
583
584QString Python::unicode(PyObject* const string)
585{
586 if (!PyUnicode_Check(string))
587 return QString();
588
589 const int unichars = PyUnicode_GetLength(string);
590 if (0 != PyUnicode_READY(string))
591 return QString();
592
593 switch (PyUnicode_KIND(string)) {
594 case PyUnicode_1BYTE_KIND:
595 return QString::fromLatin1((const char*)PyUnicode_1BYTE_DATA(string), unichars);
596 case PyUnicode_2BYTE_KIND:
597 return QString::fromUtf16(PyUnicode_2BYTE_DATA(string), unichars);
598 case PyUnicode_4BYTE_KIND:
599 return QString::fromUcs4(PyUnicode_4BYTE_DATA(string), unichars);
600 default:
601 break;
602 }
603 return QString();
604}
605
606bool Python::isUnicode(PyObject* const string)
607{
608 return PyUnicode_Check(string);
609}
610
611bool Python::prependPythonPaths(const QString& path)
612{
613 PyObject* sys_path = itemString("path", "sys");
614 return bool(sys_path) && prependPythonPaths(path, sys_path);
615}
616
618{
619 PyObject* sys_path = itemString("path", "sys");
620 if (!sys_path)
621 return false;
622
624 QStringList reversed_paths;
625 std::reverse_copy(
626 paths.begin()
627 , paths.end()
628 , std::back_inserter(reversed_paths)
629 );
630
631 Q_FOREACH(const QString & path, reversed_paths)
632 if (!prependPythonPaths(path, sys_path))
633 return false;
634
635 return true;
636}
637
638bool Python::prependPythonPaths(const QString& path, PyObject* sys_path)
639{
640 Q_ASSERT("Dir entry expected to be valid" && sys_path);
641 return bool(prependStringToList(sys_path, path));
642}
643
644} // namespace PyKrita
645
646// krita: indent-width 4;
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
float value(const T *src, size_t ch)
const Params2D p
qreal u
QPointF p2
QList< QString > QStringList
#define PYKRITA_INIT
static QString getApplicationRoot()
static QStringList findDirs(const QString &type)
QString moduleHelp(const char *moduleName)
static PyObject * unicode(const QString &string)
Convert a QString to a Python unicode object.
bool itemStringSet(const char *item, PyObject *value, const char *moduleName=PYKRITA_ENGINE)
static bool isUnicode(PyObject *string)
Test if a Python object is compatible with a QString.
PyGILState_STATE m_state
Definition utilities.h:224
bool prependStringToList(PyObject *list, const QString &value)
Prepend a QString to a list as a Python unicode object.
static void libraryUnload()
PyObject * moduleDict(const char *moduleName=PYKRITA_ENGINE)
bool itemStringDel(const char *item, const char *moduleName=PYKRITA_ENGINE)
static bool setPath(const QStringList &scriptPaths)
bool functionCall(const char *functionName, const char *moduleName=PYKRITA_ENGINE)
PyObject * moduleConfigPages(const char *moduleName)
static const char * PYKRITA_ENGINE
Definition utilities.h:219
PyObject * moduleImport(const char *moduleName)
static void ensureInitialized()
QString m_traceback
Definition utilities.h:225
PyObject * moduleActions(const char *moduleName)
bool prependPythonPaths(const QString &path)
void traceback(const QString &description)
PyObject * kritaHandler(const char *moduleName, const char *handler)
static bool libraryLoad()
PyObject * itemString(const char *item, const char *moduleName=PYKRITA_ENGINE)
static void maybeFinalize()
QString lastTraceback(void) const
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define dbgScript
Definition kis_debug.h:56
#define warnScript
Definition kis_debug.h:98
#define errScript
Definition kis_debug.h:118
static QScopedPointer< PythonPluginManager > pluginManagerInstance
Definition utilities.cpp:42
@ INIT_UNINITIALIZED
Definition utilities.h:28
@ INIT_CANNOT_LOAD_PYKRITA_MODULE
Definition utilities.h:32
@ INIT_CANNOT_LOAD_PYTHON_LIBRARY
Definition utilities.h:30
@ INIT_CANNOT_SET_PYTHON_PATHS
Definition utilities.h:31
@ INIT_OK
Definition utilities.h:29
void finalize()
Definition utilities.cpp:99
static InitResult initStatus
Definition utilities.cpp:41
InitResult initialize()
Definition utilities.cpp:44
PythonPluginManager * pluginManager()
Definition utilities.cpp:92