WIP Python plugin PoC

This commit is contained in:
xarkes 2018-06-23 18:59:23 +02:00 committed by Florian Märkl
parent a83791a3e0
commit a1110ee2ac
14 changed files with 350 additions and 172 deletions

View File

@ -228,7 +228,9 @@ SOURCES += \
RunScriptTask.cpp \
dialogs/EditMethodDialog.cpp \
dialogs/LoadNewTypesDialog.cpp \
widgets/SdbWidget.cpp
widgets/SdbWidget.cpp \
plugins/CutterPythonPlugin.cpp \
common/PythonManager.cpp
HEADERS += \
Cutter.h \
@ -339,7 +341,9 @@ HEADERS += \
common/Json.h \
dialogs/EditMethodDialog.h \
dialogs/LoadNewTypesDialog.h \
widgets/SdbWidget.h
widgets/SdbWidget.h \
plugins/CutterPythonPlugin.h \
common/PythonManager.h
FORMS += \
dialogs/AboutDialog.ui \

View File

@ -1,4 +1,9 @@
#include "common/PythonManager.h"
#include "CutterApplication.h"
#ifdef CUTTER_ENABLE_JUPYTER
#include "common/JupyterConnection.h"
#endif
#include "plugins/CutterPythonPlugin.h"
#include <QApplication>
#include <QFileOpenEvent>
@ -14,11 +19,6 @@
#include <QTranslator>
#include <QLibraryInfo>
#ifdef CUTTER_ENABLE_JUPYTER
#include "common/JupyterConnection.h"
#endif
#include "plugins/CutterPlugin.h"
#include "CutterConfig.h"
#include <cstdlib>
@ -72,11 +72,9 @@ CutterApplication::CutterApplication(int &argc, char **argv) : QApplication(argc
QObject::tr("file"));
cmd_parser.addOption(scriptOption);
#ifdef CUTTER_ENABLE_JUPYTER
QCommandLineOption pythonHomeOption("pythonhome", QObject::tr("PYTHONHOME to use for Jupyter"),
QCommandLineOption pythonHomeOption("pythonhome", QObject::tr("PYTHONHOME to use for embeded python interpreter"),
"PYTHONHOME");
cmd_parser.addOption(pythonHomeOption);
#endif
cmd_parser.process(*this);
@ -98,11 +96,12 @@ CutterApplication::CutterApplication(int &argc, char **argv) : QApplication(argc
}
}
#ifdef CUTTER_ENABLE_JUPYTER
// Init python
if (cmd_parser.isSet(pythonHomeOption)) {
Jupyter()->setPythonHome(cmd_parser.value(pythonHomeOption));
Python()->setPythonHome(cmd_parser.value(pythonHomeOption));
}
#endif
Python()->initialize();
bool analLevelSpecified = false;
int analLevel = 0;
@ -219,18 +218,29 @@ void CutterApplication::loadPlugins()
return;
}
Python()->addPythonPath(pluginsDir.absolutePath().toLatin1().data());
CutterPlugin *cutterPlugin = nullptr;
foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
QObject *plugin = pluginLoader.instance();
if (plugin) {
CutterPlugin *cutterPlugin = qobject_cast<CutterPlugin *>(plugin);
if (cutterPlugin) {
cutterPlugin->setupPlugin(Core());
plugins.append(cutterPlugin);
if (fileName.endsWith(".py")) {
// Load python plugins
cutterPlugin = Python()->loadPlugin(fileName.toLatin1().data());
} else {
// Load C++ plugins
QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
QObject *plugin = pluginLoader.instance();
if (plugin) {
cutterPlugin = qobject_cast<CutterPlugin *>(plugin);
}
}
if (cutterPlugin) {
cutterPlugin->setupPlugin(Core());
plugins.append(cutterPlugin);
}
}
qDebug() << "Loaded" << plugins.length() << "plugins.";
Core()->setCutterPlugins(plugins);
}

View File

@ -1,3 +1,4 @@
#include "common/PythonManager.h"
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "common/Helpers.h"

