Mercurial > octave
changeset 30293:1a1f3ae76e74
Use underscores instead of CamelCase in jupyter_notebook classdef
* scripts/miscellaneous/jupyter_notebook.m: Rename file, adapt classdef name.
* scripts/miscellaneous/module.mk: Rename jupyter_notebook classdef.
* test/jupyter-notebook/jupyter-notebook.tst: Rename file, adapt classdef name.
* test/jupyter-notebook/module.mk: Rename jupyter-notebook.tst test file.
* NEWS: Update classdef name.
author | Abdallah Elshamy <abdallah.k.elshamy@gmail.com> |
---|---|
date | Sat, 13 Nov 2021 12:26:07 -0500 |
parents | 8ee507796a34 |
children | 488548c762de |
files | NEWS scripts/miscellaneous/JupyterNotebook.m scripts/miscellaneous/jupyter_notebook.m scripts/miscellaneous/module.mk test/jupyter-notebook/JupyterNotebook.tst test/jupyter-notebook/jupyter-notebook.tst test/jupyter-notebook/module.mk |
diffstat | 7 files changed, 873 insertions(+), 873 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Nov 15 09:39:29 2021 +0100 +++ b/NEWS Sat Nov 13 12:26:07 2021 -0500 @@ -97,7 +97,7 @@ `jsondecode` and `jsonencode` functions to read and write JSON data. - As part of GSoC 2021, Abdallah K. Elshamy implemented the -`JupyterNotebook` classdef class. This class supports running and +`jupyter_notebook` classdef class. This class supports running and filling Jupyter Notebooks using the Octave language kernel from Octave itself. Making the evaluation of long-running Jupyter Notebooks on a computing server without permanent browser connection possible. @@ -226,7 +226,7 @@ the file contains a 2-D text matrix. - The file functions `copyfile`, `mkdir`, `movefile`, `rmdir` now return -a logical value (true/false) rather than a numeric value (1/0). +a logical value (true/false) rather than a numeric value (1/0). - `uimenu` now accepts property `"Text"` which is identical to `"Label"`. Matlab recommends using `"Text"` in new code, although there @@ -271,7 +271,7 @@ * `fill3` * `jsondecode` * `jsonencode` -* `JupyterNotebook` +* `jupyter_notebook` * `listfonts` * `matlab.net.base64decode` * `matlab.net.base64encode`
--- a/scripts/miscellaneous/JupyterNotebook.m Mon Nov 15 09:39:29 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,668 +0,0 @@ -## Copyright (C) 2021 The Octave Project Developers -## -## 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 3 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, see -## <https://www.gnu.org/licenses/>. - - -classdef JupyterNotebook < handle - - ## -*- texinfo -*- - ## @deftypefn {} {@var{notebook} =} JupyterNotebook (@var{notebookFileName}) - ## - ## Run and fill the Jupyter Notebook in @var{notebookFileName} within - ## GNU Octave. - ## - ## Supported are textual and graphical Octave outputs. - ## - ## This class has a public attribute @qcode{notebook} which is a struct - ## representing the JSON-decoded Jupyter Notebook. This attribute is - ## intentionally public to enable advanced notebook manipulations. - ## - ## Note: Jupyter Notebook versions (@qcode{nbformat}) lower than 4.0 are - ## not supported. - ## - ## @qcode{%plot} magic is supported with the following settings: - ## @itemize @bullet - ## @item - ## "%plot -f <format>" or "%plot --format <format>": specifies the - ## image storage format. Supported formats are: - ## - ## @itemize @minus - ## @item - ## PNG (default) - ## - ## @item - ## SVG (Note: If SVG images do not appear in the notebook, it is most - ## related to the Jupyter Notebook security mechanism and explicitly - ## "trusting" them is necessary). - ## - ## @item - ## JPG - ## @end itemize - ## - ## @item - ## "%plot -r <number>" or "%plot --resolution <number>": specifies the - ## image resolution. - ## - ## @item - ## "%plot -w <number>" or "%plot --width <number>": specifies the - ## image width. - ## - ## @item - ## "%plot -h <number>" or "%plot --height <number>": specifies the - ## image height. - ## @end itemize - ## - ## Examples: - ## - ## @example - ## @group - ## ## Run all cells and generate the filled notebook - ## - ## ## Instantiate an object from the notebook file - ## notebook = JupyterNotebook("myNotebook.ipynb") - ## @result{} notebook = - ## - ## <object JupyterNotebook> - ## - ## ## Run the code and embed the results in the @qcode{notebook} attribute - ## notebook.runAll() - ## ## Generate the new notebook by overwriting the original notebook - ## notebook.generateNotebook("myNotebook.ipynb") - ## @end group - ## - ## @group - ## ## Run the second cell and generate the filled notebook - ## - ## ## Instantiate an object from the notebook file - ## notebook = JupyterNotebook("myNotebook.ipynb") - ## @result{} notebook = - ## - ## <object JupyterNotebook> - ## - ## ## Run the code and embed the results in the @qcode{notebook} attribute - ## notebook.run(2) - ## ## Generate the new notebook in a new file - ## notebook.generateNotebook("myNewNotebook.ipynb") - ## @end group - ## - ## @group - ## ## Generate an Octave script from a notebook - ## - ## ## Instantiate an object from the notebook file - ## notebook = JupyterNotebook("myNotebook.ipynb") - ## @result{} notebook = - ## - ## <object JupyterNotebook> - ## - ## ## Generate the octave script - ## notebook.generateOctaveScript("myScript.m") - ## @end group - ## @end example - ## - ## @seealso{jsondecode, jsonencode} - ## @end deftypefn - - properties - - notebook = struct() - - endproperties - - properties (Access = "private") - - context = struct("ans", "") - - endproperties - - methods - - function obj = JupyterNotebook (notebookFileName) - - if (nargin != 1) - print_usage (); - endif - - if (! (ischar (notebookFileName) && isrow (notebookFileName))) - error ("JupyterNotebook: notebookFileName must be a string"); - endif - - obj.notebook = jsondecode (fileread (notebookFileName), - "makeValidName", false); - - ## Validate the notebook's format according to nbformat: 4.0 - if (! (isfield (obj.notebook, "metadata") - && isfield (obj.notebook, "nbformat") - && isfield (obj.notebook, "nbformat_minor") - && isfield (obj.notebook, "cells"))) - error ("JupyterNotebook: not valid format for Jupyter notebooks"); - endif - - ## Issue a warning if the format is lower than 4.0 - if (obj.notebook.nbformat < 4) - warning (["JupyterNotebook: nbformat versions lower than 4.0 are ", ... - "not supported"]); - endif - - ## Handle the case if there is only one cell. - ## Make "obj.notebook.cells" a cell of structs to match the format. - if (numel (obj.notebook.cells) == 1) - obj.notebook.cells = {obj.notebook.cells}; - endif - - ## Handle the case if the cells have the same keys. - ## Make "obj.notebook.cells" a cell of structs instead of struct array - ## to unify the indexing method. - if (isstruct (obj.notebook.cells)) - obj.notebook.cells = num2cell (obj.notebook.cells); - endif - - for i = 1:numel (obj.notebook.cells) - if (! isfield (obj.notebook.cells{i}, "source")) - error ("JupyterNotebook: cells must contain a \"source\" field"); - endif - - if (! isfield (obj.notebook.cells{i}, "cell_type")) - error ("JupyterNotebook: cells must contain a \"cell_type\" field"); - endif - - ## Handle when null JSON values are decoded into empty arrays. - if (isfield (obj.notebook.cells{i}, "execution_count") - && numel (obj.notebook.cells{i}.execution_count) == 0) - obj.notebook.cells{i}.execution_count = 1; - endif - - ## Handle the case if there is only one output in the cell. - ## Make the outputs of the cell a cell of structs to match the format. - if (isfield (obj.notebook.cells{i}, "outputs") - && numel (obj.notebook.cells{i}.outputs) == 1) - obj.notebook.cells{i}.outputs = {obj.notebook.cells{i}.outputs}; - endif - endfor - - endfunction - - - function generateOctaveScript (obj, scriptFileName) - - ## -*- texinfo -*- - ## @deftypefn {} {} generateOctaveScript (@var{scriptFileName}) - ## - ## Write an Octave script that has the contents of the Jupyter Notebook - ## stored in the @qcode{notebook} attribute to @var{scriptFileName}. - ## - ## Non code cells are generated as block comments. - ## - ## See @code{help JupyterNotebook} for examples. - ## - ## @end deftypefn - - if (nargin != 2) - print_usage (); - endif - - if (! (ischar (scriptFileName) && isrow (scriptFileName))) - error ("JupyterNotebook: scriptFileName must be a string"); - endif - - fhandle = fopen (scriptFileName, "w"); - - for i = 1:numel (obj.notebook.cells) - if (strcmp (obj.notebook.cells{i}.cell_type, "markdown")) - fputs (fhandle, "\n#{\n"); - endif - - for k = 1:numel (obj.notebook.cells{i}.source) - fputs (fhandle, obj.notebook.cells{i}.source{k}); - endfor - - if (strcmp (obj.notebook.cells{i}.cell_type, "markdown")) - fputs (fhandle, "\n#}\n"); - endif - fputs (fhandle, "\n"); - endfor - fclose (fhandle); - - endfunction - - - function generateNotebook (obj, notebookFileName) - - ## -*- texinfo -*- - ## @deftypefn {} {} generateNotebook (@var{notebookFileName}) - ## - ## Write the Jupyter Notebook stored in the @qcode{notebook} - ## attribute to @var{notebookFileName}. - ## - ## The @qcode{notebook} attribute is encoded to JSON text. - ## - ## See @code{help JupyterNotebook} for examples. - ## - ## @end deftypefn - - if (nargin != 2) - print_usage (); - endif - - if (! (ischar (notebookFileName) && isrow (notebookFileName))) - error ("JupyterNotebook: notebookFileName must be a string"); - endif - - fhandle = fopen (notebookFileName, "w"); - - fputs (fhandle, jsonencode (obj.notebook, "ConvertInfAndNaN", false, - "PrettyPrint", true)); - - fclose (fhandle); - - endfunction - - - function run (obj, cell_index) - - ## -*- texinfo -*- - ## @deftypefn {} {} run (@var{cell_index}) - ## - ## Run the Jupyter Notebook cell with index @var{cell_index} - ## and eventually replace previous output cells in the object. - ## - ## The first Jupyter Notebook cell has the index 1. - ## - ## Note: The code evaluation of the Jupyter Notebook cells is done - ## in a separate Jupyter Notebook context. Thus currently open - ## figures and workspace variables won't be affected by executing - ## this function. However, current workspace variables cannot be - ## accessed either. - ## - ## See @code{help JupyterNotebook} for examples. - ## - ## @end deftypefn - - if (nargin != 2) - print_usage (); - endif - - if (! (isscalar (cell_index) && ! islogical (cell_index) - && (mod (cell_index, 1) == 0) && (cell_index > 0))) - error ("JupyterNotebook: cell_index must be a scalar positive integer"); - endif - - if (cell_index > length (obj.notebook.cells)) - error ("JupyterNotebook: cell_index is out of bound"); - endif - - if (! strcmp (obj.notebook.cells{cell_index}.cell_type, "code")) - return; - endif - - ## Remove previous outputs. - obj.notebook.cells{cell_index}.outputs = {}; - - if (isempty (obj.notebook.cells{cell_index}.source)) - return; - endif - - ## Default values for printOptions. - printOptions.imageFormat = "png"; - printOptions.resolution = "0"; - - ## The default width and height in Jupyter notebook - printOptions.width = "640"; - printOptions.height = "480"; - - ## Parse "plot magic" commands. - ## https://github.com/Calysto/metakernel/blob/master/metakernel/ ... - ## magics/README.md#plot - for j = 1 : numel (obj.notebook.cells{cell_index}.source) - if (strncmpi (obj.notebook.cells{cell_index}.source{j}, "%plot", 5)) - magics = strsplit (strtrim ( - obj.notebook.cells{cell_index}.source{j})); - for i = 1 : numel (magics) - if (any (strcmp (magics{i}, {"-f", "--format"})) - && (i < numel (magics))) - printOptions.imageFormat = magics{i+1}; - endif - if (any (strcmp (magics{i}, {"-r", "--resolution"})) - && (i < numel (magics))) - printOptions.resolution = magics{i+1}; - endif - if (any (strcmp (magics{i}, {"-w", "--width"})) - && (i < numel (magics))) - printOptions.width = magics{i+1}; - endif - if (any (strcmp (magics{i}, {"-h", "--height"})) - && (i < numel (magics))) - printOptions.height = magics{i+1}; - endif - endfor - endif - endfor - - ## Remember previously opened figures. - fig_ids = findall (0, "type", "figure"); - - ## Create a new figure, if there are existing plots. - if (! isempty (fig_ids)) - newFig = figure (); - endif - - stream_output = struct ("name", "stdout", "output_type", "stream"); - - output_lines = obj.evalCode (strjoin ( - obj.notebook.cells{cell_index}.source)); - - if (! isempty(output_lines)) - stream_output.text = {output_lines}; - endif - - if (isfield (stream_output, "text")) - obj.notebook.cells{cell_index}.outputs{end + 1} = stream_output; - endif - - ## If there are existing plots and newFig is empty, delete it. - if (exist ("newFig") && isempty (get (newFig, "children"))) - delete (newFig); - endif - - ## Check for newly created figures. - fig_ids_new = setdiff (findall (0, "type", "figure"), fig_ids); - - if (numel (fig_ids_new) > 0) - if (exist ("__octave_jupyter_temp__", "dir")) - ## Delete open figures before raising the error. - for i = 1:numel (fig_ids_new) - delete (fig_ids_new(i)); - endfor - error (["JupyterNotebook: temporary directory ", ... - "__octave_jupyter_temp__ exists. Please remove it ", ... - "manually."]); - endif - - [status, msg, msgid] = mkdir ("__octave_jupyter_temp__"); - if (status == 0) - ## Delete open figures before raising the error. - for i = 1 : numel (fig_ids_new) - delete (fig_ids_new(i)); - endfor - error (["JupyterNotebook: Cannot create a temporary directory. ", ... - msg]); - endif - - for i = 1:numel (fig_ids_new) - figure (fig_ids_new(i), "visible", "off"); - obj.embedImage (cell_index, fig_ids_new (i), printOptions); - delete (fig_ids_new(i)); - endfor - - [status, msg, msgid] = rmdir ("__octave_jupyter_temp__"); - if (status == 0) - error (["JupyterNotebook: Cannot delete the temporary ", ... - "directory. ", msg]); - endif - endif - - endfunction - - - function runAll (obj) - - ## -*- texinfo -*- - ## @deftypefn {} {} runAll () - ## - ## Run all Jupyter Notebook cells and eventually replace previous - ## output cells in the object. - ## - ## Note: The code evaluation of the Jupyter Notebook cells is done - ## in a separate Jupyter Notebook context. Thus currently open - ## figures and workspace variables won't be affected by executing - ## this function. However, current workspace variables cannot be - ## accessed either. - ## - ## See @code{help JupyterNotebook} for examples. - ## - ## @end deftypefn - - if (nargin != 1) - print_usage (); - endif - - for i = 1:numel (obj.notebook.cells) - obj.run(i); - endfor - - endfunction - - endmethods - - - methods (Access = "private") - - function retVal = evalCode (__obj__, __code__) - - ## Evaluate the code string "__code__" using "evalc". - ## Before the code is evaluated, the previous notebook context is - ## loaded from "__obj__" and the new context is saved to that struct. - - if (nargin != 2) - print_usage (); - endif - - if (isempty (__code__)) - retVal = []; - return; - endif - - if (! (ischar (__code__) && isrow (__code__))) - error ("JupyterNotebook: code must be a string"); - endif - - __obj__.loadContext (); - - ## Add a statement to detect the value of the variable "ans" - __code__ = [__code__, "\nans"]; - - retVal = strtrim (evalc (__code__, ["printf (\"error: \"); ", ... - "printf (lasterror.message)"])); - - ## Handle the "ans" variable in the context. - start_index = rindex (retVal, "ans =") + 6; - if ((start_index > 6)) - if ((start_index <= length (retVal))) - end_index = start_index; - while ((retVal(end_index) != "\n") && (end_index < length (retVal))) - end_index += 1; - endwhile - __obj__.context.ans = retVal(start_index:end_index); - else - end_index = length (retVal); - __obj__.context.ans = ""; - endif - - ## Delete the output of the additional statement if the execution - ## is completed with no errors. - if (end_index == length (retVal)) - ## Remove the extra new line if there are other outputs with - ## the "ans" statement output - if (start_index == 7) - start_index = 1; - else - start_index = start_index - 7; - endif - retVal(start_index:end_index) = ""; - endif - endif - - __obj__.saveContext (); - - endfunction - - - function saveContext (obj, op) - - ## Save the context in private "obj" attribute. - - ## Handle the "ans" variable in the context. - obj.context = struct ("ans", obj.context.ans); - - forbidden_var_names = {"__code__", "__obj__", "ans"}; - - ## Get variable names. - var_names = {evalin("caller", "whos").name}; - - ## Store all variables to context. - for i = 1:length (var_names) - if (! any (strcmp (var_names{i}, forbidden_var_names))) - obj.context.(var_names{i}) = evalin ("caller", var_names{i}); - endif - endfor - - endfunction - - - function loadContext (obj) - - ## Load the context from private "obj" attribute. - for [val, key] = obj.context - assignin ("caller", key, val); - endfor - - endfunction - - - function embedImage (obj, cell_index, figHandle, printOptions) - - ## Embed images in the notebook. - ## - ## To support a new format: - ## 1. Create a new function that embeds the new format - ## (e.g. embed_svg_image). - ## 2. Add a new case to the switch-statement below. - - if (isempty (get (figHandle, "children"))) - error_text = {"The figure is empty!"}; - obj.addErrorOutput (cell_index, "The figure is empty!"); - return; - endif - - ## Check if the resolution is correct - if (isempty (str2num (printOptions.resolution))) - obj.addErrorOutput (cell_index, - "A number is required for resolution, not a string"); - return; - endif - - ## Check if the width is correct - if (isempty (str2num (printOptions.width))) - obj.addErrorOutput (cell_index, - "A number is required for width, not a string"); - return; - endif - - ## Check if the height is correct - if (isempty (str2num (printOptions.height))) - obj.addErrorOutput (cell_index, - "A number is required for height, not a string"); - return; - endif - - switch (lower (printOptions.imageFormat)) - case "png" - display_output = obj.embed_png_jpg_image (figHandle, - printOptions, "png"); - case "jpg" - display_output = obj.embed_png_jpg_image (figHandle, - printOptions, "jpg"); - case "svg" - display_output = obj.embed_svg_image (figHandle, printOptions); - otherwise - obj.addErrorOutput (cell_index, ["Cannot embed the \'", ... - printOptions.imageFormat, ... - "\' image format\n"]); - return; - endswitch - - obj.notebook.cells{cell_index}.outputs{end + 1} = display_output; - - endfunction - - - function dstruct = embed_png_jpg_image (obj, figHandle, printOptions, fmt) - - if (strcmp (fmt, "png")) - mime = "image/png"; - else - mime = "image/jpeg"; - endif - - image_path = fullfile ("__octave_jupyter_temp__", ["temp.", fmt]); - print (figHandle, image_path, ["-d", fmt], - ["-r" printOptions.resolution]); - - dstruct.output_type = "display_data"; - dstruct.metadata.(mime).width = printOptions.width; - dstruct.metadata.(mime).height = printOptions.height; - dstruct.data.("text/plain") = {"<IPython.core.display.Image object>"}; - dstruct.data.(mime) = base64_encode (uint8 (fileread (image_path))); - - delete (image_path); - - endfunction - - - function dstruct = embed_svg_image (obj, figHandle, printOptions) - - image_path = fullfile ("__octave_jupyter_temp__", "temp.svg"); - print (figHandle, image_path, "-dsvg", ["-r" printOptions.resolution]); - - dstruct.output_type = "display_data"; - dstruct.metadata = struct (); - dstruct.data.("text/plain") = {"<IPython.core.display.SVG object>"}; - dstruct.data.("image/svg+xml") = strsplit (fileread (image_path), "\n"); - - ## FIXME: The following is a workaround until we can properly print - ## SVG images in the right width and height. - ## Detect the <svg> tag. it is either the first or the second item - if (strncmpi (dstruct.data.("image/svg+xml"){1}, "<svg", 4)) - i = 1; - else - i = 2; - endif - - ## Embed the width and height in the image itself - svg_tag = dstruct.data.("image/svg+xml"){i}; - svg_tag = regexprep (svg_tag, "width=\"(.*?)\"", - ["width=\"" printOptions.width "px\""]); - svg_tag = regexprep (svg_tag, "height=\"(.*?)\"", - ["height=\"" printOptions.height "px\""]); - dstruct.data.("image/svg+xml"){i} = svg_tag; - - delete (image_path); - - endfunction - - - function addErrorOutput (obj, cell_index, error_msg) - - stream_output.name = "stderr"; - stream_output.output_type = "stream"; - stream_output.text = {error_msg}; - obj.notebook.cells{cell_index}.outputs{end + 1} = stream_output; - - endfunction - - endmethods - -endclassdef - -#!error JupyterNotebook ()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/miscellaneous/jupyter_notebook.m Sat Nov 13 12:26:07 2021 -0500 @@ -0,0 +1,668 @@ +## Copyright (C) 2021 The Octave Project Developers +## +## 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 3 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, see +## <https://www.gnu.org/licenses/>. + + +classdef jupyter_notebook < handle + + ## -*- texinfo -*- + ## @deftypefn {} {@var{notebook} =} jupyter_notebook (@var{notebook_file_name}) + ## + ## Run and fill the Jupyter Notebook in @var{notebook_file_name} within + ## GNU Octave. + ## + ## Supported are textual and graphical Octave outputs. + ## + ## This class has a public attribute @qcode{notebook} which is a struct + ## representing the JSON-decoded Jupyter Notebook. This attribute is + ## intentionally public to enable advanced notebook manipulations. + ## + ## Note: Jupyter Notebook versions (@qcode{nbformat}) lower than 4.0 are + ## not supported. + ## + ## @qcode{%plot} magic is supported with the following settings: + ## @itemize @bullet + ## @item + ## "%plot -f <format>" or "%plot --format <format>": specifies the + ## image storage format. Supported formats are: + ## + ## @itemize @minus + ## @item + ## PNG (default) + ## + ## @item + ## SVG (Note: If SVG images do not appear in the notebook, it is most + ## related to the Jupyter Notebook security mechanism and explicitly + ## "trusting" them is necessary). + ## + ## @item + ## JPG + ## @end itemize + ## + ## @item + ## "%plot -r <number>" or "%plot --resolution <number>": specifies the + ## image resolution. + ## + ## @item + ## "%plot -w <number>" or "%plot --width <number>": specifies the + ## image width. + ## + ## @item + ## "%plot -h <number>" or "%plot --height <number>": specifies the + ## image height. + ## @end itemize + ## + ## Examples: + ## + ## @example + ## @group + ## ## Run all cells and generate the filled notebook + ## + ## ## Instantiate an object from the notebook file + ## notebook = jupyter_notebook("myNotebook.ipynb") + ## @result{} notebook = + ## + ## <object jupyter_notebook> + ## + ## ## Run the code and embed the results in the @qcode{notebook} attribute + ## notebook.run_all() + ## ## Generate the new notebook by overwriting the original notebook + ## notebook.generate_notebook("myNotebook.ipynb") + ## @end group + ## + ## @group + ## ## Run the second cell and generate the filled notebook + ## + ## ## Instantiate an object from the notebook file + ## notebook = jupyter_notebook("myNotebook.ipynb") + ## @result{} notebook = + ## + ## <object jupyter_notebook> + ## + ## ## Run the code and embed the results in the @qcode{notebook} attribute + ## notebook.run(2) + ## ## Generate the new notebook in a new file + ## notebook.generate_notebook("myNewNotebook.ipynb") + ## @end group + ## + ## @group + ## ## Generate an Octave script from a notebook + ## + ## ## Instantiate an object from the notebook file + ## notebook = jupyter_notebook("myNotebook.ipynb") + ## @result{} notebook = + ## + ## <object jupyter_notebook> + ## + ## ## Generate the octave script + ## notebook.generate_octave_script("myScript.m") + ## @end group + ## @end example + ## + ## @seealso{jsondecode, jsonencode} + ## @end deftypefn + + properties + + notebook = struct() + + endproperties + + properties (Access = "private") + + context = struct("ans", "") + + endproperties + + methods + + function obj = jupyter_notebook (notebook_file_name) + + if (nargin != 1) + print_usage (); + endif + + if (! (ischar (notebook_file_name) && isrow (notebook_file_name))) + error ("jupyter_notebook: notebook_file_name must be a string"); + endif + + obj.notebook = jsondecode (fileread (notebook_file_name), + "makeValidName", false); + + ## Validate the notebook's format according to nbformat: 4.0 + if (! (isfield (obj.notebook, "metadata") + && isfield (obj.notebook, "nbformat") + && isfield (obj.notebook, "nbformat_minor") + && isfield (obj.notebook, "cells"))) + error ("jupyter_notebook: not valid format for Jupyter notebooks"); + endif + + ## Issue a warning if the format is lower than 4.0 + if (obj.notebook.nbformat < 4) + warning (["jupyter_notebook: nbformat versions lower than 4.0 are ", ... + "not supported"]); + endif + + ## Handle the case if there is only one cell. + ## Make "obj.notebook.cells" a cell of structs to match the format. + if (numel (obj.notebook.cells) == 1) + obj.notebook.cells = {obj.notebook.cells}; + endif + + ## Handle the case if the cells have the same keys. + ## Make "obj.notebook.cells" a cell of structs instead of struct array + ## to unify the indexing method. + if (isstruct (obj.notebook.cells)) + obj.notebook.cells = num2cell (obj.notebook.cells); + endif + + for i = 1:numel (obj.notebook.cells) + if (! isfield (obj.notebook.cells{i}, "source")) + error ("jupyter_notebook: cells must contain a \"source\" field"); + endif + + if (! isfield (obj.notebook.cells{i}, "cell_type")) + error ("jupyter_notebook: cells must contain a \"cell_type\" field"); + endif + + ## Handle when null JSON values are decoded into empty arrays. + if (isfield (obj.notebook.cells{i}, "execution_count") + && numel (obj.notebook.cells{i}.execution_count) == 0) + obj.notebook.cells{i}.execution_count = 1; + endif + + ## Handle the case if there is only one output in the cell. + ## Make the outputs of the cell a cell of structs to match the format. + if (isfield (obj.notebook.cells{i}, "outputs") + && numel (obj.notebook.cells{i}.outputs) == 1) + obj.notebook.cells{i}.outputs = {obj.notebook.cells{i}.outputs}; + endif + endfor + + endfunction + + + function generate_octave_script (obj, script_file_name) + + ## -*- texinfo -*- + ## @deftypefn {} {} generate_octave_script (@var{script_file_name}) + ## + ## Write an Octave script that has the contents of the Jupyter Notebook + ## stored in the @qcode{notebook} attribute to @var{script_file_name}. + ## + ## Non code cells are generated as block comments. + ## + ## See @code{help jupyter_notebook} for examples. + ## + ## @end deftypefn + + if (nargin != 2) + print_usage (); + endif + + if (! (ischar (script_file_name) && isrow (script_file_name))) + error ("jupyter_notebook: script_file_name must be a string"); + endif + + fhandle = fopen (script_file_name, "w"); + + for i = 1:numel (obj.notebook.cells) + if (strcmp (obj.notebook.cells{i}.cell_type, "markdown")) + fputs (fhandle, "\n#{\n"); + endif + + for k = 1:numel (obj.notebook.cells{i}.source) + fputs (fhandle, obj.notebook.cells{i}.source{k}); + endfor + + if (strcmp (obj.notebook.cells{i}.cell_type, "markdown")) + fputs (fhandle, "\n#}\n"); + endif + fputs (fhandle, "\n"); + endfor + fclose (fhandle); + + endfunction + + + function generate_notebook (obj, notebook_file_name) + + ## -*- texinfo -*- + ## @deftypefn {} {} generate_notebook (@var{notebook_file_name}) + ## + ## Write the Jupyter Notebook stored in the @qcode{notebook} + ## attribute to @var{notebook_file_name}. + ## + ## The @qcode{notebook} attribute is encoded to JSON text. + ## + ## See @code{help jupyter_notebook} for examples. + ## + ## @end deftypefn + + if (nargin != 2) + print_usage (); + endif + + if (! (ischar (notebook_file_name) && isrow (notebook_file_name))) + error ("jupyter_notebook: notebook_file_name must be a string"); + endif + + fhandle = fopen (notebook_file_name, "w"); + + fputs (fhandle, jsonencode (obj.notebook, "ConvertInfAndNaN", false, + "PrettyPrint", true)); + + fclose (fhandle); + + endfunction + + + function run (obj, cell_index) + + ## -*- texinfo -*- + ## @deftypefn {} {} run (@var{cell_index}) + ## + ## Run the Jupyter Notebook cell with index @var{cell_index} + ## and eventually replace previous output cells in the object. + ## + ## The first Jupyter Notebook cell has the index 1. + ## + ## Note: The code evaluation of the Jupyter Notebook cells is done + ## in a separate Jupyter Notebook context. Thus currently open + ## figures and workspace variables won't be affected by executing + ## this function. However, current workspace variables cannot be + ## accessed either. + ## + ## See @code{help jupyter_notebook} for examples. + ## + ## @end deftypefn + + if (nargin != 2) + print_usage (); + endif + + if (! (isscalar (cell_index) && ! islogical (cell_index) + && (mod (cell_index, 1) == 0) && (cell_index > 0))) + error ("jupyter_notebook: cell_index must be a scalar positive integer"); + endif + + if (cell_index > length (obj.notebook.cells)) + error ("jupyter_notebook: cell_index is out of bound"); + endif + + if (! strcmp (obj.notebook.cells{cell_index}.cell_type, "code")) + return; + endif + + ## Remove previous outputs. + obj.notebook.cells{cell_index}.outputs = {}; + + if (isempty (obj.notebook.cells{cell_index}.source)) + return; + endif + + ## Default values for printOptions. + printOptions.imageFormat = "png"; + printOptions.resolution = "0"; + + ## The default width and height in Jupyter notebook + printOptions.width = "640"; + printOptions.height = "480"; + + ## Parse "plot magic" commands. + ## https://github.com/Calysto/metakernel/blob/master/metakernel/ ... + ## magics/README.md#plot + for j = 1 : numel (obj.notebook.cells{cell_index}.source) + if (strncmpi (obj.notebook.cells{cell_index}.source{j}, "%plot", 5)) + magics = strsplit (strtrim ( + obj.notebook.cells{cell_index}.source{j})); + for i = 1 : numel (magics) + if (any (strcmp (magics{i}, {"-f", "--format"})) + && (i < numel (magics))) + printOptions.imageFormat = magics{i+1}; + endif + if (any (strcmp (magics{i}, {"-r", "--resolution"})) + && (i < numel (magics))) + printOptions.resolution = magics{i+1}; + endif + if (any (strcmp (magics{i}, {"-w", "--width"})) + && (i < numel (magics))) + printOptions.width = magics{i+1}; + endif + if (any (strcmp (magics{i}, {"-h", "--height"})) + && (i < numel (magics))) + printOptions.height = magics{i+1}; + endif + endfor + endif + endfor + + ## Remember previously opened figures. + fig_ids = findall (0, "type", "figure"); + + ## Create a new figure, if there are existing plots. + if (! isempty (fig_ids)) + newFig = figure (); + endif + + stream_output = struct ("name", "stdout", "output_type", "stream"); + + output_lines = obj.evalCode (strjoin ( + obj.notebook.cells{cell_index}.source)); + + if (! isempty(output_lines)) + stream_output.text = {output_lines}; + endif + + if (isfield (stream_output, "text")) + obj.notebook.cells{cell_index}.outputs{end + 1} = stream_output; + endif + + ## If there are existing plots and newFig is empty, delete it. + if (exist ("newFig") && isempty (get (newFig, "children"))) + delete (newFig); + endif + + ## Check for newly created figures. + fig_ids_new = setdiff (findall (0, "type", "figure"), fig_ids); + + if (numel (fig_ids_new) > 0) + if (exist ("__octave_jupyter_temp__", "dir")) + ## Delete open figures before raising the error. + for i = 1:numel (fig_ids_new) + delete (fig_ids_new(i)); + endfor + error (["jupyter_notebook: temporary directory ", ... + "__octave_jupyter_temp__ exists. Please remove it ", ... + "manually."]); + endif + + [status, msg, msgid] = mkdir ("__octave_jupyter_temp__"); + if (status == 0) + ## Delete open figures before raising the error. + for i = 1 : numel (fig_ids_new) + delete (fig_ids_new(i)); + endfor + error (["jupyter_notebook: Cannot create a temporary directory. ", ... + msg]); + endif + + for i = 1:numel (fig_ids_new) + figure (fig_ids_new(i), "visible", "off"); + obj.embedImage (cell_index, fig_ids_new (i), printOptions); + delete (fig_ids_new(i)); + endfor + + [status, msg, msgid] = rmdir ("__octave_jupyter_temp__"); + if (status == 0) + error (["jupyter_notebook: Cannot delete the temporary ", ... + "directory. ", msg]); + endif + endif + + endfunction + + + function run_all (obj) + + ## -*- texinfo -*- + ## @deftypefn {} {} run_all () + ## + ## Run all Jupyter Notebook cells and eventually replace previous + ## output cells in the object. + ## + ## Note: The code evaluation of the Jupyter Notebook cells is done + ## in a separate Jupyter Notebook context. Thus currently open + ## figures and workspace variables won't be affected by executing + ## this function. However, current workspace variables cannot be + ## accessed either. + ## + ## See @code{help jupyter_notebook} for examples. + ## + ## @end deftypefn + + if (nargin != 1) + print_usage (); + endif + + for i = 1:numel (obj.notebook.cells) + obj.run(i); + endfor + + endfunction + + endmethods + + + methods (Access = "private") + + function retVal = evalCode (__obj__, __code__) + + ## Evaluate the code string "__code__" using "evalc". + ## Before the code is evaluated, the previous notebook context is + ## loaded from "__obj__" and the new context is saved to that struct. + + if (nargin != 2) + print_usage (); + endif + + if (isempty (__code__)) + retVal = []; + return; + endif + + if (! (ischar (__code__) && isrow (__code__))) + error ("jupyter_notebook: code must be a string"); + endif + + __obj__.loadContext (); + + ## Add a statement to detect the value of the variable "ans" + __code__ = [__code__, "\nans"]; + + retVal = strtrim (evalc (__code__, ["printf (\"error: \"); ", ... + "printf (lasterror.message)"])); + + ## Handle the "ans" variable in the context. + start_index = rindex (retVal, "ans =") + 6; + if ((start_index > 6)) + if ((start_index <= length (retVal))) + end_index = start_index; + while ((retVal(end_index) != "\n") && (end_index < length (retVal))) + end_index += 1; + endwhile + __obj__.context.ans = retVal(start_index:end_index); + else + end_index = length (retVal); + __obj__.context.ans = ""; + endif + + ## Delete the output of the additional statement if the execution + ## is completed with no errors. + if (end_index == length (retVal)) + ## Remove the extra new line if there are other outputs with + ## the "ans" statement output + if (start_index == 7) + start_index = 1; + else + start_index = start_index - 7; + endif + retVal(start_index:end_index) = ""; + endif + endif + + __obj__.saveContext (); + + endfunction + + + function saveContext (obj, op) + + ## Save the context in private "obj" attribute. + + ## Handle the "ans" variable in the context. + obj.context = struct ("ans", obj.context.ans); + + forbidden_var_names = {"__code__", "__obj__", "ans"}; + + ## Get variable names. + var_names = {evalin("caller", "whos").name}; + + ## Store all variables to context. + for i = 1:length (var_names) + if (! any (strcmp (var_names{i}, forbidden_var_names))) + obj.context.(var_names{i}) = evalin ("caller", var_names{i}); + endif + endfor + + endfunction + + + function loadContext (obj) + + ## Load the context from private "obj" attribute. + for [val, key] = obj.context + assignin ("caller", key, val); + endfor + + endfunction + + + function embedImage (obj, cell_index, figHandle, printOptions) + + ## Embed images in the notebook. + ## + ## To support a new format: + ## 1. Create a new function that embeds the new format + ## (e.g. embed_svg_image). + ## 2. Add a new case to the switch-statement below. + + if (isempty (get (figHandle, "children"))) + error_text = {"The figure is empty!"}; + obj.addErrorOutput (cell_index, "The figure is empty!"); + return; + endif + + ## Check if the resolution is correct + if (isempty (str2num (printOptions.resolution))) + obj.addErrorOutput (cell_index, + "A number is required for resolution, not a string"); + return; + endif + + ## Check if the width is correct + if (isempty (str2num (printOptions.width))) + obj.addErrorOutput (cell_index, + "A number is required for width, not a string"); + return; + endif + + ## Check if the height is correct + if (isempty (str2num (printOptions.height))) + obj.addErrorOutput (cell_index, + "A number is required for height, not a string"); + return; + endif + + switch (lower (printOptions.imageFormat)) + case "png" + display_output = obj.embed_png_jpg_image (figHandle, + printOptions, "png"); + case "jpg" + display_output = obj.embed_png_jpg_image (figHandle, + printOptions, "jpg"); + case "svg" + display_output = obj.embed_svg_image (figHandle, printOptions); + otherwise + obj.addErrorOutput (cell_index, ["Cannot embed the \'", ... + printOptions.imageFormat, ... + "\' image format\n"]); + return; + endswitch + + obj.notebook.cells{cell_index}.outputs{end + 1} = display_output; + + endfunction + + + function dstruct = embed_png_jpg_image (obj, figHandle, printOptions, fmt) + + if (strcmp (fmt, "png")) + mime = "image/png"; + else + mime = "image/jpeg"; + endif + + image_path = fullfile ("__octave_jupyter_temp__", ["temp.", fmt]); + print (figHandle, image_path, ["-d", fmt], + ["-r" printOptions.resolution]); + + dstruct.output_type = "display_data"; + dstruct.metadata.(mime).width = printOptions.width; + dstruct.metadata.(mime).height = printOptions.height; + dstruct.data.("text/plain") = {"<IPython.core.display.Image object>"}; + dstruct.data.(mime) = base64_encode (uint8 (fileread (image_path))); + + delete (image_path); + + endfunction + + + function dstruct = embed_svg_image (obj, figHandle, printOptions) + + image_path = fullfile ("__octave_jupyter_temp__", "temp.svg"); + print (figHandle, image_path, "-dsvg", ["-r" printOptions.resolution]); + + dstruct.output_type = "display_data"; + dstruct.metadata = struct (); + dstruct.data.("text/plain") = {"<IPython.core.display.SVG object>"}; + dstruct.data.("image/svg+xml") = strsplit (fileread (image_path), "\n"); + + ## FIXME: The following is a workaround until we can properly print + ## SVG images in the right width and height. + ## Detect the <svg> tag. it is either the first or the second item + if (strncmpi (dstruct.data.("image/svg+xml"){1}, "<svg", 4)) + i = 1; + else + i = 2; + endif + + ## Embed the width and height in the image itself + svg_tag = dstruct.data.("image/svg+xml"){i}; + svg_tag = regexprep (svg_tag, "width=\"(.*?)\"", + ["width=\"" printOptions.width "px\""]); + svg_tag = regexprep (svg_tag, "height=\"(.*?)\"", + ["height=\"" printOptions.height "px\""]); + dstruct.data.("image/svg+xml"){i} = svg_tag; + + delete (image_path); + + endfunction + + + function addErrorOutput (obj, cell_index, error_msg) + + stream_output.name = "stderr"; + stream_output.output_type = "stream"; + stream_output.text = {error_msg}; + obj.notebook.cells{cell_index}.outputs{end + 1} = stream_output; + + endfunction + + endmethods + +endclassdef + +#!error jupyter_notebook ()
--- a/scripts/miscellaneous/module.mk Mon Nov 15 09:39:29 2021 +0100 +++ b/scripts/miscellaneous/module.mk Sat Nov 13 12:26:07 2021 -0500 @@ -40,7 +40,7 @@ %reldir%/ismethod.m \ %reldir%/ispc.m \ %reldir%/isunix.m \ - %reldir%/JupyterNotebook.m \ + %reldir%/jupyter_notebook.m \ %reldir%/license.m \ %reldir%/list_primes.m \ %reldir%/loadobj.m \
--- a/test/jupyter-notebook/JupyterNotebook.tst Mon Nov 15 09:39:29 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,200 +0,0 @@ -######################################################################## -## -## Copyright (C) 2021 The Octave Project Developers -## -## See the file COPYRIGHT.md in the top-level directory of this -## distribution or <https://octave.org/copyright/>. -## -## This file is part of Octave. -## -## Octave 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 3 of the License, or -## (at your option) any later version. -## -## Octave 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 Octave; see the file COPYING. If not, see -## <https://www.gnu.org/licenses/>. -## -######################################################################## - -## Test running a single cell -%!testif HAVE_RAPIDJSON -%! visibility = get (0, "defaultfigurevisible"); -%! toolkit = graphics_toolkit (); -%! unwind_protect -%! if (! __have_feature__ ("QT_OFFSCREEN") -%! || ! strcmp (graphics_toolkit (), "qt")) -%! try -%! graphics_toolkit ("gnuplot"); -%! catch -%! ## The system doesn't support gnuplot for drawing hidden -%! ## figures. Just return and have test marked as passing. -%! return; -%! end_try_catch -%! endif -%! set (0, "defaultfigurevisible", "off"); -%! -%! n = JupyterNotebook (fullfile ("octave_kernel.ipynb")); -%! -%! ## Test embedding images -%! n.run (2); -%! assert (n.notebook.cells{2}.outputs{1}.output_type, "display_data") -%! assert (isfield (n.notebook.cells{2}.outputs{1}.data, "image/png")); -%! assert (getfield (n.notebook.cells{2}.outputs{1}.data, "text/plain"), -%! {"<IPython.core.display.Image object>"}); -%! -%! ## Test running non-code cells -%! markdown_cell = n.notebook.cells{1}; -%! n.run (1); -%! assert (markdown_cell, n.notebook.cells{1}); -%! unwind_protect_cleanup -%! set (0, "defaultfigurevisible", visibility); -%! graphics_toolkit (toolkit); -%! end_unwind_protect - -## Test running all cells -%!testif HAVE_RAPIDJSON -%! visibility = get (0, "defaultfigurevisible"); -%! toolkit = graphics_toolkit (); -%! unwind_protect -%! if (! __have_feature__ ("QT_OFFSCREEN") -%! || ! strcmp (graphics_toolkit (), "qt")) -%! try -%! graphics_toolkit ("gnuplot"); -%! catch -%! ## The system doesn't support gnuplot for drawing hidden -%! ## figures. Just return and have test marked as passing. -%! return; -%! end_try_catch -%! endif -%! set (0, "defaultfigurevisible", "off"); -%! -%! n = JupyterNotebook (fullfile ("octave_kernel.ipynb")); -%! n.runAll (); -%! -%! ## Test embedding images -%! assert (n.notebook.cells{3}.outputs{1}.output_type, "display_data") -%! assert (isfield (n.notebook.cells{3}.outputs{1}.data, "image/png")); -%! assert (getfield (n.notebook.cells{3}.outputs{1}.data, "text/plain"), -%! {"<IPython.core.display.Image object>"}); -%! -%! ## Test running non-code cells -%! markdown_cell = n.notebook.cells{1}; -%! n.run (1); -%! assert (markdown_cell, n.notebook.cells{1}); -%! -%! ## Test embedding textual output -%! assert (n.notebook.cells{6}.outputs{1}.output_type, "stream") -%! assert (n.notebook.cells{6}.outputs{1}.name, "stdout"); -%! unwind_protect_cleanup -%! set (0, "defaultfigurevisible", visibility); -%! graphics_toolkit (toolkit); -%! end_unwind_protect - -## Test plot magic -%!testif HAVE_RAPIDJSON -%! visibility = get (0, "defaultfigurevisible"); -%! toolkit = graphics_toolkit (); -%! unwind_protect -%! if (! __have_feature__ ("QT_OFFSCREEN") -%! || ! strcmp (graphics_toolkit (), "qt")) -%! try -%! graphics_toolkit ("gnuplot"); -%! catch -%! ## The system doesn't support gnuplot for drawing hidden -%! ## figures. Just return and have test marked as passing. -%! return; -%! end_try_catch -%! endif -%! set (0, "defaultfigurevisible", "off"); -%! -%! n = JupyterNotebook (fullfile ("plot_magic_and_errors.ipynb")); -%! -%! ## PNG format -%! n.run (1); -%! assert (n.notebook.cells{1}.outputs{1}.output_type, "display_data") -%! assert (isfield (n.notebook.cells{1}.outputs{1}.data, "image/png")); -%! assert (getfield (n.notebook.cells{1}.outputs{1}.data, "text/plain"), -%! {"<IPython.core.display.Image object>"}); -%! -%! ## SVG format -%! n.run (2); -%! assert (n.notebook.cells{2}.outputs{1}.output_type, "display_data") -%! assert (isfield (n.notebook.cells{2}.outputs{1}.data, "image/svg+xml")); -%! assert (getfield (n.notebook.cells{2}.outputs{1}.data, "text/plain"), -%! {"<IPython.core.display.SVG object>"}); -%! -%! ## JPG format -%! n.run (3); -%! assert (n.notebook.cells{3}.outputs{1}.output_type, "display_data") -%! assert (isfield (n.notebook.cells{3}.outputs{1}.data, "image/jpeg")); -%! assert (getfield (n.notebook.cells{3}.outputs{1}.data, "text/plain"), -%! {"<IPython.core.display.Image object>"}); -%! unwind_protect_cleanup -%! set (0, "defaultfigurevisible", visibility); -%! graphics_toolkit (toolkit); -%! end_unwind_protect - -## Test errors -%!testif HAVE_RAPIDJSON -%! visibility = get (0, "defaultfigurevisible"); -%! toolkit = graphics_toolkit (); -%! unwind_protect -%! if (! __have_feature__ ("QT_OFFSCREEN") -%! || ! strcmp (graphics_toolkit (), "qt")) -%! try -%! graphics_toolkit ("gnuplot"); -%! catch -%! ## The system doesn't support gnuplot for drawing hidden -%! ## figures. Just return and have test marked as passing. -%! return; -%! end_try_catch -%! endif -%! set (0, "defaultfigurevisible", "off"); -%! -%! n = JupyterNotebook (fullfile ("plot_magic_and_errors.ipynb")); -%! -%! ## Wrong resolution -%! n.run (4); -%! assert (n.notebook.cells{4}.outputs{1}.output_type, "stream") -%! assert (n.notebook.cells{4}.outputs{1}.name, "stderr"); -%! assert (n.notebook.cells{4}.outputs{1}.text, -%! {"A number is required for resolution, not a string"}); -%! -%! ## Wrong width -%! n.run (5); -%! assert (n.notebook.cells{5}.outputs{1}.output_type, "stream") -%! assert (n.notebook.cells{5}.outputs{1}.name, "stderr"); -%! assert (n.notebook.cells{5}.outputs{1}.text, -%! {"A number is required for width, not a string"}); -%! -%! ## Wrong height -%! n.run (6); -%! assert (n.notebook.cells{6}.outputs{1}.output_type, "stream") -%! assert (n.notebook.cells{6}.outputs{1}.name, "stderr"); -%! assert (n.notebook.cells{6}.outputs{1}.text, -%! {"A number is required for height, not a string"}); -%! -%! ## Empty figure -%! n.run (7); -%! assert (n.notebook.cells{7}.outputs{1}.output_type, "stream") -%! assert (n.notebook.cells{7}.outputs{1}.name, "stderr"); -%! assert (n.notebook.cells{7}.outputs{1}.text, -%! {"The figure is empty!"}); -%! -%! ## Wrong format -%! n.run (8); -%! assert (n.notebook.cells{8}.outputs{1}.output_type, "stream") -%! assert (n.notebook.cells{8}.outputs{1}.name, "stderr"); -%! assert (n.notebook.cells{8}.outputs{1}.text, -%! {"Cannot embed the 'pdf' image format\n"}); -%! unwind_protect_cleanup -%! set (0, "defaultfigurevisible", visibility); -%! graphics_toolkit (toolkit); -%! end_unwind_protect
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jupyter-notebook/jupyter-notebook.tst Sat Nov 13 12:26:07 2021 -0500 @@ -0,0 +1,200 @@ +######################################################################## +## +## Copyright (C) 2021 The Octave Project Developers +## +## See the file COPYRIGHT.md in the top-level directory of this +## distribution or <https://octave.org/copyright/>. +## +## This file is part of Octave. +## +## Octave 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 3 of the License, or +## (at your option) any later version. +## +## Octave 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 Octave; see the file COPYING. If not, see +## <https://www.gnu.org/licenses/>. +## +######################################################################## + +## Test running a single cell +%!testif HAVE_RAPIDJSON +%! visibility = get (0, "defaultfigurevisible"); +%! toolkit = graphics_toolkit (); +%! unwind_protect +%! if (! __have_feature__ ("QT_OFFSCREEN") +%! || ! strcmp (graphics_toolkit (), "qt")) +%! try +%! graphics_toolkit ("gnuplot"); +%! catch +%! ## The system doesn't support gnuplot for drawing hidden +%! ## figures. Just return and have test marked as passing. +%! return; +%! end_try_catch +%! endif +%! set (0, "defaultfigurevisible", "off"); +%! +%! n = jupyter_notebook (fullfile ("octave_kernel.ipynb")); +%! +%! ## Test embedding images +%! n.run (2); +%! assert (n.notebook.cells{2}.outputs{1}.output_type, "display_data") +%! assert (isfield (n.notebook.cells{2}.outputs{1}.data, "image/png")); +%! assert (getfield (n.notebook.cells{2}.outputs{1}.data, "text/plain"), +%! {"<IPython.core.display.Image object>"}); +%! +%! ## Test running non-code cells +%! markdown_cell = n.notebook.cells{1}; +%! n.run (1); +%! assert (markdown_cell, n.notebook.cells{1}); +%! unwind_protect_cleanup +%! set (0, "defaultfigurevisible", visibility); +%! graphics_toolkit (toolkit); +%! end_unwind_protect + +## Test running all cells +%!testif HAVE_RAPIDJSON +%! visibility = get (0, "defaultfigurevisible"); +%! toolkit = graphics_toolkit (); +%! unwind_protect +%! if (! __have_feature__ ("QT_OFFSCREEN") +%! || ! strcmp (graphics_toolkit (), "qt")) +%! try +%! graphics_toolkit ("gnuplot"); +%! catch +%! ## The system doesn't support gnuplot for drawing hidden +%! ## figures. Just return and have test marked as passing. +%! return; +%! end_try_catch +%! endif +%! set (0, "defaultfigurevisible", "off"); +%! +%! n = jupyter_notebook (fullfile ("octave_kernel.ipynb")); +%! n.run_all (); +%! +%! ## Test embedding images +%! assert (n.notebook.cells{3}.outputs{1}.output_type, "display_data") +%! assert (isfield (n.notebook.cells{3}.outputs{1}.data, "image/png")); +%! assert (getfield (n.notebook.cells{3}.outputs{1}.data, "text/plain"), +%! {"<IPython.core.display.Image object>"}); +%! +%! ## Test running non-code cells +%! markdown_cell = n.notebook.cells{1}; +%! n.run (1); +%! assert (markdown_cell, n.notebook.cells{1}); +%! +%! ## Test embedding textual output +%! assert (n.notebook.cells{6}.outputs{1}.output_type, "stream") +%! assert (n.notebook.cells{6}.outputs{1}.name, "stdout"); +%! unwind_protect_cleanup +%! set (0, "defaultfigurevisible", visibility); +%! graphics_toolkit (toolkit); +%! end_unwind_protect + +## Test plot magic +%!testif HAVE_RAPIDJSON +%! visibility = get (0, "defaultfigurevisible"); +%! toolkit = graphics_toolkit (); +%! unwind_protect +%! if (! __have_feature__ ("QT_OFFSCREEN") +%! || ! strcmp (graphics_toolkit (), "qt")) +%! try +%! graphics_toolkit ("gnuplot"); +%! catch +%! ## The system doesn't support gnuplot for drawing hidden +%! ## figures. Just return and have test marked as passing. +%! return; +%! end_try_catch +%! endif +%! set (0, "defaultfigurevisible", "off"); +%! +%! n = jupyter_notebook (fullfile ("plot_magic_and_errors.ipynb")); +%! +%! ## PNG format +%! n.run (1); +%! assert (n.notebook.cells{1}.outputs{1}.output_type, "display_data") +%! assert (isfield (n.notebook.cells{1}.outputs{1}.data, "image/png")); +%! assert (getfield (n.notebook.cells{1}.outputs{1}.data, "text/plain"), +%! {"<IPython.core.display.Image object>"}); +%! +%! ## SVG format +%! n.run (2); +%! assert (n.notebook.cells{2}.outputs{1}.output_type, "display_data") +%! assert (isfield (n.notebook.cells{2}.outputs{1}.data, "image/svg+xml")); +%! assert (getfield (n.notebook.cells{2}.outputs{1}.data, "text/plain"), +%! {"<IPython.core.display.SVG object>"}); +%! +%! ## JPG format +%! n.run (3); +%! assert (n.notebook.cells{3}.outputs{1}.output_type, "display_data") +%! assert (isfield (n.notebook.cells{3}.outputs{1}.data, "image/jpeg")); +%! assert (getfield (n.notebook.cells{3}.outputs{1}.data, "text/plain"), +%! {"<IPython.core.display.Image object>"}); +%! unwind_protect_cleanup +%! set (0, "defaultfigurevisible", visibility); +%! graphics_toolkit (toolkit); +%! end_unwind_protect + +## Test errors +%!testif HAVE_RAPIDJSON +%! visibility = get (0, "defaultfigurevisible"); +%! toolkit = graphics_toolkit (); +%! unwind_protect +%! if (! __have_feature__ ("QT_OFFSCREEN") +%! || ! strcmp (graphics_toolkit (), "qt")) +%! try +%! graphics_toolkit ("gnuplot"); +%! catch +%! ## The system doesn't support gnuplot for drawing hidden +%! ## figures. Just return and have test marked as passing. +%! return; +%! end_try_catch +%! endif +%! set (0, "defaultfigurevisible", "off"); +%! +%! n = jupyter_notebook (fullfile ("plot_magic_and_errors.ipynb")); +%! +%! ## Wrong resolution +%! n.run (4); +%! assert (n.notebook.cells{4}.outputs{1}.output_type, "stream") +%! assert (n.notebook.cells{4}.outputs{1}.name, "stderr"); +%! assert (n.notebook.cells{4}.outputs{1}.text, +%! {"A number is required for resolution, not a string"}); +%! +%! ## Wrong width +%! n.run (5); +%! assert (n.notebook.cells{5}.outputs{1}.output_type, "stream") +%! assert (n.notebook.cells{5}.outputs{1}.name, "stderr"); +%! assert (n.notebook.cells{5}.outputs{1}.text, +%! {"A number is required for width, not a string"}); +%! +%! ## Wrong height +%! n.run (6); +%! assert (n.notebook.cells{6}.outputs{1}.output_type, "stream") +%! assert (n.notebook.cells{6}.outputs{1}.name, "stderr"); +%! assert (n.notebook.cells{6}.outputs{1}.text, +%! {"A number is required for height, not a string"}); +%! +%! ## Empty figure +%! n.run (7); +%! assert (n.notebook.cells{7}.outputs{1}.output_type, "stream") +%! assert (n.notebook.cells{7}.outputs{1}.name, "stderr"); +%! assert (n.notebook.cells{7}.outputs{1}.text, +%! {"The figure is empty!"}); +%! +%! ## Wrong format +%! n.run (8); +%! assert (n.notebook.cells{8}.outputs{1}.output_type, "stream") +%! assert (n.notebook.cells{8}.outputs{1}.name, "stderr"); +%! assert (n.notebook.cells{8}.outputs{1}.text, +%! {"Cannot embed the 'pdf' image format\n"}); +%! unwind_protect_cleanup +%! set (0, "defaultfigurevisible", visibility); +%! graphics_toolkit (toolkit); +%! end_unwind_protect
--- a/test/jupyter-notebook/module.mk Mon Nov 15 09:39:29 2021 +0100 +++ b/test/jupyter-notebook/module.mk Sat Nov 13 12:26:07 2021 -0500 @@ -1,5 +1,5 @@ jupyter_TEST_FILES = \ - %reldir%/JupyterNotebook.tst \ + %reldir%/jupyter-notebook.tst \ %reldir%/octave_kernel.ipynb \ %reldir%/plot_magic_and_errors.ipynb