diff --git a/src/backends/python/pythonexpression.cpp b/src/backends/python/pythonexpression.cpp index a89ba7ca..73b7e778 100644 --- a/src/backends/python/pythonexpression.cpp +++ b/src/backends/python/pythonexpression.cpp @@ -1,159 +1,163 @@ /* 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; } 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() << "expression output: " << output; if(command().simplified().startsWith(QLatin1String("help("))){ setResult(new Cantor::HelpResult(output.remove(output.lastIndexOf(QLatin1String("None")), 4))); } else { if (!output.isEmpty()) addResult(new Cantor::TextResult(output)); } if (m_tempFile == nullptr || result() != nullptr) // not plot expression setStatus(Cantor::Expression::Done); } void PythonExpression::parseError(QString error) { qDebug() << "expression error: " << error; setErrorMessage(error); setStatus(Cantor::Expression::Error); } void PythonExpression::parseWarning(QString warning) { if (!warning.isEmpty()) - addResult(new Cantor::TextResult(warning)); + { + Cantor::TextResult* result = new Cantor::TextResult(warning); + result->setStdErr(true); + addResult(result); + } } void PythonExpression::imageChanged() { 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/lib/textresult.cpp b/src/lib/textresult.cpp index e9e05304..6b3a0d5c 100644 --- a/src/lib/textresult.cpp +++ b/src/lib/textresult.cpp @@ -1,217 +1,232 @@ /* 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) 2009 Alexander Rieder */ #include "textresult.h" using namespace Cantor; #include #include #include #include #include QString rtrim(const QString& s) { QString result = s; while (result.count() > 0 && result[result.count()-1].isSpace() ) { result = result.left(result.count() -1 ); } return result; } class Cantor::TextResultPrivate { public: TextResultPrivate() { format=TextResult::PlainTextFormat; } QString data; QString plain; TextResult::Format format; + bool isStderr; }; TextResult::TextResult(const QString& data) : d(new TextResultPrivate) { d->data=rtrim(data); d->plain=d->data; } TextResult::TextResult(const QString& data, const QString& plain) : d(new TextResultPrivate) { d->data=rtrim(data); d->plain=rtrim(plain); } TextResult::~TextResult() { delete d; } QString TextResult::toHtml() { QString s=d->data.toHtmlEscaped(); s.replace(QLatin1Char('\n'), QLatin1String("
\n")); s.replace(QLatin1Char(' '), QLatin1String(" ")); return s; } QVariant TextResult::data() { return QVariant(d->data); } QString TextResult::plain() { return d->plain; } int TextResult::type() { return TextResult::Type; } QString TextResult::mimeType() { qDebug()<<"format: "<format; } void TextResult::setFormat(TextResult::Format f) { d->format=f; } QDomElement TextResult::toXml(QDomDocument& doc) { qDebug()<<"saving textresult "<isStderr); if (d->format == LatexFormat) e.setAttribute(QStringLiteral("format"), QStringLiteral("latex")); QDomText txt=doc.createTextNode(data().toString()); e.appendChild(txt); return e; } QJsonValue Cantor::TextResult::toJupyterJson() { QJsonObject root; switch (d->format) { case PlainTextFormat: { if (executionIndex() != -1) { root.insert(QLatin1String("output_type"), QLatin1String("execute_result")); root.insert(QLatin1String("execution_count"), executionIndex()); QJsonObject data; data.insert(QLatin1String("text/plain"), jupyterText(d->data)); root.insert(QLatin1String("data"), data); root.insert(QLatin1String("metadata"), jupyterMetadata()); } else { root.insert(QLatin1String("output_type"), QLatin1String("stream")); - root.insert(QLatin1String("name"), QLatin1String("stdout")); + if (d->isStderr) + root.insert(QLatin1String("name"), QLatin1String("stderr")); + else + root.insert(QLatin1String("name"), QLatin1String("stdout")); // Jupyter don't support a few text result (it merges them into one text), // so add additinoal \n to end // See https://github.com/jupyter/notebook/issues/4699 root.insert(QLatin1String("text"), jupyterText(d->data, true)); } break; } case LatexFormat: { if (executionIndex() != -1) { root.insert(QLatin1String("output_type"), QLatin1String("execute_result")); root.insert(QLatin1String("execution_count"), executionIndex()); } else root.insert(QLatin1String("output_type"), QLatin1String("display_data")); QJsonObject data; data.insert(QLatin1String("text/latex"), jupyterText(d->data)); data.insert(QLatin1String("text/plain"), jupyterText(d->plain)); root.insert(QLatin1String("data"), data); root.insert(QLatin1String("metadata"), jupyterMetadata()); break; } } return root; } void TextResult::save(const QString& filename) { QFile file(filename); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; QTextStream stream(&file); stream<data; file.close(); } QJsonArray TextResult::jupyterText(const QString& text, bool addEndNewLine) { QJsonArray array; const QStringList& lines = text.split(QLatin1Char('\n')); for (int i = 0; i < lines.size(); i++) { QString line = lines[i]; if (i != lines.size() - 1 || addEndNewLine) line.append(QLatin1Char('\n')); array.append(line); } return array; } + +bool Cantor::TextResult::isStderr() const +{ + return d->isStderr; +} + +void Cantor::TextResult::setStdErr(bool value) +{ + d->isStderr = value; +} diff --git a/src/lib/textresult.h b/src/lib/textresult.h index 081cd6e4..1a9ac7e6 100644 --- a/src/lib/textresult.h +++ b/src/lib/textresult.h @@ -1,65 +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) 2009 Alexander Rieder */ #ifndef _TEXTRESULT_H #define _TEXTRESULT_H #include "result.h" #include "cantor_export.h" namespace Cantor { class TextResultPrivate; class CANTOR_EXPORT TextResult : public Result { public: enum { Type=1 }; enum Format { PlainTextFormat, LatexFormat}; TextResult(const QString& text); TextResult(const QString& text, const QString& plain); ~TextResult() override; QString toHtml() override; QVariant data() override; QString plain(); int type() override; QString mimeType() override; Format format(); void setFormat(Format f); + bool isStderr() const; + void setStdErr(bool value); + QDomElement toXml(QDomDocument& doc) override; QJsonValue toJupyterJson() override; void save(const QString& filename) override; private: QJsonArray jupyterText(const QString& text, bool addEndNewLine = false); private: TextResultPrivate* d; }; } #endif /* _TEXTRESULT_H */ diff --git a/src/loadedexpression.cpp b/src/loadedexpression.cpp index 25607b24..0da6e7ea 100644 --- a/src/loadedexpression.cpp +++ b/src/loadedexpression.cpp @@ -1,294 +1,298 @@ /* 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) 2009 Alexander Rieder */ #include "loadedexpression.h" #include "jupyterutils.h" #include "lib/imageresult.h" #include "lib/epsresult.h" #include "lib/textresult.h" #include "lib/latexresult.h" #include "lib/animationresult.h" #include "lib/latexrenderer.h" #include "lib/mimeresult.h" #include "lib/htmlresult.h" #include #include #include #include #include #include #include #include LoadedExpression::LoadedExpression( Cantor::Session* session ) : Cantor::Expression( session, false, -1) { } void LoadedExpression::interrupt() { //Do nothing } void LoadedExpression::evaluate() { //Do nothing } void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file) { setCommand(xml.firstChildElement(QLatin1String("Command")).text()); const QDomNodeList& results = xml.elementsByTagName(QLatin1String("Result")); for (int i = 0; i < results.size(); i++) { const QDomElement& resultElement = results.at(i).toElement(); const QString& type = resultElement.attribute(QLatin1String("type")); qDebug() << "type" << type; if ( type == QLatin1String("text")) { const QString& format = resultElement.attribute(QLatin1String("format")); + bool isStderr = resultElement.attribute(QLatin1String("stderr")).toInt(); Cantor::TextResult* result = new Cantor::TextResult(resultElement.text()); if (format == QLatin1String("latex")) result->setFormat(Cantor::TextResult::LatexFormat); + result->setStdErr(isStderr); addResult(result); } else if (type == QLatin1String("mime")) { const QDomElement& resultElement = results.at(i).toElement(); const QString& mimeType = resultElement.attribute(QLatin1String("mimeType")); bool withPlain = resultElement.attribute(QLatin1String("withPlain")).toInt(); QString plain; if (withPlain) plain = resultElement.firstChildElement(QLatin1String("Plain")).text(); const QString& content = resultElement.firstChildElement(QLatin1String("Content")).text(); QJsonDocument jsonDoc = QJsonDocument::fromJson(content.toUtf8());; const QJsonValue& value = jsonDoc.object().value(QLatin1String("content")); addResult(new Cantor::MimeResult(plain, value, mimeType)); } else if (type == QLatin1String("html")) { const QString& formatString = resultElement.attribute(QLatin1String("showCode")); Cantor::HtmlResult::Format format = Cantor::HtmlResult::Html; if (formatString == QLatin1String("htmlSource")) format = Cantor::HtmlResult::HtmlSource; else if (formatString == QLatin1String("plain")) format = Cantor::HtmlResult::PlainAlternative; const QString& plain = resultElement.firstChildElement(QLatin1String("Plain")).text(); const QString& html = resultElement.firstChildElement(QLatin1String("Html")).text(); Cantor::HtmlResult* result = new Cantor::HtmlResult(html, plain); result->setFormat(format); addResult(result); } else if (type == QLatin1String("image") || type == QLatin1String("latex") || type == QLatin1String("animation")) { const KArchiveEntry* imageEntry=file.directory()->entry(resultElement.attribute(QLatin1String("filename"))); if (imageEntry&&imageEntry->isFile()) { const KArchiveFile* imageFile=static_cast(imageEntry); QString dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation); imageFile->copyTo(dir); QUrl imageUrl = QUrl::fromLocalFile(QDir(dir).absoluteFilePath(imageFile->name())); if(type==QLatin1String("latex")) { addResult(new Cantor::LatexResult(resultElement.text(), imageUrl)); }else if(type==QLatin1String("animation")) { addResult(new Cantor::AnimationResult(imageUrl)); }else if(imageFile->name().endsWith(QLatin1String(".eps"))) { addResult(new Cantor::EpsResult(imageUrl)); }else { addResult(new Cantor::ImageResult(imageUrl, resultElement.text())); } } } } const QDomElement& errElem = xml.firstChildElement(QLatin1String("Error")); if (!errElem.isNull()) { setErrorMessage(errElem.text()); setStatus(Error); } else setStatus(Done); } void LoadedExpression::loadFromJupyter(const QJsonObject& cell) { setCommand(JupyterUtils::getSource(cell)); const QJsonValue idObject = cell.value(QLatin1String("execution_count")); if (!idObject.isUndefined() && !idObject.isNull()) setId(idObject.toInt()); const QJsonArray& outputs = cell.value(QLatin1String("outputs")).toArray(); for (QJsonArray::const_iterator iter = outputs.begin(); iter != outputs.end(); iter++) { if (!JupyterUtils::isJupyterOutput(*iter)) continue; const QJsonObject& output = iter->toObject(); const QString& outputType = JupyterUtils::getOutputType(output); if (JupyterUtils::isJupyterTextOutput(output)) { const QString& text = JupyterUtils::fromJupyterMultiline(output.value(QLatin1String("text"))); - - addResult(new Cantor::TextResult(text)); + bool isStderr = output.value(QLatin1String("name")).toString() == QLatin1String("stderr"); + Cantor::TextResult* result = new Cantor::TextResult(text); + result->setStdErr(isStderr); + addResult(result); } else if (JupyterUtils::isJupyterErrorOutput(output)) { const QJsonArray& tracebackLineArray = output.value(QLatin1String("traceback")).toArray(); QString traceback; // Looks like the traceback in Jupyter joined with '\n', no '' // So, manually add it for (const QJsonValue& line : tracebackLineArray) traceback += line.toString() + QLatin1Char('\n'); traceback.chop(1); // IPython returns error with terminal colors, we handle it here, but should we? static const QChar ESC(0x1b); traceback.remove(QRegExp(QString(ESC)+QLatin1String("\\[[0-9;]*m"))); setErrorMessage(traceback); } else if (JupyterUtils::isJupyterDisplayOutput(output) || JupyterUtils::isJupyterExecutionResult(output)) { const QJsonObject& data = output.value(QLatin1String("data")).toObject(); QJsonObject metadata = JupyterUtils::getMetadata(output); const QString& text = JupyterUtils::fromJupyterMultiline(data.value(JupyterUtils::textMime)); const QString& mainKey = JupyterUtils::mainBundleKey(data); Cantor::Result* result = nullptr; if (mainKey == JupyterUtils::gifMime) { const QByteArray& bytes = QByteArray::fromBase64(data.value(mainKey).toString().toLatin1()); QTemporaryFile file; file.setAutoRemove(false); file.open(); file.write(bytes); file.close(); result = new Cantor::AnimationResult(QUrl::fromLocalFile(file.fileName()), text); } else if (mainKey == JupyterUtils::textMime) { result = new Cantor::TextResult(text); } else if (mainKey == JupyterUtils::htmlMime) { const QString& html = JupyterUtils::fromJupyterMultiline(data.value(JupyterUtils::htmlMime)); // Some backends places gif animation in hmlt (img tag), for example, Sage if (JupyterUtils::isGifHtml(html)) { result = new Cantor::AnimationResult(JupyterUtils::loadGifHtml(html), text); } else { result = new Cantor::HtmlResult(html, text); } } else if (mainKey == JupyterUtils::latexMime) { QString latex = JupyterUtils::fromJupyterMultiline(data.value(mainKey)); QScopedPointer renderer(new Cantor::LatexRenderer(this)); renderer->setLatexCode(latex); renderer->setEquationOnly(false); renderer->setMethod(Cantor::LatexRenderer::LatexMethod); renderer->renderBlocking(); result = new Cantor::LatexResult(latex, QUrl::fromLocalFile(renderer->imagePath()), text); // If we have failed to render LaTeX i think Cantor should show the latex code at least if (!renderer->renderingSuccessful()) static_cast(result)->showCode(); } // So this is image else if (JupyterUtils::imageKeys(data).contains(mainKey)) { const QImage& image = JupyterUtils::loadImage(data, mainKey); result = new Cantor::ImageResult(image, text); const QJsonValue size = metadata.value(mainKey); if (size.isObject()) { int w = size.toObject().value(QLatin1String("width")).toInt(-1); int h = size.toObject().value(QLatin1String("height")).toInt(-1); if (w != -1 && h != -1) { static_cast(result)->setDisplaySize(QSize(w, h)); // Remove size information, because we don't need it after setting display size // Also, we encode image to 'image/png' on saving as .ipynb, even original image don't png // So, without removing the size info here, after loading for example 'image/tiff' to Cantor from .ipynb and saving the worksheet // (which means, that the image will be saved as png and not as tiff). // We will have outdated key 'image/tiff' in metadata. metadata.remove(mainKey); } } } else if (data.keys().size() == 1 && data.keys()[0] == JupyterUtils::textMime) result = new Cantor::TextResult(text); // Cantor don't know, how handle this, so pack into mime container result else if (data.keys().count() > 0) { qDebug() << "Found unsupported " << outputType << "result with mimes" << data.keys() << ", so add them to mime container result"; QString key; if (data.keys().contains(JupyterUtils::textMime) && data.keys().count() > 1) if (data.keys()[0] == JupyterUtils::textMime) key = data.keys()[1]; else key = data.keys()[0]; else key = data.keys()[0]; result = new Cantor::MimeResult(text, data.value(key), key); } if (result) { result->setJupyterMetadata(metadata); int resultIndex = output.value(QLatin1String("execution_count")).toInt(-1); if (resultIndex != -1) result->setExecutionIndex(resultIndex); addResult(result); } } } if (errorMessage().isEmpty()) setStatus(Done); else setStatus(Error); }