diff --git a/src/backends/python/CMakeLists.txt b/src/backends/python/CMakeLists.txt index 773d9b07..bb6b333d 100644 --- a/src/backends/python/CMakeLists.txt +++ b/src/backends/python/CMakeLists.txt @@ -1,29 +1,28 @@ set( PythonBackend_SRCS pythonbackend.cpp pythonsession.cpp pythonexpression.cpp pythonkeywords.cpp pythonvariablemodel.cpp pythonhighlighter.cpp pythoncompletionobject.cpp pythonextensions.cpp ) qt5_add_resources(PythonBackend_RSCS python.qrc) ki18n_wrap_ui(PythonBackend_SRCS settings.ui) add_library(cantor_pythonbackend SHARED ${PythonBackend_SRCS} ${PythonBackend_RSCS}) generate_export_header(cantor_pythonbackend) target_link_libraries(cantor_pythonbackend cantorlibs KF5::KIOCore KF5::ConfigCore KF5::ConfigGui KF5::SyntaxHighlighting - Qt5::DBus ) install(TARGETS cantor_pythonbackend DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) #install(DIRECTORY . DESTINATION ${KDE_INSTALL_DATADIR}/cantor/pythonbackend FILES_MATCHING PATTERN "*.py") diff --git a/src/backends/python/pythoncompletionobject.cpp b/src/backends/python/pythoncompletionobject.cpp index 703bca0e..d0189ffc 100644 --- a/src/backends/python/pythoncompletionobject.cpp +++ b/src/backends/python/pythoncompletionobject.cpp @@ -1,176 +1,172 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2013 Filipe Saraiva */ #include "pythoncompletionobject.h" #include #include "result.h" #include "pythonsession.h" #include "pythonkeywords.h" using namespace Cantor; PythonCompletionObject::PythonCompletionObject(const QString& command, int index, PythonSession* session) : Cantor::CompletionObject(session), m_expression(nullptr) { setLine(command, index); } PythonCompletionObject::~PythonCompletionObject() { if (m_expression) m_expression->setFinishingBehavior(Expression::DeleteOnFinish); } void PythonCompletionObject::fetchCompletions() { if (session()->status() != Session::Done) { QStringList allCompletions; allCompletions << PythonKeywords::instance()->variables(); allCompletions << PythonKeywords::instance()->functions(); allCompletions << PythonKeywords::instance()->keywords(); setCompletions(allCompletions); emit fetchingDone(); } else { if (m_expression) return; qDebug() << "run fetchCompletions"; const QString& expr = QString::fromLatin1( "from __main__ import __dict__;" "import rlcompleter;" "print('|'.join(rlcompleter.Completer(__dict__).global_matches('%1')+rlcompleter.Completer(__dict__).attr_matches('%1')))" ).arg(command()); m_expression = session()->evaluateExpression(expr, Cantor::Expression::FinishingBehavior::DoNotDelete, true); - // TODO: Python exec the expression before connect, so manualy run handler. Uncomment the connection after removing DBus - // connect(m_expression, &Cantor::Expression::statusChanged, this, &PythonCompletionObject::extractCompletions); - extractCompletions(m_expression->status()); + connect(m_expression, &Cantor::Expression::statusChanged, this, &PythonCompletionObject::extractCompletions); } } void PythonCompletionObject::fetchIdentifierType() { if (session()->status() != Cantor::Session::Done) { if (qBinaryFind(PythonKeywords::instance()->functions().begin(), PythonKeywords::instance()->functions().end(), identifier()) != PythonKeywords::instance()->functions().end()) emit fetchingTypeDone(FunctionType); else if (qBinaryFind(PythonKeywords::instance()->keywords().begin(), PythonKeywords::instance()->keywords().end(), identifier()) != PythonKeywords::instance()->keywords().end()) emit fetchingTypeDone(KeywordType); else emit fetchingTypeDone(VariableType); } else { if (m_expression) return; const QString& expr = QString::fromLatin1("callable(%1)").arg(identifier()); m_expression = session()->evaluateExpression(expr, Cantor::Expression::FinishingBehavior::DoNotDelete, true); - // TODO: Python exec the expression before connect, so manualy run handler. Uncomment the connection after removing DBus - // connect(m_expression, &Cantor::Expression::statusChanged, this, &PythonCompletionObject::extractIdentifierType); - extractIdentifierType(m_expression->status()); + connect(m_expression, &Cantor::Expression::statusChanged, this, &PythonCompletionObject::extractIdentifierType); } } void PythonCompletionObject::extractCompletions(Cantor::Expression::Status status) { switch(status) { case Cantor::Expression::Error: qDebug() << "Error with PythonCompletionObject" << (m_expression->result() ? m_expression->result()->toHtml() : QLatin1String("extractCompletions")); break; case Cantor::Expression::Interrupted: qDebug() << "PythonCompletionObject was interrupted"; break; case Cantor::Expression::Done: if (m_expression->result()) setCompletions(m_expression->result()->data().toString().remove(QLatin1Char('(')).split(QLatin1Char('|'))); break; default: return; } m_expression->deleteLater(); m_expression = nullptr; emit fetchingDone(); } void PythonCompletionObject::extractIdentifierType(Cantor::Expression::Status status) { switch(status) { case Cantor::Expression::Error: if (m_expression->errorMessage().contains(QLatin1String("SyntaxError: invalid syntax"))) emit fetchingTypeDone(KeywordType); else { qDebug() << "Error with PythonCompletionObject" << (m_expression->result() ? m_expression->result()->toHtml() : QLatin1String("extractIdentifierType")); emit fetchingTypeDone(UnknownType); } break; case Cantor::Expression::Interrupted: qDebug() << "PythonCompletionObject was interrupted"; emit fetchingTypeDone(UnknownType); break; case Cantor::Expression::Done: if (m_expression->result()) { if (m_expression->result()->data().toString() == QLatin1String("True")) emit fetchingTypeDone(FunctionType); else emit fetchingTypeDone(VariableType); } else emit fetchingTypeDone(UnknownType); break; default: return; } m_expression->deleteLater(); m_expression = nullptr; } bool PythonCompletionObject::mayIdentifierContain(QChar c) const { return c.isLetter() || c.isDigit() || c == QLatin1Char('_') || c == QLatin1Char('%') || c == QLatin1Char('$') || c == QLatin1Char('.'); } bool PythonCompletionObject::mayIdentifierBeginWith(QChar c) const { return c.isLetter() || c == QLatin1Char('_') || c == QLatin1Char('%') || c == QLatin1Char('$'); } diff --git a/src/backends/python/pythonexpression.cpp b/src/backends/python/pythonexpression.cpp index 8c32fe54..bbe53709 100644 --- a/src/backends/python/pythonexpression.cpp +++ b/src/backends/python/pythonexpression.cpp @@ -1,137 +1,153 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2012 Filipe Saraiva */ #include "pythonexpression.h" #include #include "textresult.h" #include "imageresult.h" #include "helpresult.h" #include #include #include #include "pythonsession.h" #include "settings.h" #include #include #include PythonExpression::PythonExpression(Cantor::Session* session, bool internal) : Cantor::Expression(session, internal), m_tempFile(nullptr) { } PythonExpression::~PythonExpression() { if(m_tempFile) delete m_tempFile; } void PythonExpression::evaluate() { if(m_tempFile) { delete m_tempFile; m_tempFile = nullptr; } - PythonSession* pythonSession = static_cast(session()); - - pythonSession->runExpression(this); + session()->enqueueExpression(this); } QString PythonExpression::internalCommand() { QString cmd = command(); PythonSession* pythonSession = static_cast(session()); if((pythonSession->integratePlots()) && (command().contains(QLatin1String("show()")))){ m_tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_python-XXXXXX.png")); m_tempFile->open(); QString saveFigCommand = QLatin1String("savefig('%1')"); cmd.replace(QLatin1String("show()"), saveFigCommand.arg(m_tempFile->fileName())); QFileSystemWatcher* watcher = fileWatcher(); watcher->removePaths(watcher->files()); watcher->addPath(m_tempFile->fileName()); connect(watcher, &QFileSystemWatcher::fileChanged, this, &PythonExpression::imageChanged, Qt::UniqueConnection); } QStringList commandLine = cmd.split(QLatin1String("\n")); QString commandProcessing; for(const QString& command : commandLine){ const QString firstLineWord = command.trimmed().replace(QLatin1String("("), QLatin1String(" ")) .split(QLatin1String(" ")).at(0); // Ignore comments if (firstLineWord.length() != 0 && firstLineWord[0] == QLatin1Char('#')){ commandProcessing += command + QLatin1String("\n"); continue; } if(firstLineWord.contains(QLatin1String("execfile"))){ commandProcessing += command; continue; } commandProcessing += command + QLatin1String("\n"); } return commandProcessing; } void PythonExpression::parseOutput(QString output) { - qDebug() << "output: " << output; + qDebug() << "expression output: " << output; if(command().simplified().startsWith(QLatin1String("help("))){ setResult(new Cantor::HelpResult(output.remove(output.lastIndexOf(QLatin1String("None")), 4))); } else { if (!output.isEmpty()) - setResult(new Cantor::TextResult(output)); + addResult(new Cantor::TextResult(output)); } - setStatus(Cantor::Expression::Done); + if (m_tempFile == nullptr || result() != nullptr) // not plot expression + setStatus(Cantor::Expression::Done); } void PythonExpression::parseError(QString error) { - qDebug() << "error: " << error; + qDebug() << "expression error: " << error; setErrorMessage(error.replace(QLatin1String("\n"), QLatin1String("
"))); setStatus(Cantor::Expression::Error); } void PythonExpression::imageChanged() { - addResult(new Cantor::ImageResult(QUrl::fromLocalFile(m_tempFile->fileName()))); + if(m_tempFile->size() <= 0) + return; + + Cantor::ImageResult* newResult = new Cantor::ImageResult(QUrl::fromLocalFile(m_tempFile->fileName())); + if (result() == nullptr) + setResult(newResult); + else + { + bool found = false; + for (int i = 0; i < results().size(); i++) + if (results()[i]->type() == newResult->type()) + { + replaceResult(i, newResult); + found = true; + } + if (!found) + addResult(newResult); + } setStatus(Done); } void PythonExpression::interrupt() { qDebug()<<"interruptinging command"; setStatus(Cantor::Expression::Interrupted); } diff --git a/src/backends/python/pythonserver.cpp b/src/backends/python/pythonserver.cpp index fc7af615..3df158af 100644 --- a/src/backends/python/pythonserver.cpp +++ b/src/backends/python/pythonserver.cpp @@ -1,205 +1,203 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include "pythonserver.h" #include PythonServer::PythonServer(QObject* parent) : QObject(parent), m_pModule(nullptr) { } namespace { QString pyObjectToQString(PyObject* obj) { #if PY_MAJOR_VERSION == 3 return QString::fromUtf8(PyUnicode_AsUTF8(obj)); #elif PY_MAJOR_VERSION == 2 return QString::fromLocal8Bit(PyString_AsString(obj)); #else #warning Unknown Python version #endif } } void PythonServer::login() { Py_InspectFlag = 1; Py_Initialize(); m_pModule = PyImport_AddModule("__main__"); filePath = QStringLiteral("python_cantor_worksheet"); } void PythonServer::runPythonCommand(const QString& command) const { PyObject* py_dict = PyModule_GetDict(m_pModule); const char* prepareCommand = "import sys;\n"\ "class CatchOutPythonBackend:\n"\ " def __init__(self):\n"\ " self.value = ''\n"\ " def write(self, txt):\n"\ " self.value += txt\n"\ "outputPythonBackend = CatchOutPythonBackend()\n"\ "errorPythonBackend = CatchOutPythonBackend()\n"\ "sys.stdout = outputPythonBackend\n"\ "sys.stderr = errorPythonBackend\n"; PyRun_SimpleString(prepareCommand); #if PY_MAJOR_VERSION == 3 PyObject* compile = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_single_input); // There are two reasons for the error: // 1) This code is not single expression, so we can't compile this with flag Py_single_input // 2) There are errors in the code if (PyErr_Occurred()) { PyErr_Clear(); // Try to recompile code as sequence of expressions compile = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_file_input); if (PyErr_Occurred()) { // We now know, that we have a syntax error, so print the traceback and exit PyErr_PrintEx(0); return; } } PyEval_EvalCode(compile, py_dict, py_dict); #elif PY_MAJOR_VERSION == 2 // Python 2.X don't check, that input string contains only one expression. // So for checking this, we compile string as file and as single expression and compare bytecode // FIXME? PyObject* codeFile = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_file_input); if (PyErr_Occurred()) { PyErr_PrintEx(0); return; } PyObject* codeSingle = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_single_input); if (PyErr_Occurred()) { // We have error with Py_single_input, but haven't error with Py_file_input // So, the code can't be compiled as singel input -> use file input right away PyErr_Clear(); PyEval_EvalCode((PyCodeObject*)codeFile, py_dict, py_dict); } else { PyObject* bytecode1 = ((PyCodeObject*)codeSingle)->co_code; PyObject* bytecode2 = ((PyCodeObject*)codeFile)->co_code; if (PyObject_Length(bytecode1) >= PyObject_Length(bytecode2)) { PyEval_EvalCode((PyCodeObject*)codeSingle, py_dict, py_dict); } else { PyEval_EvalCode((PyCodeObject*)codeFile, py_dict, py_dict); } } #else #warning Unknown Python version #endif if (PyErr_Occurred()) PyErr_PrintEx(0); } QString PythonServer::getError() const { PyObject *errorPython = PyObject_GetAttrString(m_pModule, "errorPythonBackend"); PyObject *error = PyObject_GetAttrString(errorPython, "value"); return pyObjectToQString(error); } QString PythonServer::getOutput() const { PyObject *outputPython = PyObject_GetAttrString(m_pModule, "outputPythonBackend"); PyObject *output = PyObject_GetAttrString(outputPython, "value"); return pyObjectToQString(output); } void PythonServer::setFilePath(const QString& path) { this->filePath = path; PyRun_SimpleString(("__file__ = '"+path.toStdString()+"'").c_str()); } QString PythonServer::variables(bool parseValue) const { - // FIXME: This code allows get full form of numpy array, but for big arrays it's could cause performonce problems - // especially for displaying in variables panel - // So, uncomment this, when fix this problem - /* + PyRun_SimpleString( "try: \n" " import numpy \n" " __cantor_numpy_internal__ = numpy.get_printoptions()['threshold'] \n" " numpy.set_printoptions(threshold=100000000) \n" "except ModuleNotFoundError: \n" " pass \n" - - "try: \n" - " import numpy \n" - " numpy.set_printoptions(threshold=__cantor_numpy_internal__) \n" - " del __cantor_numpy_internal__ \n" - "except ModuleNotFoundError: \n" - " pass \n" - */ + ); PyRun_SimpleString("__tmp_globals__ = globals()"); PyObject* globals = PyObject_GetAttrString(m_pModule,"__tmp_globals__"); PyObject *key, *value; Py_ssize_t pos = 0; QStringList vars; - const QChar sep(30); // INFORMATION SEPARATOR TWO while (PyDict_Next(globals, &pos, &key, &value)) { const QString& keyString = pyObjectToQString(key); if (keyString.startsWith(QLatin1String("__"))) continue; if (keyString == QLatin1String("CatchOutPythonBackend") || keyString == QLatin1String("errorPythonBackend") || keyString == QLatin1String("outputPythonBackend")) continue; if (PyModule_Check(value)) continue; if (PyFunction_Check(value)) continue; if (PyType_Check(value)) continue; QString valueString; if (parseValue) valueString = pyObjectToQString(PyObject_Repr(value)); - vars.append(keyString + QChar(31) + valueString); + vars.append(keyString + QChar(17) + valueString); } - return vars.join(sep); + PyRun_SimpleString( + "try: \n" + " import numpy \n" + " numpy.set_printoptions(threshold=__cantor_numpy_internal__) \n" + " del __cantor_numpy_internal__ \n" + "except ModuleNotFoundError: \n" + " pass \n" + ); + + return vars.join(QChar(18))+QChar(18); } diff --git a/src/backends/python/pythonservermain.cpp b/src/backends/python/pythonservermain.cpp new file mode 100644 index 00000000..dbf16bf6 --- /dev/null +++ b/src/backends/python/pythonservermain.cpp @@ -0,0 +1,124 @@ +/* + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + + --- + Copyright (C) 2015 Minh Ngo + */ + +#include + +#include +#include +#include +#include + +#include "pythonserver.h" + +const QChar recordSep(30); +const QChar unitSep(31); +const char messageEnd = 29; + +PythonServer server; +QTimer inputTimer; +QMetaObject::Connection connection; + +QString inputBuffer; + +QLatin1String LOGIN("login"); +QLatin1String EXIT("exit"); +QLatin1String CODE("code"); +QLatin1String FILEPATH("setFilePath"); +QLatin1String MODEL("model"); + + +void routeInput() { + QByteArray bytes; + char c; + while (std::cin.get(c)) + { + if (messageEnd == c) + break; + else + bytes.append(c); + } + inputBuffer.append(QString::fromLocal8Bit(bytes)); + if (inputBuffer.isEmpty()) + return; + + const QStringList& records = inputBuffer.split(recordSep); + inputBuffer.clear(); + + if (records.size() == 2) + { + if (records[0] == EXIT) + { + QObject::disconnect(connection); + QObject::connect(&inputTimer, &QTimer::timeout, QCoreApplication::instance(), &QCoreApplication::quit); + } + else if (records[0] == LOGIN) + { + server.login(); + const QByteArray bytes = QString::fromLatin1("login done").toLocal8Bit(); + //std::cout << bytes.data(); + } + else if (records[0] == CODE) + { + server.runPythonCommand(records[1]); + + const QString& result = + server.getOutput() + + unitSep + + server.getError() + + QLatin1Char(messageEnd); + + const QByteArray bytes = result.toLocal8Bit(); + std::cout << bytes.data(); + } + else if (records[0] == FILEPATH) + { + server.setFilePath(records[1]); + } + else if (records[0] == MODEL) + { + bool ok; + bool val = records[1].toInt(&ok); + + QString result; + if (ok) + result = server.variables(val) + unitSep; + else + result = unitSep + QLatin1String("Invalid argument %1 for 'model' command", val); + result += QLatin1Char(messageEnd); + + const QByteArray bytes = result.toLocal8Bit(); + std::cout << bytes.data(); + } + std::cout.flush(); + } +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + connection = QObject::connect(&inputTimer, &QTimer::timeout, routeInput); + inputTimer.setInterval(100); + inputTimer.start(); + + std::cout << "ready" << std::endl; + + return app.exec(); +} diff --git a/src/backends/python/pythonsession.cpp b/src/backends/python/pythonsession.cpp index cdb3e89a..d9e681cf 100644 --- a/src/backends/python/pythonsession.cpp +++ b/src/backends/python/pythonsession.cpp @@ -1,361 +1,239 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2012 Filipe Saraiva Copyright (C) 2015 Minh Ngo */ +#include #include "pythonsession.h" #include "pythonexpression.h" #include "pythonvariablemodel.h" #include "pythonhighlighter.h" #include "pythoncompletionobject.h" #include "pythonkeywords.h" #include "pythonutils.h" #include #include - #include -#include -#include -#include - -#include +#include #include -#include +#ifndef Q_OS_WIN +#include +#endif -PythonSession::PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName, const QString DbusChannelName) + +const QChar recordSep(30); +const QChar unitSep(31); +const QChar messageEnd = 29; + +PythonSession::PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName) : Session(backend) - , m_variableModel(new PythonVariableModel(this)) - , m_currentExpression(nullptr) - , m_pIface(nullptr) - , m_pProcess(nullptr) + , m_process(nullptr) , serverName(serverName) - , DbusChannelName(DbusChannelName) , m_pythonVersion(pythonVersion) - , m_needUpdate(false) { + setVariableModel(new PythonVariableModel(this)); +} + +PythonSession::~PythonSession() +{ + if (m_process) { + m_process->kill(); + m_process->deleteLater(); + } } void PythonSession::login() { qDebug()<<"login"; emit loginStarted(); - // TODO: T6113, T6114 - if (m_pProcess) - m_pProcess->deleteLater(); - - m_pProcess = new KProcess(this); - m_pProcess->setOutputChannelMode(KProcess::SeparateChannels); + if (m_process) + m_process->deleteLater(); - (*m_pProcess) << QStandardPaths::findExecutable(serverName); + m_process = new QProcess(this); + m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel); - m_pProcess->start(); + m_process->start(QStandardPaths::findExecutable(serverName)); - m_pProcess->waitForStarted(); - m_pProcess->waitForReadyRead(); - QTextStream stream(m_pProcess->readAllStandardOutput()); + m_process->waitForStarted(); + m_process->waitForReadyRead(); + QTextStream stream(m_process->readAllStandardOutput()); const QString& readyStatus = QString::fromLatin1("ready"); - while (m_pProcess->state() == QProcess::Running) + while (m_process->state() == QProcess::Running) { const QString& rl = stream.readLine(); if (rl == readyStatus) break; } - if (!QDBusConnection::sessionBus().isConnected()) - { - qWarning() << "Can't connect to the D-Bus session bus.\n" - "To start it, run: eval `dbus-launch --auto-syntax`"; - return; - } - - const QString& serviceName = DbusChannelName + QString::fromLatin1("-%1").arg(m_pProcess->pid()); + connect(m_process, &QProcess::readyReadStandardOutput, this, &PythonSession::readOutput); - m_pIface = new QDBusInterface(serviceName, - QString::fromLatin1("/"), QString(), QDBusConnection::sessionBus()); - if (!m_pIface->isValid()) - { - qWarning() << QDBusConnection::sessionBus().lastError().message(); - return; - } - - m_variableModel->setPythonServer(m_pIface); - - m_pIface->call(QString::fromLatin1("login")); - m_pIface->call(QString::fromLatin1("setFilePath"), worksheetPath); + sendCommand(QLatin1String("login")); + sendCommand(QLatin1String("setFilePath"), QStringList(worksheetPath)); const QStringList& scripts = autorunScripts(); if(!scripts.isEmpty()){ QString autorunScripts = scripts.join(QLatin1String("\n")); - getPythonCommandOutput(autorunScripts); - m_variableModel->update(); + evaluateExpression(autorunScripts, Cantor::Expression::DeleteOnFinish, true); + variableModel()->update(); } const QString& importerFile = QLatin1String(":py/import_default_modules.py"); evaluateExpression(fromSource(importerFile), Cantor::Expression::DeleteOnFinish, true); + changeStatus(Session::Done); emit loginDone(); } void PythonSession::logout() { - // TODO: T6113, T6114 - m_pProcess->terminate(); + if (!m_process) + return; + + sendCommand(QLatin1String("exit")); + m_process = nullptr; - m_variableModel->clearVariables(); + variableModel()->clearVariables(); qDebug()<<"logout"; changeStatus(Status::Disable); } void PythonSession::interrupt() { - // TODO: T6113, T6114 - if (m_pProcess->pid()) - m_pProcess->kill(); - - qDebug()<<"interrupt"; - - foreach(Cantor::Expression* e, m_runningExpressions) - e->interrupt(); + if(!expressionQueue().isEmpty()) + { + qDebug()<<"interrupting " << expressionQueue().first()->command(); + if(m_process->state() != QProcess::NotRunning) + { +#ifndef Q_OS_WIN + const int pid=m_process->pid(); + kill(pid, SIGINT); +#else + ; //TODO: interrupt the process on windows +#endif + } + for (Cantor::Expression* expression : expressionQueue()) + expression->setStatus(Cantor::Expression::Interrupted); + expressionQueue().clear(); + + // Cleanup inner state and call octave prompt printing + // If we move this code for interruption to Session, we need add function for + // cleaning before setting Done status + m_output.clear(); + m_process->write("\n"); + + qDebug()<<"done interrupting"; + } - m_runningExpressions.clear(); changeStatus(Cantor::Session::Done); } Cantor::Expression* PythonSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal) { qDebug() << "evaluating: " << cmd; PythonExpression* expr = new PythonExpression(this, internal); changeStatus(Cantor::Session::Running); expr->setFinishingBehavior(behave); expr->setCommand(cmd); expr->evaluate(); return expr; } -void PythonSession::runExpression(PythonExpression* expr) -{ - qDebug() << "run expression"; - - m_currentExpression = expr; - - const QString& command = expr->internalCommand(); - - readExpressionOutput(command); -} - -// Is called asynchronously in the Python3 plugin -void PythonSession::readExpressionOutput(const QString& commandProcessing) -{ - readOutput(commandProcessing); -} - -void PythonSession::getPythonCommandOutput(const QString& commandProcessing) -{ - runPythonCommand(commandProcessing); - - m_output = getOutput(); - m_error = getError(); -} - -bool PythonSession::identifyKeywords(const QString& command) -{ - QString verifyErrorImport; - - QString listKeywords; - QString keywordsString; - - QString moduleImported; - QString moduleVariable; - - getPythonCommandOutput(command); - - qDebug() << "verifyErrorImport: "; - - if(!m_error.isEmpty()){ - - qDebug() << "returned false"; - - return false; - } - - moduleImported += identifyPythonModule(command); - moduleVariable += identifyVariableModule(command); - - if((moduleVariable.isEmpty()) && (!command.endsWith(QLatin1String("*")))){ - keywordsString = command.section(QLatin1String(" "), 3).remove(QLatin1String(" ")); - } - - if(moduleVariable.isEmpty() && (command.endsWith(QLatin1String("*")))){ - listKeywords += QString::fromLatin1("import %1\n" \ - "print(dir(%1))\n" \ - "del %1\n").arg(moduleImported); - } - - if(!moduleVariable.isEmpty()){ - listKeywords += QLatin1String("print(dir(") + moduleVariable + QLatin1String("))\n"); - } - - if(!listKeywords.isEmpty()){ - getPythonCommandOutput(listKeywords); - - keywordsString = m_output; - - keywordsString.remove(QLatin1String("'")); - keywordsString.remove(QLatin1String(" ")); - keywordsString.remove(QLatin1String("[")); - keywordsString.remove(QLatin1String("]")); - } - - QStringList keywordsList = keywordsString.split(QLatin1String(",")); - PythonKeywords::instance()->loadFromModule(moduleVariable, keywordsList); - - qDebug() << "Module imported" << moduleImported; - - return true; -} - -QString PythonSession::identifyPythonModule(const QString& command) const -{ - QString module; - - if(command.contains(QLatin1String("import "))){ - module = command.section(QLatin1String(" "), 1, 1); - } - - qDebug() << "module identified" << module; - return module; -} - -QString PythonSession::identifyVariableModule(const QString& command) const +QSyntaxHighlighter* PythonSession::syntaxHighlighter(QObject* parent) { - QString variable; - - if(command.contains(QLatin1String("import "))){ - variable = command.section(QLatin1String(" "), 1, 1); - } - - if((command.contains(QLatin1String("import "))) && (command.contains(QLatin1String(" as ")))){ - variable = command.section(QLatin1String(" "), 3, 3); - } - - if(command.contains(QLatin1String("from "))){ - variable = QLatin1String(""); - } - - qDebug() << "variable identified" << variable; - return variable; + return new PythonHighlighter(parent, this, m_pythonVersion); } -void PythonSession::expressionFinished() +Cantor::CompletionObject* PythonSession::completionFor(const QString& command, int index) { - qDebug()<< "finished"; - PythonExpression* expression = qobject_cast(sender()); - - m_runningExpressions.removeAll(expression); - qDebug() << "size: " << m_runningExpressions.size(); + return new PythonCompletionObject(command, index, this); } -void PythonSession::updateOutput() +void PythonSession::runFirstExpression() { - m_needUpdate |= !m_currentExpression->isInternal(); - if(m_error.isEmpty()){ - m_currentExpression->parseOutput(m_output); - - qDebug() << "output: " << m_output; - } else { - m_currentExpression->parseError(m_error); + if (expressionQueue().isEmpty()) + return; - qDebug() << "error: " << m_error; - } + Cantor::Expression* expr = expressionQueue().first(); + const QString command = expr->internalCommand(); + qDebug() << "run first expression" << command; + expr->setStatus(Cantor::Expression::Computing); - if (m_needUpdate) + if (expr->isInternal() && command.startsWith(QLatin1String("%variables "))) { - m_variableModel->update(); - m_needUpdate = false; + const QString arg = command.section(QLatin1String(" "), 1); + sendCommand(QLatin1String("model"), QStringList(arg)); } - - changeStatus(Cantor::Session::Done); + else + sendCommand(QLatin1String("code"), QStringList(expr->internalCommand())); } -void PythonSession::readOutput(const QString& commandProcessing) +void PythonSession::sendCommand(const QString& command, const QStringList arguments) const { - qDebug() << "readOutput"; - - getPythonCommandOutput(commandProcessing); - - updateOutput(); + qDebug() << "send command: " << command << arguments; + const QString& message = command + recordSep + arguments.join(unitSep) + messageEnd; + m_process->write(message.toLocal8Bit()); } -QSyntaxHighlighter* PythonSession::syntaxHighlighter(QObject* parent) +void PythonSession::readOutput() { - return new PythonHighlighter(parent, this, m_pythonVersion); -} + while (m_process->bytesAvailable() > 0) + m_output.append(QString::fromLocal8Bit(m_process->readAll())); -Cantor::CompletionObject* PythonSession::completionFor(const QString& command, int index) -{ - return new PythonCompletionObject(command, index, this); -} + qDebug() << "m_output: " << m_output; -Cantor::DefaultVariableModel* PythonSession::variableModel() const -{ - return m_variableModel; -} - -void PythonSession::runPythonCommand(const QString& command) const -{ - // TODO: T6113, T6114 - m_pIface->call(QString::fromLatin1("runPythonCommand"), command); -} - -QString PythonSession::getOutput() const -{ - // TODO: T6113, T6114 - const QDBusReply& reply = m_pIface->call(QString::fromLatin1("getOutput")); - if (reply.isValid()) - return reply.value(); - - return reply.error().message(); -} + if (!m_output.contains(messageEnd)) + return; + const QStringList packages = m_output.split(messageEnd, QString::SkipEmptyParts); + if (m_output.endsWith(messageEnd)) + m_output.clear(); + else + m_output = m_output.section(messageEnd, -1); -QString PythonSession::getError() const -{ - // TODO: T6113, T6114 - const QDBusReply& reply = m_pIface->call(QString::fromLatin1("getError")); - if (reply.isValid()) - return reply.value(); + for (const QString& message: packages) + { + if (expressionQueue().isEmpty()) + break; - return reply.error().message(); + const QString& output = message.section(unitSep, 0, 0); + const QString& error = message.section(unitSep, 1, 1); + if(error.isEmpty()){ + static_cast(expressionQueue().first())->parseOutput(output); + } else { + static_cast(expressionQueue().first())->parseError(error); + } + finishFirstExpression(true); + } } void PythonSession::setWorksheetPath(const QString& path) { worksheetPath = path; } diff --git a/src/backends/python/pythonsession.h b/src/backends/python/pythonsession.h index aafc2b8b..0e93dcf1 100644 --- a/src/backends/python/pythonsession.h +++ b/src/backends/python/pythonsession.h @@ -1,103 +1,68 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2012 Filipe Saraiva Copyright (C) 2015 Minh Ngo */ #ifndef _PYTHONSESSION_H #define _PYTHONSESSION_H #include "session.h" #include #include -namespace Cantor { - class DefaultVariableModel; -} - -class PythonExpression; -class PythonVariableModel; -class KDirWatch; -class QDBusInterface; -class KProcess; +class QProcess; class CANTOR_PYTHONBACKEND_EXPORT PythonSession : public Cantor::Session { Q_OBJECT public: - PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName, const QString DbusChannelName); - ~PythonSession() override = default; + PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName); + ~PythonSession() override; void login() override; void logout() override; void interrupt() override; - void runExpression(PythonExpression* expr); Cantor::Expression* evaluateExpression(const QString& command, Cantor::Expression::FinishingBehavior behave = Cantor::Expression::FinishingBehavior::DoNotDelete, bool internal = false) override; Cantor::CompletionObject* completionFor(const QString& command, int index=-1) override; QSyntaxHighlighter* syntaxHighlighter(QObject* parent) override; - Cantor::DefaultVariableModel* variableModel() const override; void setWorksheetPath(const QString& path) override; virtual bool integratePlots() const = 0; virtual QStringList autorunScripts() const = 0; virtual bool variableManagement() const = 0; private: - PythonVariableModel* m_variableModel; - - QList m_runningExpressions; - PythonExpression* m_currentExpression; - - QDBusInterface* m_pIface; - KProcess* m_pProcess; + QProcess* m_process; QString serverName; - QString DbusChannelName; QString worksheetPath; int m_pythonVersion; - bool m_needUpdate; - - protected: QString m_output; - QString m_error; private: - void getPythonCommandOutput(const QString& commandProcessing); - - QString identifyPythonModule(const QString& command) const; - QString identifyVariableModule(const QString& command) const; - bool identifyKeywords(const QString& command); - - void runPythonCommand(const QString& command) const; - QString getOutput() const; - QString getError() const; - - virtual void readExpressionOutput(const QString& commandProcessing); - - protected: - void updateOutput(); + void runFirstExpression() override; + void readOutput(); - private Q_SLOTS: - void readOutput(const QString& commandProcessing); - void expressionFinished(); + void sendCommand(const QString& command, const QStringList arguments = QStringList()) const; }; #endif /* _PYTHONSESSION_H */ diff --git a/src/backends/python/pythonvariablemodel.cpp b/src/backends/python/pythonvariablemodel.cpp index cb934a33..b63bad59 100644 --- a/src/backends/python/pythonvariablemodel.cpp +++ b/src/backends/python/pythonvariablemodel.cpp @@ -1,62 +1,93 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2018 Nikita Sirgienko */ #include "pythonvariablemodel.h" #include "pythonsession.h" #include "textresult.h" #include #include #include #include using namespace Cantor; PythonVariableModel::PythonVariableModel(PythonSession* session): - DefaultVariableModel(session), - m_pIface(nullptr) + DefaultVariableModel(session) { } -void PythonVariableModel::setPythonServer(QDBusInterface* pIface) +PythonVariableModel::~PythonVariableModel() { - m_pIface = pIface; + if (m_expression) + m_expression->setFinishingBehavior(Cantor::Expression::FinishingBehavior::DeleteOnFinish); } void PythonVariableModel::update() { - if (!m_pIface) + if (m_expression) return; - bool variableManagement = static_cast(session())->variableManagement(); - const QString& data = QDBusReply(m_pIface->call(QString::fromLatin1("variables"), variableManagement)).value(); - const QStringList& records = data.split(QChar(30), QString::SkipEmptyParts); + int variableManagement = static_cast(session())->variableManagement(); + const QString command = QString::fromLatin1("%variables %1").arg(variableManagement); + m_expression = session()->evaluateExpression(command, Cantor::Expression::FinishingBehavior::DoNotDelete, true); + connect(m_expression, &Cantor::Expression::statusChanged, this, &PythonVariableModel::extractVariables); +} - QList variables; - for (const QString& record : records) +void PythonVariableModel::extractVariables(Cantor::Expression::Status status) +{ + switch(status) { - const QString& name = record.section(QChar(31), 0, 0); - const QString& value = record.section(QChar(31), 1, 1); + case Cantor::Expression::Done: + { + Cantor::Result* result = m_expression->result(); + if (result) + { + const QString data = result->data().toString(); + // In Cantor server response DC2(18) is delimiter between variables + const QStringList& records = data.split(QChar(18), QString::SkipEmptyParts); + + QList variables; + for (const QString& record : records) + { + // DC1(17) is delimiter between variable name and its value. + const QString& name = record.section(QChar(17), 0, 0); + const QString& value = record.section(QChar(17), 1, 1); + + variables << Variable{name, value}; + } - variables << Variable{name, value}; + setVariables(variables); + } + break; + } + case Cantor::Expression::Interrupted: + case Cantor::Expression::Error: + { + qDebug() << "python variable model update finished with status" << (status == Cantor::Expression::Error? "Error" : "Interrupted"); + break; + } + default: + return; } - setVariables(variables); + m_expression->deleteLater(); + m_expression=nullptr; } diff --git a/src/backends/python/pythonvariablemodel.h b/src/backends/python/pythonvariablemodel.h index 0dab8839..40851468 100644 --- a/src/backends/python/pythonvariablemodel.h +++ b/src/backends/python/pythonvariablemodel.h @@ -1,43 +1,44 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2019 Nikita Sirgienko */ #ifndef _PYTHONVARIABLEMODEL_H #define _PYTHONVARIABLEMODEL_H #include "defaultvariablemodel.h" class PythonSession; class QDBusInterface; class PythonVariableModel : public Cantor::DefaultVariableModel { public: PythonVariableModel( PythonSession* session); - ~PythonVariableModel() override = default; + ~PythonVariableModel() override; void update() override; - void setPythonServer(QDBusInterface* pIface); - private: - QDBusInterface* m_pIface; + Cantor::Expression* m_expression{nullptr}; + + private Q_SLOTS: + void extractVariables(Cantor::Expression::Status status); }; #endif /* _PYTHONVARIABLEMODEL_H */ diff --git a/src/backends/python2/CMakeLists.txt b/src/backends/python2/CMakeLists.txt index adc06197..7bfa1f38 100644 --- a/src/backends/python2/CMakeLists.txt +++ b/src/backends/python2/CMakeLists.txt @@ -1,32 +1,36 @@ set( Python2Backend_SRCS python2backend.cpp python2session.cpp ) +set(Python2Server_SRCS + ../python/pythonservermain.cpp + ../python/pythonserver.cpp +) + kconfig_add_kcfg_files(Python2Backend_SRCS settings.kcfgc) if(MSVC) # ssize_t is typedef'd in both kdewin and python headers, this prevents using the kdewin one add_definitions(-D_SSIZE_T_DEFINED) endif(MSVC) include_directories(${PYTHON_LIBRARIES_DIR}) include_directories(${PYTHON_INCLUDE_DIR}) - add_backend(python2backend ${Python2Backend_SRCS}) +target_link_libraries(cantor_python2backend ${PYTHON_LIBRARIES} cantor_pythonbackend) -target_link_libraries(cantor_python2backend - ${PYTHON_LIBRARIES} - cantor_pythonbackend -) +add_executable(cantor_python2server ${Python2Server_SRCS}) +set_target_properties(cantor_python2server PROPERTIES INSTALL_RPATH_USE_LINK_PATH false) +target_link_libraries(cantor_python2server ${PYTHON_LIBRARIES} Qt5::Widgets) if(BUILD_TESTING) add_executable(testpython2 testpython2.cpp settings.cpp) target_link_libraries(testpython2 ${QT_QTTEST_LIBRARY} cantorlibs cantortest) add_test(NAME testpython2 COMMAND testpython2) endif() install(FILES cantor_python2.knsrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(FILES python2backend.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) -add_subdirectory(python2server) +install(TARGETS cantor_python2server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/backends/python2/python2server/CMakeLists.txt b/src/backends/python2/python2server/CMakeLists.txt deleted file mode 100644 index edfd2c52..00000000 --- a/src/backends/python2/python2server/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -include_directories(${PYTHON_INCLUDE_DIR}) - -set(Python2Server_SRCS - main.cpp - ../../python/pythonserver.cpp -) - -add_executable(cantor_python2server ${Python2Server_SRCS}) - -set_target_properties(cantor_python2server PROPERTIES INSTALL_RPATH_USE_LINK_PATH false) -target_link_libraries(cantor_python2server - ${PYTHON_LIBRARIES} - Qt5::Widgets - Qt5::DBus) - -install(TARGETS cantor_python2server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/backends/python2/python2server/main.cpp b/src/backends/python2/python2server/main.cpp deleted file mode 100644 index 82945d16..00000000 --- a/src/backends/python2/python2server/main.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/* - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. - - --- - Copyright (C) 2015 Minh Ngo - */ - -#include -#include -#include -#include -#include - -#include "../../python/pythonserver.h" - -int main(int argc, char *argv[]) -{ - QCoreApplication app(argc, argv); - - if (!QDBusConnection::sessionBus().isConnected()) - { - qDebug() << "Can't connect to the D-Bus session bus.\n" - "To start it, run: eval `dbus-launch --auto-syntax`"; - return 1; - } - - const QString& serviceName = QStringLiteral("org.kde.Cantor.Python2-%1").arg(app.applicationPid()); - - if (!QDBusConnection::sessionBus().registerService(serviceName)) - { - qDebug() << QDBusConnection::sessionBus().lastError().message(); - return 2; - } - - PythonServer server; - QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), &server, QDBusConnection::ExportAllSlots); - - QTextStream(stdout) << "ready" << endl; - - return app.exec(); -} diff --git a/src/backends/python2/python2session.cpp b/src/backends/python2/python2session.cpp index 7ed92280..b220cafa 100644 --- a/src/backends/python2/python2session.cpp +++ b/src/backends/python2/python2session.cpp @@ -1,51 +1,51 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include "python2session.h" #include "settings.h" #include "../python/pythonexpression.h" #include "../python/pythonkeywords.h" #include #include #include #include #include Python2Session::Python2Session(Cantor::Backend* backend) - : PythonSession(backend, 2, QLatin1String("cantor_python2server"), QLatin1String("org.kde.Cantor.Python2")) + : PythonSession(backend, 2, QLatin1String("cantor_python2server")) { } bool Python2Session::integratePlots() const { return PythonSettings::integratePlots(); } QStringList Python2Session::autorunScripts() const { return PythonSettings::autorunScripts(); } bool Python2Session::variableManagement() const { return PythonSettings::variableManagement(); } diff --git a/src/backends/python2/testpython2.cpp b/src/backends/python2/testpython2.cpp index 5406c02b..51b4f9df 100644 --- a/src/backends/python2/testpython2.cpp +++ b/src/backends/python2/testpython2.cpp @@ -1,270 +1,279 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2013 Tuukka Verho */ #include "testpython2.h" #include "session.h" #include "backend.h" #include "expression.h" #include "result.h" #include "defaultvariablemodel.h" #include "imageresult.h" #include "completionobject.h" #include "settings.h" QString TestPython2::backendName() { return QLatin1String("python2"); } void TestPython2::testCommandQueue() { Cantor::Expression* e1=session()->evaluateExpression(QLatin1String("0+1")); Cantor::Expression* e2=session()->evaluateExpression(QLatin1String("1+1")); Cantor::Expression* e3=evalExp(QLatin1String("1+2")); QVERIFY(e1!=nullptr); QVERIFY(e2!=nullptr); QVERIFY(e3!=nullptr); QVERIFY(e1->result()); QVERIFY(e2->result()); QVERIFY(e3->result()); QCOMPARE(cleanOutput(e1->result()->data().toString()), QLatin1String("1")); QCOMPARE(cleanOutput(e2->result()->data().toString()), QLatin1String("2")); QCOMPARE(cleanOutput(e3->result()->data().toString()), QLatin1String("3")); } void TestPython2::testImportNumpy() { Cantor::Expression* e = evalExp(QLatin1String("import numpy")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Done); } void TestPython2::testCodeWithComments() { { Cantor::Expression* e = evalExp(QLatin1String("#comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } { Cantor::Expression* e = evalExp(QLatin1String(" #comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } } void TestPython2::testSimpleCode() { Cantor::Expression* e=evalExp( QLatin1String("2+2")); QVERIFY( e!=nullptr ); QVERIFY( e->result()!=nullptr ); QCOMPARE( cleanOutput(e->result()->data().toString()), QLatin1String("4") ); } void TestPython2::testMultilineCode() { Cantor::Expression* e=evalExp(QLatin1String( "a = 2+2\n" "b = 3+3\n" "print a,b" )); QVERIFY( e!=nullptr ); QVERIFY( e->result()!=nullptr ); QCOMPARE( cleanOutput(e->result()->data().toString()), QLatin1String("4 6") ); evalExp(QLatin1String("del a; del b")); } void TestPython2::testVariablesCreatingFromCode() { QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, model->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("a")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("15")); QCOMPARE(model->index(1,0).data().toString(), QLatin1String("b")); QCOMPARE(model->index(1,1).data().toString(), QLatin1String("'S'")); evalExp(QLatin1String("del a; del b")); } void TestPython2::testVariableCleanupAfterRestart() { Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, static_cast(model)->rowCount()); session()->logout(); session()->login(); QCOMPARE(0, static_cast(model)->rowCount()); } void TestPython2::testDictVariable() { Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("d = {'value': 33}")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(1, static_cast(model)->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("d")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("{'value': 33}")); evalExp(QLatin1String("del d")); } void TestPython2::testCommentExpression() { Cantor::Expression* e = evalExp(QLatin1String("#only comment")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Status::Done); QCOMPARE(e->results().size(), 0); } void TestPython2::testInvalidSyntax() { Cantor::Expression* e=evalExp( QLatin1String("2+2*+.") ); QVERIFY( e!=nullptr ); QCOMPARE( e->status(), Cantor::Expression::Error ); } void TestPython2::testSimplePlot() { if (!PythonSettings::integratePlots()) QSKIP("This test needs enabled plots integration in Python2 settings", SkipSingle); Cantor::Expression* e = evalExp(QLatin1String( "import matplotlib\n" "import matplotlib.pyplot as plt\n" "import numpy as np" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); //the variable model shouldn't have any entries after the module imports QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); + if(session()->status()==Cantor::Session::Running) + waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QVERIFY(model->rowCount() == 0); //create data for plotting e = evalExp(QLatin1String( "t = np.arange(0.0, 2.0, 0.01)\n" "s = 1 + np.sin(2 * np.pi * t)" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); + if(session()->status()==Cantor::Session::Running) + waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); //the variable model should have two entries now QVERIFY(model->rowCount() == 2); //plot the data and check the results e = evalExp(QLatin1String( "plt.plot(t,s)\n" "plt.show()" )); - waitForSignal(e, SIGNAL(gotResult())); + QVERIFY(e != nullptr); + if (e->result() == nullptr) + waitForSignal(e, SIGNAL(gotResult())); QVERIFY(e->errorMessage().isEmpty() == true); QVERIFY(model->rowCount() == 2); //still only two variables //there must be one single image result QVERIFY(e->results().size() == 1); const Cantor::ImageResult* result = dynamic_cast(e->result()); QVERIFY(result != nullptr); evalExp(QLatin1String("del t; del s")); } void TestPython2::testSimpleExpressionWithComment() { Cantor::Expression* e = evalExp(QLatin1String("2+2 # comment")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4")); } void TestPython2::testMultilineCommandWithComment() { Cantor::Expression* e = evalExp(QLatin1String( "print 2+2 \n" "#comment in middle \n" "print 7*5")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4\n35")); } void TestPython2::testCompletion() { + if(session()->status()==Cantor::Session::Running) + waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); + Cantor::CompletionObject* help = session()->completionFor(QLatin1String("ma"), 2); waitForSignal(help, SIGNAL(fetchingDone())); // Checks all completions for this request // This correct for Python 2.7.15 const QStringList& completions = help->completions(); qDebug() << completions; QCOMPARE(completions.size(), 2); QVERIFY(completions.contains(QLatin1String("map"))); QVERIFY(completions.contains(QLatin1String("max"))); } QTEST_MAIN(TestPython2) diff --git a/src/backends/python3/CMakeLists.txt b/src/backends/python3/CMakeLists.txt index b7f9f629..52b3b6ab 100644 --- a/src/backends/python3/CMakeLists.txt +++ b/src/backends/python3/CMakeLists.txt @@ -1,29 +1,35 @@ set(Python3Backend_SRCS python3backend.cpp python3session.cpp ) -kconfig_add_kcfg_files(Python3Backend_SRCS settings.kcfgc) +set(Python3Server_SRCS + ../python/pythonservermain.cpp + ../python/pythonserver.cpp +) +include_directories(${PYTHONLIBS3_INCLUDE_DIRS}) +kconfig_add_kcfg_files(Python3Backend_SRCS settings.kcfgc) add_backend(python3backend ${Python3Backend_SRCS}) +target_link_libraries(cantor_python3backend cantor_pythonbackend) -target_link_libraries(cantor_python3backend - cantor_pythonbackend - Qt5::DBus) - -add_subdirectory(python3server) +add_executable(cantor_python3server ${Python3Server_SRCS}) +set_target_properties(cantor_python3server PROPERTIES INSTALL_RPATH_USE_LINK_PATH false) +target_link_libraries(cantor_python3server ${PYTHONLIBS3_LIBRARIES} Qt5::Widgets) if(BUILD_TESTING) add_executable(testpython3 testpython3.cpp settings.cpp) add_test(NAME testpython3 COMMAND testpython3) target_link_libraries(testpython3 Qt5::Test KF5::ConfigCore KF5::ConfigGui cantorlibs cantortest ) endif(BUILD_TESTING) install(FILES cantor_python3.knsrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(FILES python3backend.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) + +install(TARGETS cantor_python3server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/backends/python3/python3server/CMakeLists.txt b/src/backends/python3/python3server/CMakeLists.txt deleted file mode 100644 index c4ab90fa..00000000 --- a/src/backends/python3/python3server/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -include_directories(${PYTHONLIBS3_INCLUDE_DIRS}) - -set(Python3Server_SRCS - main.cpp - ../../python/pythonserver.cpp -) - -add_executable(cantor_python3server ${Python3Server_SRCS}) - -set_target_properties(cantor_python3server PROPERTIES INSTALL_RPATH_USE_LINK_PATH false) -target_link_libraries(cantor_python3server - ${PYTHONLIBS3_LIBRARIES} - Qt5::Widgets - Qt5::DBus) - -install(TARGETS cantor_python3server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/backends/python3/python3server/main.cpp b/src/backends/python3/python3server/main.cpp deleted file mode 100644 index f396cd0f..00000000 --- a/src/backends/python3/python3server/main.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/* - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. - - --- - Copyright (C) 2015 Minh Ngo - */ - -#include -#include -#include -#include -#include - -#include "../../python/pythonserver.h" - -int main(int argc, char *argv[]) -{ - QCoreApplication app(argc, argv); - - if (!QDBusConnection::sessionBus().isConnected()) - { - qDebug() << "Can't connect to the D-Bus session bus.\n" - "To start it, run: eval `dbus-launch --auto-syntax`"; - return 1; - } - - const QString& serviceName = QStringLiteral("org.kde.Cantor.Python3-%1").arg(app.applicationPid()); - - if (!QDBusConnection::sessionBus().registerService(serviceName)) - { - qDebug() << QDBusConnection::sessionBus().lastError().message(); - return 2; - } - - PythonServer server; - QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), &server, QDBusConnection::ExportAllSlots); - - QTextStream(stdout) << "ready" << endl; - - return app.exec(); -} diff --git a/src/backends/python3/python3session.cpp b/src/backends/python3/python3session.cpp index 81366d00..840a795e 100644 --- a/src/backends/python3/python3session.cpp +++ b/src/backends/python3/python3session.cpp @@ -1,43 +1,43 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include "python3session.h" #include "settings.h" Python3Session::Python3Session(Cantor::Backend* backend) - : PythonSession(backend, 3, QLatin1String("cantor_python3server"), QLatin1String("org.kde.Cantor.Python3")) + : PythonSession(backend, 3, QLatin1String("cantor_python3server")) { } bool Python3Session::integratePlots() const { return PythonSettings::integratePlots(); } QStringList Python3Session::autorunScripts() const { return PythonSettings::autorunScripts(); } bool Python3Session::variableManagement() const { return PythonSettings::variableManagement(); } diff --git a/src/backends/python3/testpython3.cpp b/src/backends/python3/testpython3.cpp index 5ec3194b..8d19e133 100644 --- a/src/backends/python3/testpython3.cpp +++ b/src/backends/python3/testpython3.cpp @@ -1,282 +1,290 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include "testpython3.h" #include "session.h" #include "backend.h" #include "expression.h" #include "imageresult.h" #include "defaultvariablemodel.h" #include "completionobject.h" #include "settings.h" QString TestPython3::backendName() { return QLatin1String("python3"); } void TestPython3::testSimpleCommand() { Cantor::Expression* e = evalExp(QLatin1String("2+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4")); } void TestPython3::testMultilineCommand() { Cantor::Expression* e = evalExp(QLatin1String("print(2+2)\nprint(7*5)")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4\n35")); } void TestPython3::testCommandQueue() { Cantor::Expression* e1=session()->evaluateExpression(QLatin1String("0+1")); Cantor::Expression* e2=session()->evaluateExpression(QLatin1String("1+1")); Cantor::Expression* e3=evalExp(QLatin1String("1+2")); QVERIFY(e1!=nullptr); QVERIFY(e2!=nullptr); QVERIFY(e3!=nullptr); QVERIFY(e1->result()); QVERIFY(e2->result()); QVERIFY(e3->result()); QCOMPARE(cleanOutput(e1->result()->data().toString()), QLatin1String("1")); QCOMPARE(cleanOutput(e2->result()->data().toString()), QLatin1String("2")); QCOMPARE(cleanOutput(e3->result()->data().toString()), QLatin1String("3")); } void TestPython3::testCommentExpression() { Cantor::Expression* e = evalExp(QLatin1String("#only comment")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Status::Done); QCOMPARE(e->results().size(), 0); } void TestPython3::testSimpleExpressionWithComment() { Cantor::Expression* e = evalExp(QLatin1String("2+2 # comment")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4")); } void TestPython3::testMultilineCommandWithComment() { Cantor::Expression* e = evalExp(QLatin1String( "print(2+2) \n" "#comment in middle \n" "print(7*5)")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4\n35")); } void TestPython3::testInvalidSyntax() { Cantor::Expression* e=evalExp( QLatin1String("2+2*+.") ); QVERIFY( e!=nullptr ); QCOMPARE( e->status(), Cantor::Expression::Error ); } void TestPython3::testCompletion() { + if(session()->status()==Cantor::Session::Running) + waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); + Cantor::CompletionObject* help = session()->completionFor(QLatin1String("p"), 1); waitForSignal(help, SIGNAL(fetchingDone())); // Checks all completions for this request // This correct for Python 3.6.7 const QStringList& completions = help->completions(); qDebug() << completions; QCOMPARE(completions.size(), 4); QVERIFY(completions.contains(QLatin1String("pass"))); QVERIFY(completions.contains(QLatin1String("pow"))); QVERIFY(completions.contains(QLatin1String("print"))); QVERIFY(completions.contains(QLatin1String("property"))); } void TestPython3::testImportNumpy() { Cantor::Expression* e = evalExp(QLatin1String("import numpy")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Done); } void TestPython3::testCodeWithComments() { { Cantor::Expression* e = evalExp(QLatin1String("#comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } { Cantor::Expression* e = evalExp(QLatin1String(" #comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } } void TestPython3::testPython3Code() { { Cantor::Expression* e = evalExp(QLatin1String("print 1 + 2")); QVERIFY(e != nullptr); QVERIFY(!e->errorMessage().isEmpty()); } { Cantor::Expression* e = evalExp(QLatin1String("print(1 + 2)")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } } void TestPython3::testSimplePlot() { if (!PythonSettings::integratePlots()) QSKIP("This test needs enabled plots integration in Python3 settings", SkipSingle); Cantor::Expression* e = evalExp(QLatin1String( "import matplotlib\n" "import matplotlib.pyplot as plt\n" "import numpy as np" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); //the variable model shouldn't have any entries after the module imports QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); QVERIFY(model->rowCount() == 0); //create data for plotting e = evalExp(QLatin1String( "t = np.arange(0.0, 2.0, 0.01)\n" "s = 1 + np.sin(2 * np.pi * t)" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); + if(session()->status()==Cantor::Session::Running) + waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); //the variable model should have two entries now QVERIFY(model->rowCount() == 2); //plot the data and check the results e = evalExp(QLatin1String( "plt.plot(t,s)\n" "plt.show()" )); - waitForSignal(e, SIGNAL(gotResult())); + QVERIFY(e != nullptr); + if (e->result() == nullptr) + waitForSignal(e, SIGNAL(gotResult())); + QVERIFY(e->errorMessage().isEmpty() == true); QVERIFY(model->rowCount() == 2); //still only two variables //there must be one single image result QVERIFY(e->results().size() == 1); const Cantor::ImageResult* result = dynamic_cast(e->result()); QVERIFY(result != nullptr); evalExp(QLatin1String("del t; del s")); } void TestPython3::testVariablesCreatingFromCode() { QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, model->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("a")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("15")); QCOMPARE(model->index(1,0).data().toString(), QLatin1String("b")); QCOMPARE(model->index(1,1).data().toString(), QLatin1String("'S'")); evalExp(QLatin1String("del a; del b")); } void TestPython3::testVariableCleanupAfterRestart() { Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, static_cast(model)->rowCount()); session()->logout(); session()->login(); QCOMPARE(0, static_cast(model)->rowCount()); } void TestPython3::testDictVariable() { Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("d = {'value': 33}")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(1, static_cast(model)->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("d")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("{'value': 33}")); evalExp(QLatin1String("del d")); } QTEST_MAIN(TestPython3)