Merge pull request #46 from WolframResearch/feature/add-hooks-and-improve-multi-expression-input-handling

Make common idioms for setting a persistent format type for output effective
This commit is contained in:
cc-wr 2019-06-20 11:02:24 -04:00 committed by GitHub
commit 75fc9c238d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 364 additions and 162 deletions

View File

@ -27,14 +27,14 @@ If[
files
*************************************)
Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* loopState *)
Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* loopState, applyHook, $canUseFrontEnd *)
(************************************
wrapper for interacting
with the cloud
*************************************)
(* Interact is wrapper, open to the user, for asking that the result of the evaluation
(* Interact is a wrapper, open to the user, for asking that the result of the evaluation
be displayed as an embedded cloud object that can be interacted with *)
(* set Interact to not evaluate its arguments *)
SetAttributes[Interact, HoldAll];
@ -46,6 +46,24 @@ If[
(* begin the private context for WolframLanguageForJupyter *)
Begin["`Private`"];
(************************************
helper utilities for
diagnosing and removing
command wrappers
(e.g., Interact[])
*************************************)
(* check if expr is wrapped with Interact *)
interactQ[expr___] := MatchQ[expr, Hold[Interact[___]]];
SetAttributes[interactQ, HoldAll];
(* remove any Interact wrappers,
apply $Pre,
and bring in the Front End for the evaluation of expr *)
uninteract[Interact[expr_]] := UsingFrontEnd[applyHook[$Pre, expr]];
uninteract[expr_] := UsingFrontEnd[applyHook[$Pre, expr]];
SetAttributes[uninteract, HoldAll];
(************************************
version of Print that
sends output to Jupyter
@ -78,21 +96,143 @@ If[
more easily intercept
*************************************)
Unprotect[Throw];
Throw[value_] :=
Throw[
value,
WolframLanguageForJupyter`Private`$ThrowLabel
];
Protect[Throw];
Unprotect[Throw];
Throw[value_] :=
Throw[
value,
WolframLanguageForJupyter`Private`$ThrowLabel
];
Protect[Throw];
Unprotect[Catch];
Catch[expr_] :=
Catch[
expr,
WolframLanguageForJupyter`Private`$ThrowLabel
];
Protect[Catch];
Unprotect[Catch];
Catch[expr_] :=
Catch[
expr,
WolframLanguageForJupyter`Private`$ThrowLabel
];
Protect[Catch];
(************************************
utilities for splitting
input by Wolfram Language
expression
*************************************)
(* start parsing the input, and return an Association for keeping track of said parse *)
startParsingInput[codeStr_] :=
Module[
{
(* an Association for keeping track of the parse containing:
"LinesLeft" - the lines of input left to be processed,
"ExpressionsParsed" - the number of expressions parsed out so far,
"ExpressionString" - an expression string generated by a pass of the parser,
"SyntaxError" - a flag for if the expression string contains a syntax error,
"ParseDone" - a flag for if the parse is done *)
parseTrackerInit
},
(* initialize parseTrackerInit *)
parseTrackerInit = Association[];
(* add the annotated lines left, and the defaults *)
AssociateTo[
parseTrackerInit,
{
"LinesLeft" -> StringSplit[StringJoin[" ", codeStr], "\r\n" | "\n"],
"ExpressionsParsed" -> 0,
"ExpressionString" -> " ",
"SyntaxError" -> False,
"ParseDone" -> False
}
];
(* return the parse tracker *)
Return[parseTrackerInit];
];
(* parse out an expression *)
parseIntoExpr[tracker_] :=
Module[
{
(* the parse tracker to be updated with the results of this parse *)
newTracker,
(* for storing the lines of input left to be processed, annotated with their line numbers *)
annotatedLinesLeft,
(* for storing the current line position when moving through annotatedLinesLeft *)
linePos,
(* for storing the result *)
result,
(* for storing the concatenation of some lines of input *)
exprStr
},
(* initialize the parse tracker to be updated *)
newTracker = tracker;
(* if there are no lines of input left to be processed,
set newTracker["ParseDone"] to True and return the updated tracker *)
If[Length[newTracker["LinesLeft"]] == 0,
newTracker["ParseDone"] = True;
(* return the updated tracker *)
Return[newTracker];
];
(* annotate the lines of input left to be processed with their line numbers *)
annotatedLinesLeft =
Partition[
Riffle[
newTracker["LinesLeft"],
Range[Length[newTracker["LinesLeft"]]]
],
2
];
(* start with a line position of 1 *)
linePos = 1;
result =
(* fold until an expression is built, or there are no lines of input left *)
Fold[
(
(* save the new line position *)
linePos = Last[{##}][[2]] + 1;
(* save the new expression string with a new line of input *)
exprStr = StringJoin[First /@ {##}];
(* if exprStr is syntactically correct, it represents a complete expression,
and folding can be stopped *)
If[SyntaxQ[exprStr],
(* return the complete expression string *)
Return[{exprStr, linePos, False}, Fold];,
(* exprStr may be incomplete, keep going if possible *)
{exprStr, linePos, True}
]
) &,
(* starting value *)
{"", -1, False},
(* the annotated lines of input left to be processed *)
annotatedLinesLeft
];
AssociateTo[
newTracker,
{
(* discard the lines of input processed *)
"LinesLeft" -> newTracker["LinesLeft"][[result[[2]];;]],
(* increment ExpressionsParsed *)
"ExpressionsParsed" -> newTracker["ExpressionsParsed"] + 1,
(* save the result generated *)
"ExpressionString" -> result[[1]],
(* save the syntactic correctness of the result *)
"SyntaxError" -> result[[3]]
}
];
(* return the updated tracker *)
Return[newTracker];
];
(************************************
main evaluation command
@ -100,7 +240,7 @@ Protect[Catch];
(* evaluate input, and capture required information such as generated messages *)
(* TODO: review other method: evaluate input through WSTP in another kernel *)
simulatedEvaluate[expr___] :=
simulatedEvaluate[codeStr_] :=
Module[
{
(* for saving $Messages before changing it *)
@ -110,11 +250,14 @@ Protect[Catch];
(* the string form of the generated messages, obtained from stream *)
generatedMessages,
(* the length of expr *)
exprLength,
(* for keeping track of parsing the input into separate expressions *)
parseTracker,
(* a new version of expr that simulates lines *)
exprWithLines,
(* a raw evaluation result to be built, before Nulls have been removed *)
rawEvaluationResult,
(* for storing a single expression string *)
exprStr,
(* the result of evaluation *)
evaluationResult,
@ -130,7 +273,8 @@ Protect[Catch];
the result of evaluation ("EvaluationResult"),
indices of the output lines of the result ("EvaluationResultOutputLineIndices"),
the total number of indices consumed by this evaluation ("ConsumedIndices"),
generated messages ("GeneratedMessages")
generated messages ("GeneratedMessages"),
if the input was one expression and wrapped with Interact[] ("InteractStatus")
*)
totalResult
},
@ -149,80 +293,116 @@ Protect[Catch];
(* set $Messages to use the new stream *)
$Messages = {stream};
(* obtain the length of expr *)
exprLength = Length[Unevaluated[{expr}]];
(* predefine the In[n] for the input *)
Unprotect[In];
(* for every line input in the input, set In[n], using placeholders *)
Table[
ReleaseHold[
(* replace index with the number of the input line,
and replace placeHolder with the element in expr corresponding to inIndex'th input line using Extract *)
Replace[
Hold[
SetDelayed[
In[index],
ReleaseHold[placeHolder]
]
],
{
index -> loopState["executionCount"] + inIndex - 1,
placeHolder ->
Extract[
Hold[{expr}],
{1, inIndex},
Hold
]
},
(* the level of index and placeHolder *)
{3}
]
];,
{inIndex, 1, exprLength}
];
Protect[In];
(* create a new version of expr that simulates lines *)
exprWithLines =
Table[
$Line++;
(* catch any Throws that were not handled by the input itself *)
intermediate =
Catch[
ReleaseHold[
Extract[
Hold[{expr}],
{1, inIndex},
Hold
]
],
_,
WolframLanguageForJupyter`Private`$ThrowNoCatch[#1, #2] &
];
If[
Head[intermediate] =!= WolframLanguageForJupyter`Private`$ThrowNoCatch,
(* if we did not catch anything, set result to intermediate *)
result = intermediate;,
(* if we did catch something, obtain the correct held form of the Throw to return, and message *)
If[intermediate[[2]] === WolframLanguageForJupyter`Private`$ThrowLabel,
result = Replace[Hold[Throw[placeHolder]], {placeHolder -> intermediate[[1]]}, {2}];,
result = Replace[Hold[Throw[placeHolder1, placeHolder2]], {placeHolder1 -> intermediate[[1]], placeHolder2 -> intermediate[[2]]}, {2}];
];
(* message *)
Message[
Throw::nocatch,
StringTrim[ToString[result, OutputForm], "Hold[" | "]"]
];
];
(* the overall result *)
result
,
{inIndex, 1, exprLength}
(* start the parse of the input *)
parseTracker =
startParsingInput[
(* apply $PreRead to the input *)
applyHook[$PreRead, codeStr]
];
(* initialize rawEvaluationResult to an empty list *)
rawEvaluationResult = {};
(* while the parse is not done, keep evaluating expressions in the input *)
While[
(
parseTracker = parseIntoExpr[parseTracker];
!parseTracker["ParseDone"]
),
(* save the current expression string *)
exprStr = parseTracker["ExpressionString"];
If[
!parseTracker["SyntaxError"],
(* increment $Line *)
$Line++;
(* set InString *)
Unprotect[InString];
InString[
loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1
] = exprStr;
Protect[InString];
];
(* catch any Throws that were not handled by the input itself *)
intermediate =
Catch[
(* evaluate the expression string *)
ToExpression[
exprStr,
InputForm,
uninteract
],
_,
WolframLanguageForJupyter`Private`$ThrowNoCatch[#1, #2] &
];
If[
Head[intermediate] =!= WolframLanguageForJupyter`Private`$ThrowNoCatch,
(* if we did not catch anything, set result to intermediate *)
result = intermediate;,
(* if we did catch something, obtain the correct held form of the Throw to return, and message *)
If[intermediate[[2]] === WolframLanguageForJupyter`Private`$ThrowLabel,
result = Replace[Hold[Throw[placeHolder]], {placeHolder -> intermediate[[1]]}, {2}];,
result = Replace[Hold[Throw[placeHolder1, placeHolder2]], {placeHolder1 -> intermediate[[1]], placeHolder2 -> intermediate[[2]]}, {2}];
];
(* message *)
Message[
Throw::nocatch,
StringTrim[ToString[result, OutputForm], "Hold[" | "]"]
];
];
If[
!parseTracker["SyntaxError"],
(* set the In[] for this expression *)
Unprotect[In];
Replace[
ToExpression[exprStr, InputForm, Hold],
Hold[held_] :>
SetDelayed[
In[
loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1
],
held
]
];
Protect[In];
(* apply $Post to the result *)
result = applyHook[$Post, result];
(* set the Out[] for this expression *)
Unprotect[Out];
Out[loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1] = result;
Protect[Out];
(* create the overall result with $PrePrint *)
result = applyHook[$PrePrint, result];
,
(* syntax error *)
result = $Failed;
];
(* save the result in rawEvaluationResult *)
AppendTo[rawEvaluationResult, result];
];
(* add the Interact[] wrapper status of the input *)
AssociateTo[
totalResult,
"InteractStatus" ->
(
(* if the input has no syntax errors,
is made up of only one expression,
and is wrapped with Interact[],
mark "InteractStatus" as True
*)
parseTracker["ExpressionsParsed"] == 1 &&
!parseTracker["SyntaxError"] &&
(interactQ @ ToExpression[parseTracker["ExpressionString"], InputForm, Hold])
)
];
(* evaluate the input from Jupyter, removing Nulls from the Output *)
evaluationResult = DeleteCases[exprWithLines, Null];
evaluationResult = DeleteCases[rawEvaluationResult, Null];
(* preserve the locations of the output lines with respect to the Nulls *)
AssociateTo[
@ -230,19 +410,10 @@ Protect[Catch];
"EvaluationResultOutputLineIndices" ->
(
(loopState["executionCount"] - 1) +
Flatten[Position[exprWithLines, Except[Null], {1}, Heads -> False]]
Flatten[Position[rawEvaluationResult, Except[Null], {1}, Heads -> False]]
)
];
(* set Out[n] *)
Unprotect[Out];
(* for every output line, set the Out with the corresponding index *)
Table[
Out[loopState["executionCount"] + outIndex - 1] = exprWithLines[[outIndex]];,
{outIndex, 1, Length[exprWithLines]}
];
Protect[Out];
(* restore $Messages *)
$Messages = oldMessages;
@ -254,7 +425,13 @@ Protect[Catch];
(* add the total number of indices consumed by this evaluation *)
AssociateTo[
totalResult,
"ConsumedIndices" -> exprLength
"ConsumedIndices" ->
(* if parseTracker["SyntaxError"] is true, one less index was consumed *)
If[
parseTracker["SyntaxError"],
parseTracker["ExpressionsParsed"] - 1,
parseTracker["ExpressionsParsed"]
]
];
(* add the result of the evaluation and any generated messages to totalResult *)

View File

@ -5,6 +5,11 @@ Description:
Initialization for
WolframLanguageForJupyter
Symbols defined:
loopState,
applyHook,
$canUseFrontEnd,
$outputSetToTraditionalForm,
$trueFormatType,
connectionAssoc,
bannerWarning,
keyString,
@ -84,6 +89,17 @@ If[
"printFunction" -> Function[#;]
];
(* helper utility for applying hooks if they are set *)
applyHook[hook_, value_] /; Length[OwnValues[hook]] != 0 := hook[value];
applyHook[hook_, value_] := value;
Attributes[applyHook] := HoldAll;
(* can we use the Front End? *)
$canUseFrontEnd := (UsingFrontEnd[$FrontEnd] =!= Null);
$outputSetToTraditionalForm := (Lookup[Options[$Output], FormatType] === TraditionalForm);
$trueFormatType := If[$outputSetToTraditionalForm, TraditionalForm, #&];
(* obtain details on how to connect to Jupyter, from Jupyter's invocation of "KernelForWolframLanguageForJupyter.wl" *)
connectionAssoc = ToString /@ Association[Import[$CommandLine[[4]], "JSON"]];

View File

@ -6,7 +6,7 @@ Description:
Entry point for WolframLanguageForJupyter
kernels started by Jupyter
Symbols defined:
loopState
loop
*************************************************)
(************************************
@ -180,6 +180,7 @@ End[]; (* `Private` *)
(* end the WolframLanguageForJupyter package *)
EndPackage[]; (* WolframLanguageForJupyter` *)
(* $ContextPath = DeleteCases[$ContextPath, "WolframLanguageForJupyter`"]; *)
(************************************
evaluate loop[]

View File

@ -22,6 +22,15 @@ If[
WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities = True;
(************************************
load required
WolframLanguageForJupyter
files
*************************************)
Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* $canUseFrontEnd, $outputSetToTraditionalForm
$trueFormatType *)
(************************************
private symbols
*************************************)
@ -29,18 +38,40 @@ If[
(* begin the private context for WolframLanguageForJupyter *)
Begin["`Private`"];
(************************************
helper utility for converting
an expression into a
textual form
*************************************)
(* convert an expression into a textual form,
using as much of the options already set for $Output as possible for ToString *)
toStringUsingOutput[expr_] :=
ToString[
expr,
Sequence @@
Cases[
Options[$Output],
Verbatim[Rule][opt_, val_] /;
MemberQ[
Keys[Options[ToString]],
opt
]
]
];
(************************************
helper utility for determining
if a result should be
displayed as text or an image
*************************************)
(* check if a string contains any private use area characters *)
containsPUAQ[str_] :=
AnyTrue[
ToCharacterCode[str, "Unicode"],
(57344 <= #1 <= 63743 || 983040 <= #1 <= 1048575 || 1048576 <= #1 <= 1114111) &
];
(* check if a string contains any private use area characters *)
containsPUAQ[str_] :=
AnyTrue[
ToCharacterCode[str, "Unicode"],
(57344 <= #1 <= 63743 || 983040 <= #1 <= 1048575 || 1048576 <= #1 <= 1114111) &
];
(************************************
utility for determining if a
@ -61,16 +92,26 @@ containsPUAQ[str_] :=
(* if we cannot use the frontend, use text *)
If[
UsingFrontEnd[$FrontEnd] === Null,
!$canUseFrontEnd,
Return[True];
];
(* if the expression is wrapped with InputForm or OutputForm, automatically format as text *)
(* save the head of the expression *)
exprHead = Head[expr];
(* if the expression is wrapped with InputForm or OutputForm,
automatically format as text *)
If[exprHead === InputForm || exprHead === OutputForm,
Return[True]
];
(* if the FormatType of $Output is set to TraditionalForm,
or if the expression is wrapped with TraditionalForm,
do not use text *)
If[$outputSetToTraditionalForm || exprHead === TraditionalForm,
Return[False]
];
(* breakdown expr into atomic objects organized by their Head *)
pObjects =
GroupBy[
@ -103,10 +144,10 @@ containsPUAQ[str_] :=
If[
ContainsOnly[Keys[pObjects], {Integer, Real, String, Symbol}],
Return[
AllTrue[
Lookup[pObjects, String, {}],
(!containsPUAQ[ReleaseHold[#1]]) &
] &&
AllTrue[
Lookup[pObjects, String, {}],
(!containsPUAQ[ReleaseHold[#1]]) &
] &&
AllTrue[
Lookup[pObjects, Symbol, {}],
(
@ -140,7 +181,12 @@ containsPUAQ[str_] :=
(* 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 *)
StringJoin[{"&#", ToString[#1], ";"} & /@ ToCharacterCode[ToString[result], "Unicode"]],
StringJoin[{"&#", ToString[#1], ";"} & /@
ToCharacterCode[
(* toStringUsingOutput[result] *) ToString[result],
"Unicode"
]
],
(* end the element *)
"</pre>"
];
@ -153,7 +199,8 @@ containsPUAQ[str_] :=
(* the rasterized form of the result, converted to base64 *)
BaseEncode[
UsingFrontEnd[ExportByteArray[
If[Head[result] === Manipulate, result, Rasterize[result]],
(If[Head[#1] === Manipulate, #1, Rasterize[#1]] &) @
$trueFormatType[result],
"PNG"
]]
],

View File

@ -5,8 +5,6 @@ Description:
Handlers for message frames of type
"x_request" arriving from Jupyter
Symbols defined:
interactQ,
uninteract,
executeRequestHandler
*************************************************)
@ -41,25 +39,6 @@ If[
(* begin the private context for WolframLanguageForJupyter *)
Begin["`Private`"];
(************************************
helper utilities for the
handler for
execute_requests for
diagnosing command
wrappers (Interact[___])
and redirecting to
simulatedEvaluate
*************************************)
(* check if expr is wrapped with Interact *)
interactQ[expr___] := MatchQ[expr, Hold[Interact[___]]];
SetAttributes[interactQ, HoldAll];
(* remove any Interact wrappers, and evaluate expr *)
uninteract[Interact[expr___]] := simulatedEvaluate[expr];
uninteract[expr___] := simulatedEvaluate[expr];
SetAttributes[uninteract, HoldAll];
(************************************
handler for execute_requests
*************************************)
@ -131,30 +110,11 @@ If[
);
(* evaluate the input, and store the total result in totalResult *)
totalResult = ToExpression[loopState["frameAssoc"]["content"]["code"], InputForm, uninteract];
totalResult = simulatedEvaluate[loopState["frameAssoc"]["content"]["code"]];
(* restore printFunction to empty *)
loopState["printFunction"] = Function[#;];
(* if totalResult fails (most likely due to a syntax error in the input),
build a failure object form of totalResult, and use any messages generated by ToExpression *)
If[FailureQ[totalResult],
totalResult = Association[];
(* failed object settings: *)
totalResult["EvaluationResult"] = {$Failed};
totalResult["EvaluationResultOutputLineIndices"] = {loopState["executionCount"]};
(* when a failure like this occurs, do not increment loopState["executionCount"] *)
totalResult["ConsumedIndices"] = 0;
(* capture any messages generated by ToExpression *)
totalResult["GeneratedMessages"] =
StringJoin[
EvaluationData[
ToExpression[loopState["frameAssoc"]["content"]["code"], InputForm]
]["MessagesText"]
];
];
(* generate an HTML form of the message text *)
errorMessage =
If[StringLength[totalResult["GeneratedMessages"]] == 0,
@ -185,8 +145,9 @@ If[
(* format output as purely text, image, or cloud interface *)
If[
(* interactQ checks if the input was wrapped with Interact, which is used when the output should be displayed as an embedded cloud object *)
TrueQ[interactQ[ToExpression[loopState["frameAssoc"]["content"]["code"], InputForm, Hold]]] &&
(* check if the input was wrapped with Interact,
which is used when the output should be displayed as an embedded cloud object *)
TrueQ[totalResult["InteractStatus"]] &&
(* check if we are logged into the Cloud *)
$CloudConnected,
(* prepare the content for a reply message frame to be sent on the IO Publish socket *)