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 auto arguments = PyTuple_New(0);
150 PyObject* const result = functionCall(functionName, moduleName, arguments);
151 Py_DECREF(arguments);
152 if (result)
153 Py_DECREF(result);
154 return bool(result);
155}
156
158 const char* const functionName
159 , const char* const moduleName
160 , PyObject* const arguments
161)
162{
163 if (!arguments) {
164 errScript << "Missing arguments for" << moduleName << functionName;
165 return nullptr;
166 }
167 PyObject* const func = itemString(functionName, moduleName);
168 if (!func) {
169 errScript << "Failed to resolve" << moduleName << functionName;
170 return nullptr;
171 }
172 if (!PyCallable_Check(func)) {
173 traceback(QString("Not callable %1.%2").arg(moduleName).arg(functionName));
174 return nullptr;
175 }
176 PyObject* const result = PyObject_CallObject(func, arguments);
177 if (!result)
178 traceback(QString("No result from %1.%2").arg(moduleName).arg(functionName));
179
180 return result;
181}
182
183bool Python::itemStringDel(const char* const item, const char* const moduleName)
184{
185 PyObject* const dict = moduleDict(moduleName);
186 const bool result = dict && PyDict_DelItemString(dict, item);
187 if (!result)
188 traceback(QString("Could not delete item string %1.%2").arg(moduleName).arg(item));
189 return result;
190}
191
192PyObject* Python::itemString(const char* const item, const char* const moduleName)
193{
194 if (PyObject* const value = itemString(item, moduleDict(moduleName)))
195 return value;
196
197 errScript << "Could not get item string" << moduleName << item;
198 return 0;
199}
200
201PyObject* Python::itemString(const char* item, PyObject* dict)
202{
203 if (dict)
204 if (PyObject* const value = PyDict_GetItemString(dict, item))
205 return value;
206 traceback(QString("Could not get item string %1").arg(item));
207 return 0;
208}
209
210bool Python::itemStringSet(const char* const item, PyObject* const value, const char* const moduleName)
211{
212 PyObject* const dict = moduleDict(moduleName);
213 const bool result = dict && !PyDict_SetItemString(dict, item, value);
214 if (!result)
215 traceback(QString("Could not set item string %1.%2").arg(moduleName).arg(item));
216 return result;
217}
218
219PyObject* Python::kritaHandler(const char* const moduleName, const char* const handler)
220{
221 if (PyObject* const module = moduleImport(moduleName)) {
222 PyObject* args = Py_BuildValue("(O)", module);
223 PyObject* ret = functionCall(handler, "krita", args);
224 Py_DECREF(args);
225 return ret;
226 }
227 return 0;
228}
229
231{
232 QString result;
233 result.swap(m_traceback);
234 return result;
235}
236
238{
239 // no-op on Windows
240#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
241 if (!s_pythonLibrary) {
242
243 QFileInfo fi(PYKRITA_PYTHON_LIBRARY);
244 // get the filename of the configured Python library, without the .so suffix
245 const QString libraryName = fi.completeBaseName();
246 // 1.0 is the SONAME of the shared Python library
247 s_pythonLibrary = new QLibrary(libraryName, "1.0");
248 s_pythonLibrary->setLoadHints(QLibrary::ExportExternalSymbolsHint);
249 if (!s_pythonLibrary->load()) {
250 dbgScript << QString("Could not load %1 -- Reason: %2").arg(s_pythonLibrary->fileName()).arg(s_pythonLibrary->errorString());
251 delete s_pythonLibrary;
252 s_pythonLibrary = 0;
253 return false;
254 }
255 dbgScript << QString("Loaded %1").arg(s_pythonLibrary->fileName());
256 }
257#endif
258 return true;
259}
260
261namespace
262{
263
264QString findKritaPythonLibsPath(const QString &libdir)
265{
266 QString rootPath(KoResourcePaths::getApplicationRoot());
267
268 QDir rootDir(rootPath);
269 QDir frameworkDir(rootPath + "Frameworks/Python.framework/Versions/Current");
270
271 QFileInfoList candidates =
272 frameworkDir.entryInfoList(QStringList() << "lib", QDir::Dirs | QDir::NoDotAndDotDot) +
273 rootDir.entryInfoList(QStringList() << "lib*", QDir::Dirs | QDir::NoDotAndDotDot) +
274 rootDir.entryInfoList(QStringList() << "Frameworks", QDir::Dirs | QDir::NoDotAndDotDot) +
275 rootDir.entryInfoList(QStringList() << "share", QDir::Dirs | QDir::NoDotAndDotDot);
276 Q_FOREACH (const QFileInfo &entry, candidates) {
277 QDir libDir(entry.absoluteFilePath());
278 if (libDir.cd(libdir)) {
279 return libDir.absolutePath();
280 } else {
281 // Handle cases like Linux where libs are placed in a sub-dir
282 // with the ABI name
283 Q_FOREACH (const QFileInfo &subEntry, libDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
284 QDir subDir(subEntry.absoluteFilePath());
285 if (subDir.cd(libdir)) {
286 return subDir.absolutePath();
287 }
288 }
289 }
290 }
291 return QString();
292}
293
294} // namespace
295
296bool Python::setPath(const QStringList& scriptPaths)
297{
298
299 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!Py_IsInitialized(), false);
300 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!isPythonPathSet, false);
301
302// qDebug() << ">>>>>>>>>>>" << qgetenv("APPDIR")
303// << KoResourcePaths::getApplicationRoot()
304// << (!qgetenv("APPDIR").isNull() && KoResourcePaths::getApplicationRoot().contains(qgetenv("APPDIR")));
305
306
307#if defined Q_OS_WIN
308 bool runningInBundle = false;
309#elif defined Q_OS_MAC
310 bool runningInBundle = KoResourcePaths::getApplicationRoot().toLower().contains("krita.app");
311#else
312 bool runningInBundle = (!qgetenv("APPDIR").isNull() &&
313 KoResourcePaths::getApplicationRoot().contains(qgetenv("APPDIR")));
314#endif
315 dbgScript << "Python::setPath. Script paths:" << scriptPaths << runningInBundle;
316
317#ifdef Q_OS_WIN
318 constexpr char pathSeparator = ';';
319#else
320 constexpr char pathSeparator = ':';
321#endif
322
323 QString originalPath;
324 // Start with the script paths
325 QStringList paths(scriptPaths);
326
327 // Append the Krita libraries path
328 QString pythonLibsPath = findKritaPythonLibsPath("krita-python-libs");
329 dbgScript << "pythonLibsPath (krita-python-libs)" << pythonLibsPath;
330 if (pythonLibsPath.isEmpty()) {
331 dbgScript << "Cannot find krita-python-libs";
332 return false;
333 }
334 dbgScript << "Found krita-python-libs at" << pythonLibsPath;
335 paths.append(pythonLibsPath);
336
337#ifndef Q_OS_WIN
338 // Append the sip libraries path
339 pythonLibsPath = findKritaPythonLibsPath("sip");
340 dbgScript << "pythonLibsPath (sip)" << pythonLibsPath;
341 if (!pythonLibsPath.isEmpty()) {
342 dbgScript << "Found sip at" << pythonLibsPath;
343 paths.append(pythonLibsPath);
344 }
345#endif
346
347#ifdef Q_OS_WIN
348 // Find embeddable Python at <root>/python
349 QDir pythonDir(KoResourcePaths::getApplicationRoot());
350 if (pythonDir.cd("python")) {
351 const QString pythonHome = pythonDir.absolutePath();
352 dbgScript << "Found bundled Python at" << pythonHome;
353 // The default paths for Windows embeddable Python is
354 // ./python[0-9][0-9]+.zip;./
355 // Ordinarily, Python would find its own bundle here.
356 // But because we set sys.path manually afterwards,
357 // the prefix gets zeroed out. See
358 // https://docs.python.org/3/c-api/init.html#c.Py_SetPath
359 paths.append(pythonDir.absoluteFilePath(QStringLiteral("python%1%2.zip")
360 .arg(PY_MAJOR_VERSION)
361 .arg(PY_MINOR_VERSION)));
362 paths.append(pythonDir.absolutePath());
363 } else {
364 errScript << "Bundled Python not found, cannot set Python library paths";
365 return false;
366 }
367
368 // Add stock Python libs folder at <root>/lib/site-packages
369 QString distToolsPath = findKritaPythonLibsPath("site-packages");
370 dbgScript << "distToolsPath (site-packages)" << distToolsPath;
371 if (distToolsPath.isEmpty()) {
372 dbgScript << "Cannot find site-packages";
373 return false;
374 }
375 dbgScript << "Found site-packages at" << distToolsPath;
376 paths.append(distToolsPath);
377#else
378 // If using a system Python install, respect the current PYTHONPATH
379 if (runningInBundle) {
380 // We're running from an appimage, so we need our local python
381 QString p = QFileInfo(PYKRITA_PYTHON_LIBRARY).fileName();
382#ifdef Q_OS_MAC
383 QString p2 = p.remove("lib").remove("m.dy").remove(".dy");
384#else
385 QString p2 = p.remove("lib").remove("m.so").remove(".so");
386#endif
387 dbgScript << "\t" << p << p2;
388 originalPath = findKritaPythonLibsPath(p);
389#ifdef Q_OS_MAC
390 // Are we running with a system Python library instead?
391 if (originalPath.isEmpty()) {
392 // Keep the original Python search path.
393 originalPath = QString::fromWCharArray(Py_GetPath());
394 QString d = QFileInfo(PYKRITA_PYTHON_LIBRARY).absolutePath();
395
396 paths.append(d + "/" + p2 + "/site-packages");
397#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
398 paths.append(d + "/" + p2 + "/site-packages/PyQt6");
399#else
400 paths.append(d + "/" + p2 + "/site-packages/PyQt5");
401#endif
402
403 }
404 else {
405#endif
406 paths.append(originalPath + "/lib-dynload");
407 paths.append(originalPath + "/site-packages");
408#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
409 paths.append(originalPath + "/site-packages/PyQt6");
410#else
411 paths.append(originalPath + "/site-packages/PyQt5");
412#endif
413#ifdef Q_OS_MAC
414 }
415#endif
416 }
417 else {
418 // Use the system path
419 originalPath = QString::fromLocal8Bit(qgetenv("PYTHONPATH"));
420 }
421#endif
422
423 QString joinedPaths = paths.join(pathSeparator);
424 if (!originalPath.isEmpty()) {
425 joinedPaths = joinedPaths + pathSeparator + originalPath;
426 }
427 dbgScript << "Setting python paths:" << joinedPaths;
428 qputenv("PYTHONPATH", joinedPaths.toLocal8Bit());
429
430 isPythonPathSet = true;
431 return true;
432}
433
435{
436 if (Py_IsInitialized()) {
437 warnScript << "Python interpreter is already initialized, not initializing again";
438 } else {
439 dbgScript << "Initializing Python interpreter";
440 Py_InitializeEx(0);
441 if (!Py_IsInitialized()) {
442 errScript << "Could not initialize Python interpreter";
443 }
444#if THREADED
445 s_pythonThreadState = PyGILState_GetThisThreadState();
446 PyEval_ReleaseThread(s_pythonThreadState);
447#endif
448 }
449}
450
452{
453 if (!Py_IsInitialized()) {
454 warnScript << "Python interpreter not initialized, no need to finalize";
455 } else {
456#if THREADED
457 PyEval_AcquireThread(s_pythonThreadState);
458#endif
459 Py_Finalize();
460 }
461}
462
464{
465 // no-op on Windows
466#ifndef Q_OS_WIN
467 if (s_pythonLibrary) {
468 // Shut the interpreter down if it has been started.
469 if (s_pythonLibrary->isLoaded()) {
470 s_pythonLibrary->unload();
471 }
472 delete s_pythonLibrary;
473 s_pythonLibrary = 0;
474 }
475#endif
476}
477
478PyObject* Python::moduleActions(const char* moduleName)
479{
480 return kritaHandler(moduleName, "moduleGetActions");
481}
482
483PyObject* Python::moduleConfigPages(const char* const moduleName)
484{
485 return kritaHandler(moduleName, "moduleGetConfigPages");
486}
487
488QString Python::moduleHelp(const char* moduleName)
489{
490 QString r;
491 PyObject* const result = kritaHandler(moduleName, "moduleGetHelp");
492 if (result) {
493 r = unicode(result);
494 Py_DECREF(result);
495 }
496 return r;
497}
498
499PyObject* Python::moduleDict(const char* const moduleName)
500{
501 PyObject* const module = moduleImport(moduleName);
502 if (module)
503 if (PyObject* const dictionary = PyModule_GetDict(module))
504 return dictionary;
505
506 traceback(QString("Could not get dict %1").arg(moduleName));
507 return 0;
508}
509
510PyObject* Python::moduleImport(const char* const moduleName)
511{
512 PyObject* const module = PyImport_ImportModule(moduleName);
513 if (module)
514 return module;
515
516 traceback(QString("Could not import %1").arg(moduleName));
517 return 0;
518}
519
520// Inspired by https://lists.gt.net/python/python/150924.
521void Python::traceback(const QString& description)
522{
523 m_traceback.clear();
524 if (!PyErr_Occurred())
525 // Return an empty string on no error.
526 // NOTE "Return a string?" really??
527 return;
528
529 PyObject* exc_typ;
530 PyObject* exc_val;
531 PyObject* exc_tb;
532 PyErr_Fetch(&exc_typ, &exc_val, &exc_tb);
533 PyErr_NormalizeException(&exc_typ, &exc_val, &exc_tb);
534
535 // Include the traceback.
536 if (exc_tb) {
537 m_traceback = "Traceback (most recent call last):\n";
538 PyObject* const arguments = PyTuple_New(1);
539 PyTuple_SetItem(arguments, 0, exc_tb);
540 PyObject* const result = functionCall("format_tb", "traceback", arguments);
541 Py_DECREF(arguments);
542 if (result) {
543 for (int i = 0, j = PyList_Size(result); i < j; i++) {
544 PyObject* const tt = PyList_GetItem(result, i);
545 PyObject* const t = Py_BuildValue("(O)", tt);
546 char* buffer;
547 if (!PyArg_ParseTuple(t, "s", &buffer))
548 break;
549 m_traceback += buffer;
550 }
551 Py_DECREF(result);
552 }
553 Py_DECREF(exc_tb);
554 }
555
556 // Include the exception type and value.
557 if (exc_typ) {
558 PyObject* const temp = PyObject_GetAttrString(exc_typ, "__name__");
559 if (temp) {
560 m_traceback += unicode(temp);
561 m_traceback += ": ";
562 }
563 Py_DECREF(exc_typ);
564 }
565
566 if (exc_val) {
567 PyObject* const temp = PyObject_Str(exc_val);
568 if (temp) {
569 m_traceback += unicode(temp);
570 m_traceback += "\n";
571 }
572 Py_DECREF(exc_val);
573 }
574 m_traceback += description;
575
576 QStringList l = m_traceback.split("\n");
577 Q_FOREACH(const QString &s, l) {
578 errScript << s;
579 }
581}
582
583PyObject* Python::unicode(const QString& string)
584{
585 return PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND,
586 string.constData(),
587 string.length());
588}
589
590QString Python::unicode(PyObject* const string)
591{
592 if (!PyUnicode_Check(string))
593 return QString();
594
595 const int unichars = PyUnicode_GetLength(string);
596 if (0 != PyUnicode_READY(string))
597 return QString();
598
599 switch (PyUnicode_KIND(string)) {
600 case PyUnicode_1BYTE_KIND:
601 return QString::fromLatin1((const char*)PyUnicode_1BYTE_DATA(string), unichars);
602 case PyUnicode_2BYTE_KIND:
603 return QString::fromUtf16(PyUnicode_2BYTE_DATA(string), unichars);
604 case PyUnicode_4BYTE_KIND:
605 return QString::fromUcs4(PyUnicode_4BYTE_DATA(string), unichars);
606 default:
607 break;
608 }
609 return QString();
610}
611
612bool Python::isUnicode(PyObject* const string)
613{
614 return PyUnicode_Check(string);
615}
616
617bool Python::prependPythonPaths(const QString& path)
618{
619 PyObject* sys_path = itemString("path", "sys");
620 return bool(sys_path) && prependPythonPaths(path, sys_path);
621}
622
624{
625 PyObject* sys_path = itemString("path", "sys");
626 if (!sys_path)
627 return false;
628
630 QStringList reversed_paths;
631 std::reverse_copy(
632 paths.begin()
633 , paths.end()
634 , std::back_inserter(reversed_paths)
635 );
636
637 Q_FOREACH(const QString & path, reversed_paths)
638 if (!prependPythonPaths(path, sys_path))
639 return false;
640
641 return true;
642}
643
644bool Python::prependPythonPaths(const QString& path, PyObject* sys_path)
645{
646 Q_ASSERT("Dir entry expected to be valid" && sys_path);
647 return bool(prependStringToList(sys_path, path));
648}
649
650} // namespace PyKrita
651
652// 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