View File

@ -2,18 +2,12 @@
#include <Python.h>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QDebug>
#include <QThread>
#include <QCoreApplication>
#include <QDir>
#include "JupyterConnection.h"
#include "NestedIPyKernel.h"
#include "QtResImporter.h"
#include "PythonAPI.h"
#include "PythonManager.h"
#include <QVariant>
#include <QDebug>
Q_GLOBAL_STATIC(JupyterConnection, uniqueInstance)
@ -24,130 +18,32 @@ JupyterConnection *JupyterConnection::getInstance()
JupyterConnection::JupyterConnection(QObject *parent) : QObject(parent)
{
/* Will be removed/reworked with python plugins PR */
initPythonHome();
initPython();
qDebug() << "Python init";
}
JupyterConnection::~JupyterConnection()
{
if (pyThreadState) {
PyEval_RestoreThread(pyThreadState);
if (cutterNotebookAppInstance) {
auto stopFunc = PyObject_GetAttrString(cutterNotebookAppInstance, "stop");
PyObject_CallObject(stopFunc, nullptr);
Py_DECREF(cutterNotebookAppInstance);
}
Py_Finalize();
}
if (pythonHome) {
PyMem_RawFree(pythonHome);
}
}
void JupyterConnection::initPythonHome()
{
#if defined(APPIMAGE) || defined(MACOS_PYTHON_FRAMEWORK_BUNDLED)
if (customPythonHome.isNull()) {
auto pythonHomeDir = QDir(QCoreApplication::applicationDirPath());
#ifdef APPIMAGE
// Executable is in appdir/bin
pythonHomeDir.cdUp();
qInfo() << "Setting PYTHONHOME =" << pythonHomeDir.absolutePath() << " for AppImage.";
#else // MACOS_PYTHON_FRAMEWORK_BUNDLED
// @executable_path/../Frameworks/Python.framework/Versions/Current
pythonHomeDir.cd("../Frameworks/Python.framework/Versions/Current");
qInfo() << "Setting PYTHONHOME =" << pythonHomeDir.absolutePath() <<
" for macOS Application Bundle.";
#endif
customPythonHome = pythonHomeDir.absolutePath();
}
#endif
if (!customPythonHome.isNull()) {
qInfo() << "PYTHONHOME =" << customPythonHome;
pythonHome = Py_DecodeLocale(customPythonHome.toLocal8Bit().constData(), nullptr);
Py_SetPythonHome(pythonHome);
}
}
void JupyterConnection::initPython()
{
PyImport_AppendInittab("_cutter", &PyInit_api);
PyImport_AppendInittab("cutter_internal", &PyInit_api_internal);
PyImport_AppendInittab("_qtres", &PyInit_qtres);
Py_Initialize();
PyEval_InitThreads();
pyThreadState = PyEval_SaveThread();
}
void JupyterConnection::createCutterJupyterModule()
{
if (pyThreadState) {
PyEval_RestoreThread(pyThreadState);
}
cutterJupyterModule = QtResImport("cutter_jupyter");
if (!cutterJupyterModule) {
PyErr_Print();
qWarning() << "Could not import cutter_jupyter.";
emit creationFailed();
pyThreadState = PyEval_SaveThread();
return;
}
pyThreadState = PyEval_SaveThread();
}
void JupyterConnection::start()
{
if (cutterNotebookAppInstance) {
if (notebookInstanceExists) {
return;
}
if (!Py_IsInitialized()) {
initPython();
}
if (!cutterJupyterModule) {
createCutterJupyterModule();
if (!cutterJupyterModule) {
return;
}
}
PyEval_RestoreThread(pyThreadState);
auto startFunc = PyObject_GetAttrString(cutterJupyterModule, "start_jupyter");
cutterNotebookAppInstance = PyObject_CallObject(startFunc, nullptr);
pyThreadState = PyEval_SaveThread();
notebookInstanceExists = Python()->startJupyterNotebook();
emit urlReceived(getUrl());
}
QString JupyterConnection::getUrl()
{
if (!cutterNotebookAppInstance) {
if (!notebookInstanceExists) {
return nullptr;
}
PyEval_RestoreThread(pyThreadState);
auto urlWithToken = PyObject_GetAttrString(cutterNotebookAppInstance, "url_with_token");
auto asciiBytes = PyUnicode_AsASCIIString(urlWithToken);
auto urlWithTokenString = QString::fromUtf8(PyBytes_AsString(asciiBytes));
Py_DECREF(asciiBytes);
Py_DECREF(urlWithToken);
pyThreadState = PyEval_SaveThread();
return urlWithTokenString;
QString url = Python()->getJupyterUrl();
return url;
}
long JupyterConnection::startNestedIPyKernel(const QStringList &argv)

View File

@ -5,16 +5,9 @@
#include <QProcess>
#include <QMap>
#include <cwchar>
class NestedIPyKernel;
struct _object;
typedef _object PyObject;
struct _ts;
typedef _ts PyThreadState;
class JupyterConnection : public QObject
{
Q_OBJECT
@ -25,11 +18,6 @@ public:
JupyterConnection(QObject *parent = nullptr);
~JupyterConnection();
void setPythonHome(const QString pythonHome)
{
customPythonHome = pythonHome;
}
void start();
QString getUrl();
@ -42,26 +30,15 @@ signals:
void creationFailed();
private:
PyObject *cutterJupyterModule = nullptr;
PyObject *cutterNotebookAppInstance = nullptr;
PyThreadState *pyThreadState = nullptr;
QMap<long, NestedIPyKernel *> kernels;
long nextKernelId = 1;
QString customPythonHome;
wchar_t *pythonHome = nullptr;
void initPythonHome();
void initPython();
void createCutterJupyterModule();
bool notebookInstanceExists = false;
};
#define Jupyter() (JupyterConnection::getInstance())
#endif
#endif // CUTTER_ENABLE_JUPYTER
#endif //JUPYTERCONNECTION_H
#endif // JUPYTERCONNECTION_H

View File

@ -1,10 +1,6 @@
#ifdef CUTTER_ENABLE_JUPYTER
#include "PythonAPI.h"
#include "Cutter.h"
#include "JupyterConnection.h"
#include "NestedIPyKernel.h"
#include "CutterConfig.h"
@ -88,6 +84,9 @@ PyObject *PyInit_api()
// -----------------------------
#ifdef CUTTER_ENABLE_JUPYTER
#include "JupyterConnection.h"
#include "NestedIPyKernel.h"
PyObject *api_internal_launch_ipykernel(PyObject *self, PyObject *args, PyObject *kw)
{
@ -195,4 +194,4 @@ PyObject *PyInit_api_internal()
return PyModule_Create(&CutterInternalModule);
}
#endif
#endif // CUTTER_ENABLE_JUPYTER

View File

@ -0,0 +1,170 @@
#include "PythonAPI.h"
#include "PythonManager.h"
#include <marshal.h>
#include <QFile>
#include <QDebug>
#include "QtResImporter.h"
#include "plugins/CutterPythonPlugin.h"
Q_GLOBAL_STATIC(PythonManager, uniqueInstance)
PythonManager *PythonManager::getInstance()
{
return uniqueInstance;
}
PythonManager::PythonManager()
{
}
PythonManager::~PythonManager()
{
if (pyThreadState) {
PyEval_RestoreThread(pyThreadState);
if (cutterNotebookAppInstance) {
auto stopFunc = PyObject_GetAttrString(cutterNotebookAppInstance, "stop");
PyObject_CallObject(stopFunc, nullptr);
Py_DECREF(cutterNotebookAppInstance);
}
Py_Finalize();
}
if (pythonHome) {
PyMem_RawFree(pythonHome);
}
}
void PythonManager::initPythonHome()
{
#if defined(APPIMAGE) || defined(MACOS_PYTHON_FRAMEWORK_BUNDLED)
if (customPythonHome.isNull()) {
auto pythonHomeDir = QDir(QCoreApplication::applicationDirPath());
# ifdef APPIMAGE
// Executable is in appdir/bin
pythonHomeDir.cdUp();
qInfo() << "Setting PYTHONHOME =" << pythonHomeDir.absolutePath() << " for AppImage.";
# else // MACOS_PYTHON_FRAMEWORK_BUNDLED
// @executable_path/../Frameworks/Python.framework/Versions/Current
pythonHomeDir.cd("../Frameworks/Python.framework/Versions/Current");
qInfo() << "Setting PYTHONHOME =" << pythonHomeDir.absolutePath() <<
" for macOS Application Bundle.";
# endif
customPythonHome = pythonHomeDir.absolutePath();
}
#endif
if (!customPythonHome.isNull()) {
qInfo() << "PYTHONHOME =" << customPythonHome;
pythonHome = Py_DecodeLocale(customPythonHome.toLocal8Bit().constData(), nullptr);
Py_SetPythonHome(pythonHome);
}
}
void PythonManager::initialize()
{
initPythonHome();
PyImport_AppendInittab("_cutter", &PyInit_api);
PyImport_AppendInittab("cutter_internal", &PyInit_api_internal);
PyImport_AppendInittab("_qtres", &PyInit_qtres);
Py_Initialize();
PyEval_InitThreads();
// Import other modules
cutterJupyterModule = QtResImport("cutter_jupyter");
cutterPluginModule = QtResImport("cutter_plugin");
pyThreadState = PyEval_SaveThread();
}
void PythonManager::addPythonPath(char *path) {
if (pyThreadState) {
PyEval_RestoreThread(pyThreadState);
}
PyObject *sysModule = PyImport_ImportModule("sys");
if (!sysModule) {
return;
}
PyObject *pythonPath = PyObject_GetAttrString(sysModule, "path");
if (!pythonPath) {
return;
}
PyObject *append = PyObject_GetAttrString(pythonPath, "append");
if (!append) {
return;
}
PyEval_CallFunction(append, "(s)", path);
pyThreadState = PyEval_SaveThread();
}
bool PythonManager::startJupyterNotebook()
{
PyEval_RestoreThread(pyThreadState);
PyObject* startFunc = PyObject_GetAttrString(cutterJupyterModule, "start_jupyter");
if (!startFunc) {
qWarning() << "Couldn't get attribute start_jupyter.";
return false;
}
cutterNotebookAppInstance = PyObject_CallObject(startFunc, nullptr);
pyThreadState = PyEval_SaveThread();
return cutterNotebookAppInstance != nullptr;
}
QString PythonManager::getJupyterUrl()
{
PyEval_RestoreThread(pyThreadState);
auto urlWithToken = PyObject_GetAttrString(cutterNotebookAppInstance, "url_with_token");
auto asciiBytes = PyUnicode_AsASCIIString(urlWithToken);
auto urlWithTokenString = QString::fromUtf8(PyBytes_AsString(asciiBytes));
Py_DECREF(asciiBytes);
Py_DECREF(urlWithToken);
pyThreadState = PyEval_SaveThread();
return urlWithTokenString;
}
CutterPythonPlugin* PythonManager::loadPlugin(char *pluginName) {
CutterPythonPlugin *plugin = nullptr;
if (!cutterPluginModule) {
return plugin;
}
if (pyThreadState) {
PyEval_RestoreThread(pyThreadState);
}
PyObject *pluginModule = PyImport_ImportModule(pluginName);
if (!pluginModule) {
qWarning() << "Couldn't import the plugin" << QString(pluginName);
}
plugin = new CutterPythonPlugin(pluginModule);
pyThreadState = PyEval_SaveThread();
return plugin;
}
PyObject *PythonManager::getAttrStringSafe(PyObject *object, const char* attribute)
{
PyObject *result = nullptr;
if (pyThreadState) {
PyEval_RestoreThread(pyThreadState);
}
result = PyObject_GetAttrString(object, attribute);
pyThreadState = PyEval_SaveThread();
return result;
}

View File

@ -0,0 +1,48 @@
#ifndef PYTHONMANAGER_H
#define PYTHONMANAGER_H
#include <QObject>
class CutterPythonPlugin;
typedef struct _ts PyThreadState;
typedef struct _object PyObject;
class PythonManager : public QObject
{
Q_OBJECT
public:
static PythonManager *getInstance();
PythonManager();
~PythonManager();
void setPythonHome(const QString pythonHome)
{
customPythonHome = pythonHome;
}
void initPythonHome();
void initialize();
void addPythonPath(char *path);
bool startJupyterNotebook();
QString getJupyterUrl();
CutterPythonPlugin *loadPlugin(char *pluginName);
PyObject *getAttrStringSafe(PyObject *object, const char* attribute);
private:
QString customPythonHome;
wchar_t *pythonHome = nullptr;
PyThreadState *pyThreadState = nullptr;
PyObject *cutterJupyterModule;
PyObject *cutterPluginModule;
PyObject *cutterNotebookAppInstance = nullptr;
};
#define Python() (PythonManager::getInstance())
#endif // PYTHONMANAGER_H

View File

@ -0,0 +1,36 @@
#include <Python.h>
#include "CutterPythonPlugin.h"
CutterPythonPlugin::CutterPythonPlugin(PyObject* pluginModule)
{
this->pluginModule = pluginModule;
}
void CutterPythonPlugin::setupPlugin(CutterCore *core)
{
Q_UNUSED(core)
//PyObject *pInstance = PyObject_GetAttrString(pluginModule, "plugin");
PyObject *pInstance = Python()->getAttrStringSafe(pluginModule, "plugin");
if (!pInstance) {
qWarning() << "Cannot find plugin instance.";
return;
}
PyObject *pName = PyObject_GetAttrString(pInstance, "name");
qDebug() << "pname" << pName;
if (pName) {
this->name = QString(PyUnicode_AS_DATA(pName));
qDebug() << "OK COOL" << this->name;
}
}
CutterDockWidget* CutterPythonPlugin::setupInterface(MainWindow *main, QAction *action)
{
Q_UNUSED(main)
Q_UNUSED(action)
return nullptr;
}

View File

@ -0,0 +1,18 @@
#ifndef CUTTERPYTHONPLUGIN_H
#define CUTTERPYTHONPLUGIN_H
#include "common/PythonManager.h"
#include "CutterPlugin.h"
class CutterPythonPlugin : public CutterPlugin
{
public:
CutterPythonPlugin(PyObject* pluginModule);
void setupPlugin(CutterCore *core);
CutterDockWidget* setupInterface(MainWindow *main, QAction *action);
private:
PyObject *pluginModule;
};
#endif // CUTTERPYTHONPLUGIN_H

View File

@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
class CutterPlugin(ABC):
name = ''
description = ''
version = ''
author = ''
core = None
dockable = None
@abstractmethod
def __init__(self):
pass
@abstractmethod
def setupPlugin(self):
pass
@abstractmethod
def setupInterface(self, main, action):
pass

View File

@ -86,5 +86,6 @@
<file>img/icons/fork.svg</file>
<file>python/cutter.py</file>
<file>python/reg_qtres_importer.py</file>
<file>python/cutter_plugin.py</file>
</qresource>
</RCC>

View File

@ -1,9 +1,8 @@
#ifdef CUTTER_ENABLE_JUPYTER
#include "ui_JupyterWidget.h"
#include "common/JupyterConnection.h"
#include "JupyterWidget.h"
#include "ui_JupyterWidget.h"
#include <QTabWidget>
#include <QHBoxLayout>

View File

@ -4,12 +4,10 @@
#ifdef CUTTER_ENABLE_JUPYTER
#include <memory>
#include <QAbstractButton>
#include "CutterDockWidget.h"
#include "common/JupyterConnection.h"
#include <memory>
#include <QAbstractButton>
namespace Ui {
class JupyterWidget;