Add jupyter-console support, and add redirection of Wolfram Language messages during the execution of an input

This commit is contained in:
cc-wr 2019-11-24 19:12:30 -05:00
parent ebce0fa0fa
commit 7bc5ee8ae6
5 changed files with 313 additions and 126 deletions

View File

@ -9,6 +9,8 @@ Symbols defined:
Print,
Throw,
Catch,
redirectPrint,
redirectMessages,
simulatedEvaluate
*************************************************)
@ -72,7 +74,7 @@ If[
(* redirect Print calls into a message to Jupyter, in order to print in the Jupyter notebook *)
(* TODO: review other methods: through EvaluationData or WSTP so we don't redefine Print *)
Unprotect[Print];
Print[args___, opts:OptionsPattern[]] :=
Print[ourArgs___, opts:OptionsPattern[]] :=
Block[
{
$inPrint=True,
@ -80,7 +82,7 @@ If[
},
If[
!FailureQ[First[$Output]],
Print[args, opts];
Print[ourArgs, opts];
loopState["printFunction"][
Import[$Output[[1,1]], "String"]
];
@ -112,6 +114,96 @@ If[
];
Protect[Catch];
(************************************
redirection utilities
*************************************)
(* redirect Print to Jupyter *)
redirectPrint[sourceFrame_, printText_] :=
(* send a frame *)
sendFrame[
(* on the IO Publish socket *)
ioPubSocket,
(* create the frame *)
createReplyFrame[
(* using the current source frame *)
currentSourceFrame,
(* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#streams-stdout-stderr-etc *)
(* with a message type of "stream" *)
"stream",
(* and with message content that tells Jupyter what to Print, and to use stdout *)
ExportString[
Association[
"name" -> "stdout",
"text" -> printText
],
"JSON",
"Compact" -> True
],
(* and without branching off *)
False
]
];
(* redirect messages to Jupyter *)
redirectMessages[currentSourceFrame_, messageName_, messageText_, addNewline_, dropMessageName_:False] :=
Module[
{
(* string forms of the arguments messageName and messageText *)
messageNameString,
messageTextString
},
(* generate string forms of the arguments *)
messageNameString = ToString[messageName];
messageTextString = ToString[messageText];
(* send a frame *)
sendFrame[
(* on the IO Publish socket *)
ioPubSocket,
(* create the frame *)
createReplyFrame[
(* using the current source frame *)
currentSourceFrame,
(* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#execution-errors *)
(* with a message type of "error" *)
"error",
(* and with appropriate message content *)
ExportString[
Association[
(* use the provided message name here *)
"ename" -> messageNameString,
(* use the provided message text here *)
"evalue" -> messageTextString,
(* use the provided message name and message text here (unless dropMessageName) *)
"traceback" ->
{
(* output the message in red *)
StringJoin[
"\033[0;31m",
If[
dropMessageName,
(* use only the message text here if dropMessageName is True *)
messageTextString,
(* otherwise, combine the message name and message text *)
ToString[System`ColonForm[messageName, messageText]]
],
(* if addNewline, add a newline *)
If[addNewline, "\n", ""],
"\033[0m"
]
}
],
"JSON",
"Compact" -> True
],
(* and without branching off *)
False
]
];
(* ... and return an empty string to the Wolfram Language message system *)
Return[""];
];
(************************************
utilities for splitting
input by Wolfram Language

View File

@ -185,6 +185,13 @@ If[
(* flag for if WolframLanguageForJupyter should shut down *)
"doShutdown" -> False,
(* flag for whether to redirect messages, or to, at the end of the execution of an input,
bundle them with the result *)
"redirectMessages" -> True,
(* flag for if an is_complete_request has ever been sent to the kernel *)
"isCompleteRequestSent" -> False,
(* local to an iteration *)
(* a received frame as an Association *)
"frameAssoc" -> Null,

View File

@ -36,7 +36,7 @@ Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* init
Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* sendFrame *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "MessagingUtilities.wl"}]]; (* getFrameAssoc, createReplyFrame *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "RequestHandlers.wl"}]]; (* executeRequestHandler, completeRequestHandler *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "RequestHandlers.wl"}]]; (* isCompleteRequestHandler, executeRequestHandler, completeRequestHandler *)
(************************************
private symbols
@ -87,11 +87,10 @@ loop[] :=
bannerWarning,
"\"}"
];,
(* if asking if the input is complete, respond "unknown" *)
(* if asking if the input is complete (relevant for jupyter-console), respond appropriately *)
"is_complete_request",
(* TODO: add syntax-Q checking *)
loopState["replyMsgType"] = "is_complete_reply";
loopState["replyContent"] = "{\"status\":\"unknown\"}";,
(* isCompleteRequestHandler will read and update loopState *)
isCompleteRequestHandler[];,
(* if asking the kernel to execute something, use executeRequestHandler *)
"execute_request",
(* executeRequestHandler will read and update loopState *)
@ -140,9 +139,10 @@ loop[] :=
sendFrame[shellSocket, shellReplyFrame];
(* if an ioPubReplyFrame was created, send it on the IO Publish socket *)
If[!(loopState["ioPubReplyFrame"] === Association[]),
If[
loopState["ioPubReplyFrame"] =!= Association[],
sendFrame[ioPubSocket, loopState["ioPubReplyFrame"]];
(* reset ioPubReplyFrame *)
(* -- also, reset ioPubReplyFrame *)
loopState["ioPubReplyFrame"] = Association[];
];

View File

@ -9,9 +9,10 @@ Description:
notebooks
Symbols defined:
textQ,
toOutText,
toText,
toOutTextHTML,
toImageData,
toOutImage
toOutImageHTML
*************************************************)
(************************************
@ -178,20 +179,23 @@ If[
results as text and images
*************************************)
(* generate the textual form of a result *)
(* NOTE: the OutputForm (which ToString uses) of any expressions wrapped with, say, InputForm should
be identical to the string result of an InputForm-wrapped expression itself *)
toText[result_] := ToString[If[Head[result] =!= TeXForm, $trueFormatType[result], result]];
(* generate HTML for the textual form of a result *)
toOutText[result_] :=
toOutTextHTML[result_] :=
Module[
{isTeXWrapped, isTeXFinal},
(* check if this result is wrapped with TeXForm *)
isTeXWrapped = (Head[result] === TeXForm);
(* check if this result should be marked, in the end, as TeX *)
isTeXFinal = isTeXWrapped || $outputSetToTeXForm;
{isTeX},
(* check if this result should be marked as TeX *)
isTeX = (Head[result] === TeXForm) || $outputSetToTeXForm;
Return[
StringJoin[
(* mark this result as preformatted only if it isn't TeX *)
If[
!isTeXFinal,
!isTeX,
{
(* preformatted *)
"<pre style=\"",
@ -203,23 +207,21 @@ If[
],
(* mark the text as TeX, if is TeX *)
If[isTeXFinal, "&#36;&#36;", ""],
If[isTeX, "&#36;&#36;", ""],
(* the textual form of the result *)
(* NOTE: the OutputForm (which ToString uses) of any expressions wrapped with, say, InputForm should
be identical to the string result of an InputForm-wrapped expression itself *)
({"&#", ToString[#1], ";"} & /@
ToCharacterCode[
(* toStringUsingOutput[result] *) ToString[If[!isTeXWrapped, $trueFormatType[result], result]],
(* toStringUsingOutput[result] *) toText[result],
"Unicode"
]),
(* mark the text as TeX, if is TeX *)
If[isTeXFinal, "&#36;&#36;", ""],
If[isTeX, "&#36;&#36;", ""],
(* mark this result as preformatted only if it isn't TeX *)
If[
!isTeXFinal,
!isTeX,
{
(* end the element *)
"</pre>"
@ -259,7 +261,7 @@ If[
];
(* generate HTML for the rasterized form of a result *)
toOutImage[result_] :=
toOutImageHTML[result_] :=
Module[
{
(* the rasterization of result *)

View File

@ -2,9 +2,10 @@
RequestHandlers.wl
*************************************************
Description:
Handlers for message frames of type
"x_request" arriving from Jupyter
Handlers for message frames of the form
"*_request" arriving from Jupyter
Symbols defined:
isCompleteRequestHandler,
executeRequestHandler,
completeRequestHandler
*************************************************)
@ -29,10 +30,10 @@ If[
Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* sendFrame *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "MessagingUtilities.wl"}]]; (* createReplyFrame *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "EvaluationUtilities.wl"}]]; (* simulatedEvaluate *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "EvaluationUtilities.wl"}]]; (* redirectPrint, redirectMessages, simulatedEvaluate *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "OutputHandlingUtilities.wl"}]]; (* textQ, toOutText, toOutImage,
containsPUAQ *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "OutputHandlingUtilities.wl"}]]; (* textQ, toOutTextHTML, toOutImageHTML,
toText, containsPUAQ *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "CompletionUtilities.wl"}]]; (* rewriteNamedCharacters *)
@ -43,11 +44,58 @@ If[
(* begin the private context for WolframLanguageForJupyter *)
Begin["`Private`"];
(************************************
handler for is_complete_requests
*************************************)
(* handle is_complete_request message frames received on the shell socket *)
isCompleteRequestHandler[] :=
Module[
{
(* the length of the code string to check completeness for *)
stringLength,
(* the value returned by SyntaxLength[] on the code string to check completeness for *)
syntaxLength
},
(* mark loopState["isCompleteRequestSent"] as True *)
loopState["isCompleteRequestSent"] = True;
(* set the appropriate reply type *)
loopState["replyMsgType"] = "is_complete_reply";
(* determine the length of the code string *)
stringLength = StringLength[loopState["frameAssoc"]["content"]["code"]];
(* determine the SyntaxLength[] value for the code string *)
syntaxLength = SyntaxLength[loopState["frameAssoc"]["content"]["code"]];
(* test the value of syntaxLength to determine the completeness of the code string,
setting the content of the reply appropriately *)
Which[
(* if the above values could not be correctly determined,
the completeness status of the code string is unknown *)
!IntegerQ[stringLength] || !IntegerQ[syntaxLength],
loopState["replyContent"] = "{\"status\":\"unknown\"}";,
(* if the SyntaxLength[] value for a code string is greater than its actual length,
the code string is incomplete *)
syntaxLength > stringLength,
loopState["replyContent"] = "{\"status\":\"incomplete\"}";,
(* if the SyntaxLength[] value for a code string is less than its actual length,
the code string contains a syntax error (or is "invalid") *)
syntaxLength < stringLength,
loopState["replyContent"] = "{\"status\":\"invalid\"}";,
(* if the SyntaxLength[] value for a code string is equal to its actual length,
the code string is complete and correct *)
syntaxLength == stringLength,
loopState["replyContent"] = "{\"status\":\"complete\"}";
];
];
(************************************
handler for execute_requests
*************************************)
(* handle execute_request messages frames received on the shell socket *)
(* handle execute_request message frames received on the shell socket *)
executeRequestHandler[] :=
Module[
{
@ -64,7 +112,10 @@ If[
the total number of indices consumed by this evaluation ("ConsumedIndices"),
generated messages ("GeneratedMessages")
*)
totalResult
totalResult,
(* flag for if there are any unreported error messages after execution of the input *)
unreportedErrorMessages
},
(* set the appropriate reply type *)
@ -83,34 +134,31 @@ If[
];
(* redirect Print so that it prints in the Jupyter notebook *)
loopState["printFunction"] =
(
(* send a frame *)
sendFrame[
(* on the IO Publish socket *)
ioPubSocket,
(* create the frame *)
createReplyFrame[
(* using the current source frame *)
loopState["frameAssoc"],
(* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#streams-stdout-stderr-etc *)
(* with a message type of "stream" *)
"stream",
(* and with message content that tells Jupyter what to Print, and to use stdout *)
ExportString[
Association[
"name" -> "stdout",
"text" -> #1
],
"JSON",
"Compact" -> True
],
(* and without branching off *)
False
loopState["printFunction"] = (redirectPrint[loopState["frameAssoc"], #1] &);
(* if an is_complete_request has been sent, assume jupyter-console is running the kernel,
and redirect messages *)
If[
loopState["isCompleteRequestSent"],
loopState["redirectMessages"] = True;
];
(* if loopState["redirectMessages"] is True,
update Jupyter explicitly with any errors that occur DURING the execution of the input *)
If[
loopState["redirectMessages"],
Internal`$MessageFormatter =
(
redirectMessages[
loopState["frameAssoc"],
#1,
#2,
(* add a newline if loopState["isCompleteRequestSent"] *)
loopState["isCompleteRequestSent"]
]
]
&
);
&
);
];
(* evaluate the input, and store the total result in totalResult *)
totalResult = simulatedEvaluate[loopState["frameAssoc"]["content"]["code"]];
@ -118,17 +166,27 @@ If[
(* restore printFunction to empty *)
loopState["printFunction"] = Function[#;];
(* check if there are any unreported error messages *)
unreportedErrorMessages =
(
(* ... because messages are not being redirected *)
(!loopState["redirectMessages"]) &&
(* ... and because at least one message was generated *)
(StringLength[totalResult["GeneratedMessages"]] > 0)
);
(* generate an HTML form of the message text *)
errorMessage =
If[StringLength[totalResult["GeneratedMessages"]] == 0,
(* if there are no messages, no need to format anything *)
If[
!unreportedErrorMessages,
(* if there are no unreported error messages, there is no need to format them *)
{},
(* build the HTML form of the message text *)
{
(* preformatted *)
"<pre style=\"",
(* the color of the text should be red, and should use Courier *)
StringJoin[{"&#",ToString[#1], ";"} & /@ ToCharacterCode["color:red; font-family: \"Courier New\",Courier,monospace;", "UTF-8"]],
StringJoin[{"&#", ToString[#1], ";"} & /@ ToCharacterCode["color:red; font-family: \"Courier New\",Courier,monospace;", "UTF-8"]],
(* end pre tag *)
"\">",
(* the generated messages *)
@ -141,6 +199,18 @@ If[
(* if there are no results, do not send anything on the IO Publish socket and return *)
If[
Length[totalResult["EvaluationResultOutputLineIndices"]] == 0,
(* send any unreported error messages *)
If[unreportedErrorMessages,
redirectMessages[
loopState["frameAssoc"],
"",
totalResult["GeneratedMessages"],
(* do not add a newline *)
False,
(* drop message name *)
True
];
];
(* increment loopState["executionCount"] as needed *)
loopState["executionCount"] += totalResult["ConsumedIndices"];
Return[];
@ -161,99 +231,115 @@ If[
"execution_count" -> First[totalResult["EvaluationResultOutputLineIndices"]],
(* HTML code to embed output uploaded to the Cloud in the Jupyter notebook *)
"data" ->
{"text/html" ->
StringJoin[
(* display any generated messages as inlined PNG images encoded in base64 *)
"<div><img alt=\"\" src=\"data:image/png;base64,",
(* rasterize the generated messages in a dark red color, and convert the resulting image to base64*)
BaseEncode[ExportByteArray[Rasterize[Style[totalResult["GeneratedMessages"], Darker[Red]]], "PNG"]],
(* end image element *)
"\">",
(* embed the cloud object *)
EmbedCode[CloudDeploy[totalResult["EvaluationResult"]], "HTML"][[1]]["CodeSection"]["Content"],
(* end the whole element *)
"</div>"
]
{
"text/html" ->
StringJoin[
(* display any generated messages as inlined PNG images encoded in base64 *)
"<div><img alt=\"\" src=\"data:image/png;base64,",
(* rasterize the generated messages in a dark red color, and convert the resulting image to base64*)
BaseEncode[ExportByteArray[Rasterize[Style[totalResult["GeneratedMessages"], Darker[Red]]], "PNG"]],
(* end image element *)
"\">",
(* embed the cloud object *)
EmbedCode[CloudDeploy[totalResult["EvaluationResult"]], "HTML"][[1]]["CodeSection"]["Content"],
(* end the whole element *)
"</div>"
],
"text/plain" -> ""
},
(* no metadata *)
"metadata" -> {"text/html" -> {}}
"metadata" -> {"text/html" -> {}, "text/plain" -> {}}
],
"JSON",
"Compact" -> True
];
,
(* if every output line can be formatted as text, use a function that converts the output to text *)
(* TODO: allow for mixing text and image results *)
(* otherwise, use a function that converts the output to an image *)
(* TODO: allow for mixing text and image results *)
If[AllTrue[totalResult["EvaluationResult"], textQ],
toOut = toOutText,
toOut = toOutImage
toOut = toOutTextHTML,
toOut = toOutImageHTML
];
(* prepare the content for a reply message frame to be sent on the IO Publish socket *)
ioPubReplyContent = ExportString[
Association[
(* the first output index *)
"execution_count" -> First[totalResult["EvaluationResultOutputLineIndices"]],
(* generate HTML of results and messages *)
(* the data representing the results and messages *)
"data" ->
{"text/html" ->
(* output the results in a grid *)
If[
Length[totalResult["EvaluationResult"]] > 1,
StringJoin[
(* add grid style *)
"<style>
.grid-container {
display: inline-grid;
grid-template-columns: auto;
}
</style>
{
(* generate HTML for the results and messages *)
"text/html" ->
(* output the results in a grid *)
If[
Length[totalResult["EvaluationResult"]] > 1,
StringJoin[
(* add grid style *)
"<style>
.grid-container {
display: inline-grid;
grid-template-columns: auto;
}
</style>
<div>",
(* display error message *)
errorMessage,
(* start the grid *)
"<div class=\"grid-container\">",
(* display the output lines *)
<div>",
(* display error message *)
errorMessage,
(* start the grid *)
"<div class=\"grid-container\">",
(* display the output lines *)
Table[
{
(* start the grid item *)
"<div class=\"grid-item\">",
(* show the output line *)
toOut[totalResult["EvaluationResult"][[outIndex]]],
(* end the grid item *)
"</div>"
},
{outIndex, 1, Length[totalResult["EvaluationResult"]]}
],
(* end the element *)
"</div></div>"
],
StringJoin[
(* start the element *)
"<div>",
(* display error message *)
errorMessage,
(* if there are messages, but no results, do not display a result *)
If[
Length[totalResult["EvaluationResult"]] == 0,
"",
(* otherwise, display a result *)
toOut[First[totalResult["EvaluationResult"]]]
],
(* end the element *)
"</div>"
]
],
(* provide, as a backup, plain text for the results *)
"text/plain" ->
StringJoin[
Table[
{
(* start the grid item *)
"<div class=\"grid-item\">",
(* show the output line *)
toOut[totalResult["EvaluationResult"][[outIndex]]],
(* end the grid item *)
"</div>"
toText[totalResult["EvaluationResult"][[outIndex]]],
(* -- also, suppress newline if this is the last result *)
If[outIndex != Length[totalResult["EvaluationResult"]], "\n", ""]
},
{outIndex, 1, Length[totalResult["EvaluationResult"]]}
],
(* end the element *)
"</div></div>"
],
StringJoin[
(* start the element *)
"<div>",
(* display error message *)
errorMessage,
(* if there are messages, but no results, do not display a result *)
If[
Length[totalResult["EvaluationResult"]] == 0,
"",
(* otherwise, display a result *)
toOut[First[totalResult["EvaluationResult"]]]
],
(* end the element *)
"</div>"
]
]
]
},
(* no metadata *)
"metadata" -> {"text/html" -> {}}
"metadata" -> {"text/html" -> {}, "text/plain" -> {}}
],
"JSON",
"Compact" -> True
];
];
(* create frame from ioPubReplyContent *)
loopState["ioPubReplyFrame"] =
createReplyFrame[
@ -275,7 +361,7 @@ If[
handler for complete_requests
*************************************)
(* handle complete_request messages frames received on the shell socket *)
(* handle complete_request message frames received on the shell socket *)
completeRequestHandler[] :=
Module[
{