",
133 | "header" -> ExportString[
134 | Append[
135 | header,
136 | {"date" -> DateString["ISODateTime"], "msg_type" -> replyType, "msg_id" -> StringInsert[StringReplace[CreateUUID[], "-" -> ""], "-", 9]}
137 | ],
138 | "JSON",
139 | "Compact" -> True
140 | ],
141 | "pheader" -> If[branchOff, "{}", ExportString[header, "JSON", "Compact" -> True]],
142 | "metadata" -> ExportString[
143 | {"text/html" -> {}},
144 | "JSON",
145 | "Compact" -> True
146 | ],
147 | "content" -> replyContent
148 | ];
149 |
150 | (* generate the signature of the reply message *)
151 | AssociateTo[
152 | result,
153 | "signature" ->
154 | hmac[
155 | keyString,
156 | StringJoin[
157 | result["header"],
158 | result["pheader"],
159 | result["metadata"],
160 | If[StringQ[result["content"]], result["content"], ByteArrayToString[result["content"]]]
161 | ]
162 | ]
163 | ];
164 |
165 | (* return the built reply message frame *)
166 | Return[result];
167 | ];
168 |
169 | (* end the private context for WolframLanguageForJupyter *)
170 | End[]; (* `Private` *)
171 |
172 | (************************************
173 | Get[] guard
174 | *************************************)
175 |
176 | ] (* WolframLanguageForJupyter`Private`$GotMessagingUtilities *)
177 |
--------------------------------------------------------------------------------
/WolframLanguageForJupyter/Resources/KernelForWolframLanguageForJupyter.wl:
--------------------------------------------------------------------------------
1 | (************************************************
2 | KernelForWolframLanguage-
3 | ForJupyter.wl
4 | *************************************************
5 | Description:
6 | Entry point for WolframLanguageForJupyter
7 | kernels started by Jupyter
8 | Symbols defined:
9 | loop
10 | *************************************************)
11 |
12 | (************************************
13 | begin the
14 | WolframLanguageForJupyter
15 | package
16 | *************************************)
17 |
18 | (* begin the WolframLanguageForJupyter package *)
19 | BeginPackage["WolframLanguageForJupyter`"];
20 |
21 | (************************************
22 | get required paclets
23 | *************************************)
24 |
25 | (* obtain ZMQ utilities *)
26 | Needs["ZeroMQLink`"]; (* SocketReadMessage *)
27 |
28 | (************************************
29 | load required
30 | WolframLanguageForJupyter
31 | files
32 | *************************************)
33 |
34 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* initialize WolframLanguageForJupyter; loopState, bannerWarning, shellSocket, controlSocket, ioPubSocket *)
35 |
36 | Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* sendFrame *)
37 | Get[FileNameJoin[{DirectoryName[$InputFileName], "MessagingUtilities.wl"}]]; (* getFrameAssoc, createReplyFrame *)
38 |
39 | Get[FileNameJoin[{DirectoryName[$InputFileName], "RequestHandlers.wl"}]]; (* isCompleteRequestHandler, executeRequestHandler, completeRequestHandler *)
40 |
41 | (************************************
42 | private symbols
43 | *************************************)
44 |
45 | (* begin the private context for WolframLanguageForJupyter *)
46 | Begin["`Private`"];
47 |
48 | (* define the evaluation loop *)
49 | loop[] :=
50 | Module[
51 | {
52 | (* the socket that has become ready *)
53 | readySocket,
54 |
55 | (* the raw byte array frame received through SocketReadMessage *)
56 | rawFrame,
57 |
58 | (* a frame for sending status updates on the IO Publish socket *)
59 | statusReplyFrame,
60 |
61 | (* a frame for sending replies on a socket *)
62 | replyFrame
63 | },
64 | While[
65 | True,
66 | Switch[
67 | (* poll sockets until one is ready *)
68 | readySocket = First[SocketWaitNext[{shellSocket, controlSocket}]],
69 | (* if the shell socket or control socket is ready, ... *)
70 | shellSocket | controlSocket,
71 | (* receive a frame *)
72 | rawFrame = SocketReadMessage[readySocket, "Multipart" -> True];
73 | (* check for any problems *)
74 | If[FailureQ[rawFrame],
75 | Quit[];
76 | ];
77 | (* convert the frame into an Association *)
78 | loopState["frameAssoc"] = getFrameAssoc[rawFrame];
79 | (* handle this frame based on the type of request *)
80 | Switch[
81 | loopState["frameAssoc"]["header"]["msg_type"],
82 | (* if asking for information about the kernel, ... *)
83 | "kernel_info_request",
84 | (* set the appropriate reply type *)
85 | loopState["replyMsgType"] = "kernel_info_reply";
86 | (* provide the information *)
87 | loopState["replyContent"] =
88 | StringJoin[
89 | "{\"protocol_version\": \"5.3.0\",\"implementation\": \"WolframLanguageForJupyter\",\"implementation_version\": \"0.0.1\",\"language_info\": {\"name\": \"Wolfram Language\",\"version\": \"12.0\",\"mimetype\": \"application/vnd.wolfram.m\",\"file_extension\": \".m\",\"pygments_lexer\": \"mathematica\",\"codemirror_mode\": \"mathematica\"},\"banner\" : \"Wolfram Language/Wolfram Engine Copyright 2019",
90 | bannerWarning,
91 | "\"}"
92 | ];,
93 | (* if asking if the input is complete (relevant for jupyter-console), respond appropriately *)
94 | "is_complete_request",
95 | (* isCompleteRequestHandler will read and update loopState *)
96 | isCompleteRequestHandler[];,
97 | (* if asking the kernel to execute something, use executeRequestHandler *)
98 | "execute_request",
99 | (* executeRequestHandler will read and update loopState *)
100 | executeRequestHandler[];,
101 | (* use the tab-completion functionality to rewrite named character names *)
102 | "complete_request",
103 | (* completeRequestHandler will read and update loopState *)
104 | completeRequestHandler[];,
105 | (* if asking the kernel to shutdown, set doShutdown to True *)
106 | "shutdown_request",
107 | loopState["replyMsgType"] = "shutdown_reply";
108 | loopState["replyContent"] = "{\"restart\":false}";
109 | loopState["doShutdown"] = True;,
110 | _,
111 | Continue[];
112 | ];
113 |
114 | (* create a message frame to send on the IO Publish socket to mark the kernel's status as "busy" *)
115 | statusReplyFrame =
116 | createReplyFrame[
117 | (* use the current source frame *)
118 | loopState["frameAssoc"],
119 | (* the status message type *)
120 | "status",
121 | (* the status message content *)
122 | "{\"execution_state\":\"busy\"}",
123 | (* do not branch off *)
124 | False
125 | ];
126 | (* send the frame *)
127 | sendFrame[ioPubSocket, statusReplyFrame];
128 |
129 | (* create a message frame to send a reply on the socket that became ready *)
130 | replyFrame =
131 | createReplyFrame[
132 | (* use the current source frame *)
133 | loopState["frameAssoc"],
134 | (* the reply message type *)
135 | loopState["replyMsgType"],
136 | (* the reply message content *)
137 | loopState["replyContent"],
138 | (* do not branch off *)
139 | False
140 | ];
141 | (* send the frame *)
142 | sendFrame[readySocket, replyFrame];
143 |
144 | (* if an ioPubReplyFrame was created, send it on the IO Publish socket *)
145 | If[
146 | loopState["ioPubReplyFrame"] =!= Association[],
147 | sendFrame[ioPubSocket, loopState["ioPubReplyFrame"]];
148 | (* -- also, reset ioPubReplyFrame *)
149 | loopState["ioPubReplyFrame"] = Association[];
150 | ];
151 |
152 | (* send a message frame on the IO Publish socket that marks the kernel's status as "idle" *)
153 | sendFrame[
154 | ioPubSocket,
155 | createReplyFrame[
156 | (* use the current source frame *)
157 | loopState["frameAssoc"],
158 | (* the status message type *)
159 | "status",
160 | (* the status message content *)
161 | "{\"execution_state\":\"idle\"}",
162 | (* do not branch off *)
163 | False
164 | ]
165 | ];
166 |
167 | (* if the doShutdown flag is True, shut down *)
168 | If[
169 | loopState["doShutdown"],
170 | Block[{$inQuit = True}, Quit[]];
171 | ];
172 | ,
173 | _,
174 | Continue[];
175 | ]
176 | ];
177 | ];
178 |
179 | (* end the private context for WolframLanguageForJupyter *)
180 | End[]; (* `Private` *)
181 |
182 | (************************************
183 | end the
184 | WolframLanguageForJupyter
185 | package
186 | *************************************)
187 |
188 | (* end the WolframLanguageForJupyter package *)
189 | EndPackage[]; (* WolframLanguageForJupyter` *)
190 | (* $ContextPath = DeleteCases[$ContextPath, "WolframLanguageForJupyter`"]; *)
191 |
192 | (************************************
193 | evaluate loop[]
194 | *************************************)
195 |
196 | (* start the loop *)
197 | WolframLanguageForJupyter`Private`loop[];
198 |
199 | (* This setup does not preclude dynamics or widgets. *)
200 |
--------------------------------------------------------------------------------
/WolframLanguageForJupyter/Resources/OutputHandlingUtilities.wl:
--------------------------------------------------------------------------------
1 | (************************************************
2 | OutputHandlingUtilities.wl
3 | *************************************************
4 | Description:
5 | Utilities for handling the result
6 | of Wolfram Language expressions
7 | so that, as outputs, they are
8 | reasonably displayed in Jupyter
9 | notebooks
10 | Symbols defined:
11 | textQ,
12 | toText,
13 | toOutTextHTML,
14 | toImageData,
15 | toOutImageHTML
16 | *************************************************)
17 |
18 | (************************************
19 | Get[] guard
20 | *************************************)
21 |
22 | If[
23 | !TrueQ[WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities],
24 |
25 | WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities = True;
26 |
27 | (************************************
28 | load required
29 | WolframLanguageForJupyter
30 | files
31 | *************************************)
32 |
33 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* $canUseFrontEnd, $outputSetToTeXForm,
34 | $outputSetToTraditionalForm,
35 | $trueFormatType, $truePageWidth,
36 | failedInBase64 *)
37 |
38 | (************************************
39 | private symbols
40 | *************************************)
41 |
42 | (* begin the private context for WolframLanguageForJupyter *)
43 | Begin["`Private`"];
44 |
45 | (************************************
46 | helper utility for converting
47 | an expression into a
48 | textual form
49 | *************************************)
50 |
51 | (* convert an expression into a textual form,
52 | using as much of the options already set for $Output as possible for ToString *)
53 | (* NOTE: toOutTextHTML used to call toStringUsingOutput *)
54 | toStringUsingOutput[expr_] :=
55 | ToString[
56 | expr,
57 | Sequence @@
58 | Cases[
59 | Options[$Output],
60 | Verbatim[Rule][opt_, val_] /;
61 | MemberQ[
62 | Keys[Options[ToString]],
63 | opt
64 | ]
65 | ]
66 | ];
67 |
68 | (************************************
69 | helper utility for determining
70 | if a result should be
71 | displayed as text or an image
72 | *************************************)
73 |
74 | (* check if a string contains any private use area characters *)
75 | containsPUAQ[str_] :=
76 | AnyTrue[
77 | ToCharacterCode[str, "Unicode"],
78 | (57344 <= #1 <= 63743 || 983040 <= #1 <= 1048575 || 1048576 <= #1 <= 1114111) &
79 | ];
80 |
81 | (************************************
82 | utility for determining if a
83 | result should be displayed
84 | as text or an image
85 | *************************************)
86 |
87 | (* determine if a result does not depend on any Wolfram Language frontend functionality,
88 | such that it should be displayed as text *)
89 | textQ[expr_] := Module[
90 | {
91 | (* the head of expr *)
92 | exprHead,
93 |
94 | (* pattern objects *)
95 | pObjects
96 | },
97 |
98 | (* if we cannot use the frontend, use text *)
99 | If[
100 | !$canUseFrontEnd,
101 | Return[True];
102 | ];
103 |
104 | (* save the head of the expression *)
105 | exprHead = Head[expr];
106 |
107 | (* if the expression is wrapped with InputForm or OutputForm,
108 | automatically format as text *)
109 | If[exprHead === InputForm || exprHead === OutputForm,
110 | Return[True]
111 | ];
112 |
113 | (* if the FormatType of $Output is set to TeXForm, or if the expression is wrapped with TeXForm,
114 | and the expression has an acceptable textual form, format as text *)
115 | If[($outputSetToTeXForm || exprHead == TeXForm) && !containsPUAQ[ToString[expr]],
116 | Return[True];
117 | ];
118 |
119 | (* if the FormatType of $Output is set to TraditionalForm,
120 | or if the expression is wrapped with TraditionalForm,
121 | do not use text *)
122 | If[$outputSetToTraditionalForm || exprHead === TraditionalForm,
123 | Return[False]
124 | ];
125 |
126 | (* breakdown expr into atomic objects organized by their Head *)
127 | pObjects =
128 | GroupBy[
129 | Complement[
130 | Quiet[Cases[
131 | expr,
132 | elem_ /; (Depth[Unevaluated[elem]] == 1) -> Hold[elem],
133 | {0, Infinity},
134 | Heads -> True
135 | ]],
136 | (* these symbols are fine *)
137 | {Hold[List], Hold[Association]}
138 | ],
139 | (
140 | Replace[
141 | #1,
142 | Hold[elem_] :> Head[Unevaluated[elem]]
143 | ]
144 | ) &
145 | ];
146 |
147 | (* if expr just contains atomic objects of the types listed above, return True *)
148 | If[
149 | ContainsOnly[Keys[pObjects], {Integer, Real}],
150 | Return[True];
151 | ];
152 |
153 | (* if expr just contains atomic objects of the types listed above, along with some symbols,
154 | return True only if the symbols have no attached rules *)
155 | If[
156 | ContainsOnly[Keys[pObjects], {Integer, Real, String, Symbol}],
157 | Return[
158 | AllTrue[
159 | Lookup[pObjects, String, {}],
160 | (!containsPUAQ[ReleaseHold[#1]]) &
161 | ] &&
162 | AllTrue[
163 | Lookup[pObjects, Symbol, {}],
164 | (
165 | Replace[
166 | #1,
167 | Hold[elem_] :> ToString[Definition[elem]]
168 | ] === "Null"
169 | ) &
170 | ]
171 | ];
172 | ];
173 |
174 | (* otherwise, no, the result should not be displayed as text *)
175 | Return[False];
176 | ];
177 |
178 | (************************************
179 | utilities for generating
180 | HTML for displaying
181 | results as text and images
182 | *************************************)
183 |
184 | (* generate the textual form of a result using a given page width *)
185 | (* NOTE: the OutputForm (which ToString uses) of any expressions wrapped with, say, InputForm should
186 | be identical to the string result of an InputForm-wrapped expression itself *)
187 | toText[result_, pageWidth_] :=
188 | ToString[
189 | (* make sure to apply $trueFormatType to the result if the result is not already headed by TeXForm *)
190 | If[
191 | Head[result] === TeXForm,
192 | result,
193 | $trueFormatType[result]
194 | ],
195 | (* also, use the given page width *)
196 | PageWidth -> pageWidth
197 | ];
198 | (* generate the textual form of a result using the current PageWidth setting for $Output *)
199 | toText[result_] := toText[result, $truePageWidth];
200 |
201 | (* generate HTML for the textual form of a result *)
202 | toOutTextHTML[result_] :=
203 | Module[
204 | {
205 | (* if the result should be marked as TeX *)
206 | isTeX
207 | },
208 | (* check if the result should be marked as TeX *)
209 | isTeX = ((Head[result] === TeXForm) || ($outputSetToTeXForm));
210 | Return[
211 | StringJoin[
212 |
213 | (* mark this result as preformatted only if it isn't TeX *)
214 | If[
215 | !isTeX,
216 | {
217 | (* preformatted *)
218 | ""
222 | },
223 | {}
224 | ],
225 |
226 | (* mark the text as TeX, if is TeX *)
227 | If[isTeX, "$$", ""],
228 |
229 | (* the textual form of the result *)
230 | ({"", ToString[#1], ";"} & /@
231 | ToCharacterCode[
232 | If[
233 | isTeX,
234 | (* if the result is TeX, do not allow line breaks *)
235 | toText[result, Infinity],
236 | (* otherwise, just call toText *)
237 | toText[result]
238 | ],
239 | "Unicode"
240 | ]),
241 |
242 | (* mark the text as TeX, if is TeX *)
243 | If[isTeX, "$$", ""],
244 |
245 | (* mark this result as preformatted only if it isn't TeX *)
246 | If[
247 | !isTeX,
248 | {
249 | (* end the element *)
250 | ""
251 | },
252 | {}
253 | ]
254 | ]
255 | ];
256 | ];
257 |
258 | (* generate a byte array of image data for the rasterized form of a result *)
259 | toImageData[result_] :=
260 | Module[
261 | {
262 | (* the preprocessed form of a result *)
263 | preprocessedForm
264 | },
265 | (* preprocess the result *)
266 | If[
267 | Head[result] === Manipulate,
268 | preprocessedForm = result;
269 | ,
270 | preprocessedForm = Rasterize[result];
271 | ];
272 | (* if the preprocessing failed, return $Failed *)
273 | If[
274 | FailureQ[preprocessedForm],
275 | Return[$Failed];
276 | ];
277 | (* now return preprocessedForm as a byte array corresponding to the PNG format *)
278 | Return[
279 | ExportByteArray[
280 | preprocessedForm,
281 | "PNG"
282 | ]
283 | ];
284 | ];
285 |
286 | (* generate HTML for the rasterized form of a result *)
287 | toOutImageHTML[result_] :=
288 | Module[
289 | {
290 | (* the rasterization of result *)
291 | imageData,
292 | (* the rasterization of result in base 64 *)
293 | imageDataInBase64
294 | },
295 |
296 | (* rasterize the result *)
297 | imageData =
298 | toImageData[
299 | $trueFormatType[result]
300 | ];
301 | If[
302 | !FailureQ[imageData],
303 | (* if the rasterization did not fail, convert it to base 64 *)
304 | imageInBase64 = BaseEncode[imageData];
305 | ,
306 | (* if the rasterization did fail, try to rasterize result with Shallow *)
307 | imageData =
308 | toImageData[
309 | $trueFormatType[Shallow[result]]
310 | ];
311 | If[
312 | !FailureQ[imageData],
313 | (* if the rasterization did not fail, convert it to base 64 *)
314 | imageInBase64 = BaseEncode[imageData];
315 | ,
316 | (* if the rasterization did fail, try to rasterize $Failed *)
317 | imageData =
318 | toImageData[
319 | $trueFormatType[$Failed]
320 | ];
321 | If[
322 | !FailureQ[imageData],
323 | (* if the rasterization did not fail, convert it to base 64 *)
324 | imageInBase64 = BaseEncode[imageData];
325 | ,
326 | (* if the rasterization did fail, use a hard-coded base64 rasterization of $Failed *)
327 | imageInBase64 = failedInBase64;
328 | ];
329 | ];
330 | ];
331 |
332 | (* return HTML for the rasterized form of result *)
333 | Return[
334 | StringJoin[
335 | (* display a inlined PNG image encoded in base64 *)
336 | "
"
341 | ]
342 | ]
343 | ];
344 |
345 | (* end the private context for WolframLanguageForJupyter *)
346 | End[]; (* `Private` *)
347 |
348 | (************************************
349 | Get[] guard
350 | *************************************)
351 |
352 | ] (* WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities *)
353 |
--------------------------------------------------------------------------------
/WolframLanguageForJupyter/Resources/Initialization.wl:
--------------------------------------------------------------------------------
1 | (************************************************
2 | Initialization.wl
3 | *************************************************
4 | Description:
5 | Initialization for
6 | WolframLanguageForJupyter
7 | Symbols defined:
8 | $defaultPageWidth,
9 | loopState,
10 | applyHook,
11 | $canUseFrontEnd,
12 | $outputSetToTraditionalForm,
13 | $outputSetToTeXForm,
14 | $trueFormatType,
15 | $truePageWidth,
16 | connectionAssoc,
17 | bannerWarning,
18 | keyString,
19 | baseString,
20 | heartbeatString,
21 | verticalEllipsis,
22 | unicodeNamedCharactersReplacements,
23 | ioPubString,
24 | controlString,
25 | inputString,
26 | shellString,
27 | ioPubSocket,
28 | controlSocket,
29 | inputSocket,
30 | shellSocket,
31 | heldLocalSubmit
32 | *************************************************)
33 |
34 | (************************************
35 | Get[] guard
36 | *************************************)
37 |
38 | If[
39 | !TrueQ[WolframLanguageForJupyter`Private`$GotInitialization],
40 |
41 | WolframLanguageForJupyter`Private`$GotInitialization = True;
42 |
43 | (************************************
44 | get required paclets
45 | *************************************)
46 |
47 | (* obtain ZMQ utilities *)
48 | Needs["ZeroMQLink`"]; (* SocketOpen *)
49 |
50 | (************************************
51 | private symbols
52 | *************************************)
53 |
54 | (* begin the private context for WolframLanguageForJupyter *)
55 | Begin["`Private`"];
56 |
57 | (************************************
58 | set noise settings
59 | *************************************)
60 |
61 | (* make Short[] work *)
62 | $defaultPageWidth = 89;
63 | SetOptions[$Output, PageWidth -> $defaultPageWidth];
64 |
65 | (* do not output messages to the jupyter notebook invocation
66 | $Messages = {};
67 | $Output = {}; *)
68 |
69 | (************************************
70 | discover the named unicode
71 | characters and their names
72 | *************************************)
73 |
74 | (* the vertical ellipsis character *)
75 | verticalEllipsis = FromCharacterCode[8942, "Unicode"];
76 |
77 | (* pre-define the association of names and named characters as empty *)
78 | unicodeNamedCharactersReplacements = Association[];
79 |
80 | Block[
81 | {
82 | (* the absolute file name for "UnicodeCharacters.tr" *)
83 | unicodeCharactersTRFileName,
84 |
85 | (* raw data extracted from "UnicodeCharacters.tr" *)
86 | charactersAndTheirNames
87 | },
88 |
89 | (* attempt to get the full location of "UnicodeCharacters.tr" *)
90 | unicodeCharactersTRFileName = UsingFrontEnd[System`Dump`unicodeCharactersTR];
91 |
92 | (* try again if using System`Dump`unicodeCharactersTR does not work *)
93 | If[
94 | !StringQ[unicodeCharactersTRFileName],
95 | unicodeCharactersTRFileName =
96 | UsingFrontEnd[
97 | ToFileName[
98 | FrontEnd`FileName[
99 | {
100 | $InstallationDirectory,
101 | "SystemFiles",
102 | "FrontEnd",
103 | "TextResources"
104 | },
105 | "UnicodeCharacters.tr"
106 | ]
107 | ]
108 | ];
109 | ];
110 |
111 | If[
112 | StringQ[unicodeCharactersTRFileName],
113 | charactersAndTheirNames =
114 | (
115 | (* parse the third item of a row (for a named character) into a list *)
116 | ReplacePart[
117 | #1,
118 | 3 ->
119 | StringCases[
120 | (* remove extraneous parentheses *)
121 | StringTrim[
122 | #1[[3]],
123 | "(" | ")"
124 | ],
125 | (* extract the escape sequences for the named character *)
126 | Longest[escSeq : (Except[WhitespaceCharacter] ..)] :>
127 | StringJoin[verticalEllipsis, StringTrim[escSeq, "$"], verticalEllipsis]
128 | ]
129 | ] &
130 | ) /@
131 | (* only use lists with at least three items *)
132 | Select[
133 | (* parse the rows into their items (where each item is separated by two tabs) *)
134 | (StringSplit[#1, "\t\t"] &) /@
135 | (
136 | (* split unicodeCharactersTRFileName into its lines *)
137 | StringSplit[
138 | Import[unicodeCharactersTRFileName, "String"],
139 | "\n"
140 | (* drop the first row *)
141 | ][[2 ;;]]
142 | ),
143 | (Length[#1] >= 3) &
144 | ];
145 |
146 | (* parse the data into an association of names and the named characters they correspond to *)
147 | unicodeNamedCharactersReplacements =
148 | (* sort the keys by string length *)
149 | KeySort[
150 | (* sort the keys in the default manner *)
151 | KeySort[
152 | (* drop "empty names" *)
153 | KeyDrop[
154 | (* make an association *)
155 | Association[
156 | (* create a list of rules of names and named characters *)
157 | Thread[
158 | Rule[
159 | (Prepend[#1[[3]], #1[[2]]]),
160 | FromCharacterCode[FromDigits[StringDrop[#1[[1]], 2], 16], "Unicode"]
161 | ]
162 | ] & /@ charactersAndTheirNames
163 | ],
164 | {
165 | StringJoin[Table[verticalEllipsis, {2}]],
166 | "\\[]"
167 | }
168 | ]
169 | ],
170 | (StringLength[#1] < StringLength[#2]) &
171 | ];
172 | ];
173 | ];
174 |
175 | (************************************
176 | various important symbols
177 | for use by
178 | WolframLanguageForJupyter
179 | *************************************)
180 |
181 | (* create an association for maintaining state in the evaluation loop *)
182 | loopState =
183 | (* schema for the Association *)
184 | Association[
185 | (* index for the execution of the next input *)
186 | "executionCount" -> 1,
187 |
188 | (* flag for if WolframLanguageForJupyter should shut down *)
189 | "doShutdown" -> False,
190 |
191 | (* flag for whether to redirect messages, or to, at the end of the execution of an input,
192 | bundle them with the result *)
193 | "redirectMessages" -> True,
194 |
195 | (* flag for if an is_complete_request has ever been sent to the kernel *)
196 | "isCompleteRequestSent" -> False,
197 |
198 | (* OutputStream for Jupyter's stdout *)
199 | "WolframLanguageForJupyter-stdout" -> False,
200 |
201 | (* local to an iteration *)
202 | (* a received frame as an Association *)
203 | "frameAssoc" -> Null,
204 | (* type of the reply message frame *)
205 | "replyMsgType" -> Null,
206 | (* content of the reply message frame *)
207 | "replyContent" -> Null,
208 | (* message relpy frame to send on the IO Publish socket, if it is not Null *)
209 | "ioPubReplyFrame" -> Null,
210 | (* the redirect function Print should use *)
211 | "printFunction" -> False,
212 | (* flag for if the Jupyter console (if running under a Jupyter console)
213 | should ask the user if it should exit *)
214 | "askExit" -> False
215 | ];
216 |
217 | (* helper utility for applying hooks if they are set *)
218 | applyHook[hook_, value_] /; Length[OwnValues[hook]] != 0 := hook[value];
219 | applyHook[hook_, value_] := value;
220 | Attributes[applyHook] := HoldAll;
221 |
222 | (* can we use the Front End? *)
223 | $canUseFrontEnd := (UsingFrontEnd[$FrontEnd] =!= Null);
224 |
225 | $outputSetToTraditionalForm := (Lookup[Options[$Output], FormatType] === TraditionalForm);
226 | $outputSetToTeXForm := (Lookup[Options[$Output], FormatType] === TeXForm);
227 | $trueFormatType :=
228 | If[
229 | $outputSetToTraditionalForm,
230 | TraditionalForm,
231 | If[$outputSetToTeXForm, TeXForm, #&]
232 | ];
233 | $truePageWidth :=
234 | Replace[
235 | Lookup[Options[$Output], PageWidth],
236 | Except[
237 | Alternatives[
238 | Infinity,
239 | pageWidth_ /; ((IntegerQ[pageWidth]) && (pageWidth > 0))
240 | ]
241 | ] ->
242 | $defaultPageWidth
243 | ];
244 |
245 | (* hard-coded base64 rasterization of $Failed *)
246 | failedInBase64 = "iVBORw0KGgoAAAANSUhEUgAAADcAAAARCAIAAAD2TKM6AAAAhXpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjaVYvBDcMwDAP/mqIjyLJM2uMYiQNkg45fuu0n9yApgbT1vi97felp2dgxABc5csRU6P6juNciDXn+X9MfZGWgoRhTluEkhnJXkqKFN+LCAahYcIbnIV8gNQN3o86928QyPusLVffpbh/5eCey76LuBgAAAAlwSFlzAAALEwAACxMBAJqcGAAAADx0RVh0U29mdHdhcmUAQ3JlYXRlZCB3aXRoIHRoZSBXb2xmcmFtIExhbmd1YWdlIDogd3d3LndvbGZyYW0uY29tXKKmhQAAACF0RVh0Q3JlYXRpb24gVGltZQAyMDE5OjA3OjAyIDAzOjExOjExSFD8JQAAA8xJREFUSInVlk9IKl8Ux68ajjThLynMwCKLICSDjBbRxpVSFhFkCtFfBMMKAhcu3GXhQopaJBEFISjhsn9YUGE6zSJqkRURFVpKLQqNcYgZHectpszfez9exoP33u+zmnPm3HO/c865w2XRNA3+eth/WkBOfKjEMAzH8T8o5Sd8qJybm1tcXPz1jCaTyeVyMc8URXV3d2s0Go1GEwgEvppqZGQERdEPldfX1yiKIghydnaWCerp6WFn0dbW9mlekiTdbrfX62VMNpvd398/MDAQCATu7u6+qnJ9fT0SiQAA8gAAJycnzc3NnZ2dfD5foVA4nc7W1lYAQDqd1uv1drudWZOXl/dpXi6XG4lEOBwOY7JYLLVaDQDIz8//qsRs2AAAt9vd2NjodrsXFhYuLy9VKlXmNQRB/7wDwzAAIBaLmc1miUTC5/MHBwfPz8+ZSJvNJpVKpVKpTCazWq2fbkySpMViEYvFAoFAp9M9Pz8z/v39/bq6Oj6fr9FoMueEDQBQKpUoihqNxtvb2+Li4kwlAADxePz6nXQ6DQC4uroKBoPz8/MoimIYZjabmciuri6Hw+FwOMRi8ePj46cqLRaLx+NZXV1dW1u7v78fGxsDAESj0fb29qamJp/PV19f//Ly8hZN0zRN016vVy6Xs9nsoaGh19dXxqnT6QAAzFByOJxYLEa/Q5Lk6enp9PQ0BEE4jtNZ9Pb2Go1G+t9UVla6XK6MmUwmIQja3t5mzI2NjYKCApqm7Xa7RCJJpVKMv6yszOPx0DT9dnpUKtXx8fHW1tbm5ubk5GTmi0dHRymKoigqlUoVFhYCAAiCMBqNQqFweHj48PCQIIh4PP5p5b4jHA4TBNHS0sLlcrlcbkdHB0EQOI7f3NzU1tZmN/Oj4wcHBxiGMVpVKlVm1P6TqakpBEEuLi4QBJmdnc1RFovFoigqY4pEIhaL5ff7SZIkSTKVSpEkCcOwQCAIBoM/LmcDACYmJqqrq/v6+rRarcfjYRr9kzJUVVWJRKJEImGz2XJU2dDQsLu7m06nHx4eAAAwDKvVapPJxExwNBpFEAQAoFAoQqHQ0tISSZIrKytM8JvKnZ0dxoVhmM/n02q1mQL8uJ/BYPD7/UKhsKKioqioCIKgXFTq9fq9vT0YhuVyOVPU5eXlkpKS8vLy0tJSqVTK/POVSuX4+LjBYODxeE6ns6am5m19ZqKtVuvMzAydAwRBhEIhiqJyCc5AUVQoFEomk9lOHMfD4fB3qRKJxNPTU7aHRb/fiY6Ojng8nkwmy7GJv5MPlX8z/4+b2zdkknhkRbjZsAAAAABJRU5ErkJggg==";
247 |
248 | (* obtain details on how to connect to Jupyter, from Jupyter's invocation of "KernelForWolframLanguageForJupyter.wl" *)
249 | (* NOTE: We remove the extension of the file to be imported so as to avoid accidentally loading the format "MXNet," which has a bad interaction with the paclet SetReplace (https://github.com/maxitg/SetReplace) as of June 22, 2020 *)
250 | Block[
251 | {noExtensionFile},
252 | noExtensionFile = CopyFile[$CommandLine[[4]], CreateFile[], OverwriteTarget -> True];
253 | connectionAssoc = ToString /@ Association[Import[noExtensionFile, "JSON"]];
254 | DeleteFile[noExtensionFile];
255 | ];
256 |
257 | (* warnings to display in kernel information *)
258 | bannerWarning =
259 | If[
260 | Length[$CommandLine] > 4,
261 | "\\n\\nNote: This Jupyter kernel was installed through the WolframScript install method. Accordingly, updates to a WolframLanguageForJupyter paclet will not affect this kernel.",
262 | ""
263 | ];
264 |
265 | (* key for generating signatures for reply message frames *)
266 | keyString = connectionAssoc["key"];
267 |
268 | (* base string using protocol and IP address from Jupyter *)
269 | baseString = StringJoin[connectionAssoc["transport"], "://", connectionAssoc["ip"], ":"];
270 |
271 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html for what the following correspond to *)
272 | heartbeatString = StringJoin[baseString, connectionAssoc["hb_port"]];
273 | ioPubString = StringJoin[baseString, connectionAssoc["iopub_port"]];
274 | controlString = StringJoin[baseString, connectionAssoc["control_port"]];
275 | inputString = StringJoin[baseString, connectionAssoc["stdin_port"]];
276 | shellString = StringJoin[baseString, connectionAssoc["shell_port"]];
277 |
278 | Block[
279 | {
280 | (* for storing the result of defining a new OutputStream method *)
281 | customOutputStreamMethod
282 | },
283 |
284 | (* define an OutputStream method that will allow writing to Jupyter's stdout *)
285 | customOutputStreamMethod =
286 | DefineOutputStreamMethod[
287 | "for-WolframLanguageForJupyter-stdout",
288 | {
289 | "ConstructorFunction" -> Function[{name, isAppend, caller, opts}, {True, {}}],
290 | "WriteFunction" ->
291 | Function[
292 | {state, bytes},
293 | If[
294 | loopState["frameAssoc"] =!= Null,
295 | redirectPrint[loopState["frameAssoc"], FromCharacterCode[bytes]];
296 | ];
297 | {Length[bytes], {}}
298 | ]
299 | }
300 | ];
301 |
302 | (* if defining a new OutputStream method did not fail,
303 | open an OutputStream using the new method, and store it in loopState *)
304 | If[
305 | !FailureQ[customOutputStreamMethod],
306 | loopState["WolframLanguageForJupyter-stdout"] =
307 | OpenWrite["WolframLanguageForJupyter-stdout", Method -> "for-WolframLanguageForJupyter-stdout"];
308 | (* -- also, if opening the OutputStream failed, reset loopState["WolframLanguageForJupyter-stdout"] back to False *)
309 | If[
310 | FailureQ[loopState["WolframLanguageForJupyter-stdout"]],
311 | loopState["WolframLanguageForJupyter-stdout"] = False;
312 | ];
313 | ];
314 | ];
315 |
316 | (************************************
317 | open all the non-heartbeat
318 | sockets
319 | *************************************)
320 |
321 | (* open sockets using the set strings from above *)
322 | ioPubSocket = SocketOpen[ioPubString, "ZMQ_PUB"];
323 | controlSocket = SocketOpen[controlString, "ZMQ_ROUTER"];
324 | inputSocket = SocketOpen[inputString, "ZMQ_ROUTER"];
325 | shellSocket = SocketOpen[shellString, "ZMQ_ROUTER"];
326 |
327 | (* check for any problems *)
328 | If[FailureQ[ioPubSocket] || FailureQ[controlSocket] || FailureQ[inputSocket] || FailureQ[shellSocket],
329 | Quit[];
330 | ];
331 |
332 | (************************************
333 | spin off a new kernel
334 | that nullifies Jupyter's
335 | requirement for looping
336 | back arrving "heartbeats"
337 | *************************************)
338 |
339 | (* start heartbeat thread *)
340 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#heartbeat-for-kernels *)
341 | heldLocalSubmit =
342 | Replace[
343 | Hold[
344 | (* submit a task for the new kernel *)
345 | LocalSubmit[
346 | (* get required ZMQ utilities in the new kernel *)
347 | Get["ZeroMQLink`"];
348 | (* open the heartbeat socket -- inserted with Replace and a placeholder *)
349 | heartbeatSocket = SocketOpen[placeholder1, "ZMQ_REP"];
350 | (* check for any problems *)
351 | If[
352 | FailureQ[heartbeatSocket],
353 | Quit[];
354 | ];
355 | (* do this "forever" *)
356 | While[
357 | True,
358 | (* wait for new data on the heartbeat socket *)
359 | SocketWaitNext[{heartbeatSocket}];
360 | (* receive the data *)
361 | heartbeatRecv = SocketReadMessage[heartbeatSocket];
362 | (* check for any problems *)
363 | If[
364 | FailureQ[heartbeatRecv],
365 | Continue[];
366 | ];
367 | (* and loop the data back to Jupyter *)
368 | socketWriteFunction[
369 | heartbeatSocket,
370 | heartbeatRecv,
371 | "Multipart" -> False
372 | ];
373 | ];,
374 | HandlerFunctions-> Association["TaskFinished" -> Quit]
375 | ]
376 | ],
377 | (* see above *)
378 | placeholder1 -> heartbeatString,
379 | Infinity
380 | ];
381 | (* start the heartbeat thread *)
382 | (* Quiet[ReleaseHold[heldLocalSubmit]]; *)
383 |
384 | (* end the private context for WolframLanguageForJupyter *)
385 | End[]; (* `Private`` *)
386 |
387 | (************************************
388 | Get[] guard
389 | *************************************)
390 |
391 | ] (* WolframLanguageForJupyter`Private`$GotInitialization *)
392 |
--------------------------------------------------------------------------------
/WolframLanguageForJupyter/WolframLanguageForJupyter.m:
--------------------------------------------------------------------------------
1 | BeginPackage["WolframLanguageForJupyter`"];
2 |
3 | ConfigureJupyter::subcommand = "The first argument to ConfigureJupyter is the subcommand: either \"add\", \"remove\", or \"clear\".";
4 | ConfigureJupyter::argx = "ConfigureJupyter called with `1` arguments; 1 argument is expected.";
5 |
6 | ConfigureJupyter::notfound = "Jupyter installation on Environment[\"PATH\"] not found.";
7 | ConfigureJupyter::isdir = "Provided `1` binary path is a directory. Please provide the path to the `1` binary.";
8 | ConfigureJupyter::nobin = "Provided `1` binary path does not exist.";
9 |
10 | ConfigureJupyter::notadded = "An error has occurred. The desired Wolfram Engine is not in \"jupyter kernelspec list.\" See WolframLanguageForJupyter`.`Errors`.`$ConfigureError for the message that Jupyter returned when attempting to add the Wolfram Engine.";
11 | ConfigureJupyter::notremoved = "An error has occurred: Wolfram Engine(s) still in \"jupyter kernelspec list.\" See WolframLanguageForJupyter`.`Errors`.`$ConfigureError for the message that Jupyter returned when attempting to remove the Wolfram Engine.";
12 |
13 | ConfigureJupyter::addconflict = "An error has occurred. A Wolfram Engine with the same $VersionNumber of the target Wolfram Engine is in \"jupyter kernelspec list.\" Attempting to overwrite ...";
14 | (* ConfigureJupyter::removeconflict = "An error has occurred. The Wolfram Engine(s) to be removed is/are not in \"jupyter kernelspec list.\""; *)
15 |
16 | ConfigureJupyter::nolink = "An error has occurred: Communication with provided Wolfram Engine binary could not be established.";
17 |
18 | ConfigureJupyter::usage =
19 | "ConfigureJupyter[subcommand:\"add\"|\"remove\"|\"clear\"] evaluates the action associated with subcommand, relying on the current Wolfram Engine binary path and the first Jupyter installation on Environment[\"PATH\"] when relevant.
20 | ConfigureJupyter[subcommand:\"add\"|\"remove\"|\"clear\", opts] evaluates the action associated with subcommand, using specified paths for \"WolframEngineBinary\" and \"JupyterInstallation\" when given as options.";
21 |
22 | Begin["`Private`"];
23 |
24 | (*
25 | Dictionary:
26 | mathBin/mathBinSession = WolframKernel binary
27 | kernelspec = Kernel Specification; term used by Jupyter
28 | notProvidedQ = was a Wolfram Engine Binary explicitly specified?
29 | *)
30 |
31 | (* START: Helper symbols *)
32 |
33 | projectHome = DirectoryName[$InputFileName];
34 |
35 | (* establishes link with Wolfram Engine at mathBin and evaluates $Version/$VersionNumber *)
36 | (* returns string form *)
37 | getVersionFromKernel[mathBin_String] :=
38 | Module[{link, res},
39 | link =
40 | LinkLaunch[
41 | StringJoin[
42 | {
43 | "\"",
44 | mathBin,
45 | "\" -wstp"
46 | }
47 | ]
48 | ];
49 | If[FailureQ[link],
50 | Return[$Failed];
51 | ];
52 | (* bleed link *)
53 | While[LinkReadyQ[link, 0.5], LinkRead[link];];
54 | LinkWrite[link, Unevaluated[$VersionNumber]];
55 | res = StringTrim[ToString[LinkRead[link]], "ReturnPacket[" | "]"];
56 | LinkClose[link];
57 | If[!StringContainsQ[res, "[" | "]"],
58 | Return[res];,
59 | Return[$Failed];
60 | ];
61 | ];
62 |
63 | (* determine display name for Jupyter installation from Wolfram Engine $Version/$VersionNumber *)
64 | (* returns {Kernel ID, Display Name} *)
65 | getNames[mathBin_String, notProvidedQ_?BooleanQ] :=
66 | Module[{version, installDir, (* names, hashedKernelUUID *) versionStr},
67 | (* if Wolfram Engine binary not provided, just evaluate $Version in the current session *)
68 | (* otherwise, use MathLink to obtain $Version *)
69 | If[
70 | notProvidedQ,
71 | version = ToString[$VersionNumber];
72 | installDir = $InstallationDirectory;
73 | ,
74 | version = Quiet[getVersionFromKernel[mathBin]];
75 | If[
76 | FailureQ[version],
77 | Return[$Failed];
78 | ];
79 | installDir = mathBin;
80 | ];
81 |
82 | versionStr = StringTrim[version, "."];
83 | Return[
84 | {
85 | (* Kernel ID *)
86 | StringJoin["wolframlanguage", versionStr],
87 | (* Display Name *)
88 | StringJoin["Wolfram Language ", versionStr]
89 | }
90 | ];
91 | ];
92 |
93 | (* determine symbols related to finding Wolfram Engine and Jupyter installations *)
94 | (* mathBinSession: WolframKernel location for the current session *)
95 | (* fileExt: file extension for executables *)
96 | (* pathSeperator: delimiter for directories on PATH *)
97 | defineGlobalVars[] :=
98 | Switch[
99 | $OperatingSystem,
100 | "Windows",
101 | mathBinSession = FileNameJoin[{$InstallationDirectory, "wolfram.exe"}];
102 | fileExt = ".exe";
103 | pathSeperator = ";";,
104 | "MacOSX",
105 | mathBinSession = FileNameJoin[{$InstallationDirectory, "MacOS", "WolframKernel"}];
106 | fileExt = "";
107 | pathSeperator = ":";,
108 | "Unix",
109 | mathBinSession = FileNameJoin[{$InstallationDirectory, "Executables", "WolframKernel"}];
110 | fileExt = "";
111 | pathSeperator = ":";
112 | ];
113 |
114 | mathBinSession := (defineGlobalVars[]; mathBinSession);
115 | fileExt := (defineGlobalVars[]; fileExt);
116 | pathSeperator := (defineGlobalVars[]; pathSeperator);
117 |
118 | (* a list of directories in PATH *)
119 | splitPath :=
120 | StringSplit[
121 | (* restore PATH, if due to a bug, it becomes essentially empty; this is relevant to finding the Jupyter installation *)
122 | (* otherwise, just use PATH directly *)
123 | If[
124 | $OperatingSystem === "MacOSX" && FileType["~/.profile"] === File,
125 | StringTrim[
126 | RunProcess[
127 | $SystemShell,
128 | "StandardOutput",
129 | StringJoin[Import["~/.profile", "String"], "\necho $PATH"],
130 | ProcessEnvironment -> {}
131 | ],
132 | "\n"
133 | ]
134 | ,
135 | Environment["PATH"]
136 | ],
137 | pathSeperator];
138 |
139 |
140 | (* find Jupyter installation path *)
141 | (* returns above *)
142 | findJupyterPath[] :=
143 | SelectFirst[
144 | splitPath,
145 | (* check every directory in PATH to see if a Jupyter binary is a member *)
146 | (FileType[FileNameJoin[{#1, StringJoin["jupyter", fileExt]}]] === File)&
147 | ];
148 |
149 | (* get information about installed kernels in Jupyter *)
150 | (* returns kernel IDs in Jupyter *)
151 | getKernels[jupyterPath_String, processEnvironment_] :=
152 | Module[{json, kernelspecAssoc},
153 | (* obtain information about "jupyter kernelspec list" in JSON *)
154 | json = Quiet[ImportString[RunProcess[{jupyterPath, "kernelspec", "list", "--json"}, "StandardOutput", ProcessEnvironment -> processEnvironment], "JSON"]];
155 | (* transform that JSON information into an Association *)
156 | kernelspecAssoc =
157 | If[
158 | FailureQ[json],
159 | Association[],
160 | Replace[
161 | json,
162 | part_List /; AllTrue[part, Head[#1] === Rule &] -> Association @ part,
163 | {0, Infinity}
164 | ]
165 | ];
166 | Return[
167 | (* if the above process worked, just return the kernel IDs of all the kernelspecs *)
168 | (* otherwise, return an empty list *)
169 | If[
170 | KeyExistsQ[kernelspecAssoc, "kernelspecs"],
171 | Keys[kernelspecAssoc["kernelspecs"]],
172 | {}
173 | ]
174 | ];
175 | ];
176 |
177 |
178 | (* END: Helper symbols *)
179 |
180 | (* main install command *)
181 | (* specs: options \"WolframEngineBinary\" and \"JupyterInstallation\" in an Association, when provided *)
182 | (* removeQ: remove a Jupyter installation or not *)
183 | (* removeAllQ: clear all Jupyter installations or not *)
184 | (* removeQ first, removeAllQ second: "add" is False, False; "remove" is True, False, and "clear" is True, True *)
185 | (* returns action success status *)
186 | configureJupyter[specs_Association, removeQ_?BooleanQ, removeAllQ_?BooleanQ] :=
187 | Module[
188 | {
189 | retrievedNames, kernelID, displayName,
190 | notProvidedQ,
191 | jupyterPath, mathBin,
192 | fileType,
193 | processEnvironment,
194 | baseDir, tempDir,
195 | wlKernels, (* wlKernelsL(owerCase) *) wlKernelsL,
196 | commandArgs,
197 | exitInfo, kernelspecAssoc, kernelspecs
198 | },
199 |
200 | (* just check that the REPL script is there *)
201 | If[
202 | !(
203 | FileType[
204 | FileNameJoin[{projectHome, "Resources", "KernelForWolframLanguageForJupyter.wl"}]
205 | ] === File
206 | ),
207 | Return[$Failed];
208 | ];
209 |
210 | jupyterPath = specs["JupyterInstallation"];
211 | (* if no Jupyter installation path provided, determine it from PATH *)
212 | If[
213 | MissingQ[jupyterPath],
214 | jupyterPath = findJupyterPath[];
215 | (* if Jupyter not on PATH, message *)
216 | If[MissingQ[jupyterPath],
217 | Message[ConfigureJupyter::notfound, "Jupyter"];
218 | Return[$Failed];
219 | ];
220 | jupyterPath = FileNameJoin[{jupyterPath, StringJoin["jupyter", fileExt]}];
221 | ];
222 |
223 | mathBin =
224 | Lookup[
225 | specs,
226 | "WolframEngineBinary",
227 | (* if no "WolframEngineBinary" provided, use the session Wolfram Kernel location and set notProvidedQ to True *)
228 | (notProvidedQ = True; mathBinSession)
229 | ];
230 |
231 | (* check that the Jupyter installation path is a file, and message appropriately *)
232 | If[
233 | !((fileType = FileType[jupyterPath]) === File),
234 | Switch[
235 | fileType,
236 | Directory,
237 | Message[ConfigureJupyter::isdir, "Jupyter"];,
238 | None,
239 | Message[ConfigureJupyter::nobin, "Jupyter"];
240 | ];
241 | Return[$Failed];
242 | ];
243 |
244 | {kernelID, displayName} = {"", ""};
245 | (* if not clearing, check that the Wolfram Engine installation path is a file, and message appropriately *)
246 | If[
247 | !(removeQ && removeAllQ),
248 | If[
249 | (fileType = FileType[mathBin]) === File,
250 | (* get the "Kernel ID" and "Display Name" for the new Jupyter kernel *)
251 | retrievedNames = getNames[mathBin, TrueQ[notProvidedQ]];
252 | If[FailureQ[retrievedNames], Message[ConfigureJupyter::nolink]; Return[$Failed]];
253 | {kernelID, displayName} = retrievedNames;
254 | ,
255 | Switch[
256 | fileType,
257 | Directory,
258 | Message[ConfigureJupyter::isdir, "Wolfram Engine"];,
259 | None,
260 | Message[ConfigureJupyter::nobin, "Wolfram Engine"];
261 | ];
262 | Return[$Failed];
263 | ];
264 | ];
265 |
266 | (* as an association for 11.3 compatibility *)
267 | processEnvironment = Association[GetEnvironment[]];
268 | processEnvironment["PATH"] = StringJoin[Riffle[Append[splitPath, DirectoryName[jupyterPath]], pathSeperator]];
269 |
270 | (* list of kernels in Jupyter to perform an action on *)
271 | wlKernels = {kernelID};
272 | tempDir = "";
273 | (* if adding, ...*)
274 | (* otherwise, when removing or clearing, ...*)
275 | If[
276 | !removeQ,
277 |
278 | (* create staging directory for files needed to register a kernel with Jupyter *)
279 | tempDir = CreateDirectory[
280 | FileNameJoin[{
281 | projectHome,
282 | CreateUUID[],
283 | kernelID
284 | }], CreateIntermediateDirectories -> True
285 | ];
286 |
287 | (* export a JSON file to the staging directory that contains all the relevant information on how to run the kernel *)
288 | Export[
289 | FileNameJoin[{tempDir, "kernel.json"}],
290 | Association[
291 | "argv" -> {
292 | mathBin,
293 | (* TODO: automatically find the kernel script
294 | (only) if the Wolfram Engine being installed is the same as the one used to execute this command *)
295 | "-script",
296 | FileNameJoin[{projectHome, "Resources", "KernelForWolframLanguageForJupyter.wl"}],
297 | "{connection_file}"
298 | (* , "-noprompt" *)
299 | },
300 | "display_name" -> displayName,
301 | "language" -> "Wolfram Language"
302 | ]
303 | ];
304 |
305 | (* create a list of arguments that directs Jupyter to install from the staging directory *)
306 | commandArgs = {jupyterPath, "kernelspec", "install", "--user", tempDir};,
307 | (* create a list of arguments that directs Jupyter to remove ... *)
308 | commandArgs = {jupyterPath, "kernelspec", "remove", "-f",
309 | If[
310 | !removeAllQ,
311 | (* just the specified kernel *)
312 | kernelID,
313 | (* all Wolfram Language Jupyter kernels *)
314 | (* select from all kernel IDs in Jupyter those that match the form used by this install *)
315 | Sequence @@ (wlKernels = Select[getKernels[jupyterPath, processEnvironment], StringMatchQ[#1, (* ("WolframLanguage-" | "wl-") *) "WolframLanguage" ~~ ___, IgnoreCase -> True] &])
316 | ]
317 | }
318 | ];
319 | (* if no kernels to act on, quit *)
320 | If[Length[wlKernels] == 0, Return[];];
321 | wlKernelsL = ToLowerCase /@ wlKernels;
322 |
323 | (* for error detection, get a snapshot of kernels before the action is performed *)
324 | kernelspecs = getKernels[jupyterPath, processEnvironment];
325 | (* when adding, if there is a kernel with the same id already in Jupyter, it will be replaced; thus, message, but continue *)
326 | If[SubsetQ[kernelspecs, wlKernelsL] && !removeQ, Message[ConfigureJupyter::addconflict]];
327 |
328 | (* perform the action *)
329 | exitInfo = RunProcess[commandArgs, All, ProcessEnvironment -> processEnvironment];
330 | (* remove temporary directory if it was created *)
331 | If[StringLength[tempDir] > 0, DeleteDirectory[DirectoryName[tempDir], DeleteContents -> True]];
332 |
333 | (* get list of kernels after the action was performed *)
334 | kernelspecs = getKernels[jupyterPath, processEnvironment];
335 | (* message about success with respect to the action that was performed *)
336 | If[
337 | !Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]],
338 | WolframLanguageForJupyter`Errors`$ConfigureError = exitInfo["StandardError"];
339 | Print[WolframLanguageForJupyter`Errors`$ConfigureError];
340 | If[!removeQ, Message[ConfigureJupyter::notadded];, Message[ConfigureJupyter::notremoved];];
341 | Return[$Failed];
342 | ];
343 | ];
344 |
345 | (* convert options to an Association *)
346 | ConfigureJupyter[
347 | args___,
348 | opts:OptionsPattern[] /; Length[{opts}] > 0
349 | ] := ConfigureJupyter[args, Association[opts]];
350 |
351 | (* mold ConfigureJupyter arguments to what is expected by the main install function, configureJupyter ... *)
352 | ConfigureJupyter["Add", args___] := ConfigureJupyter["add", args];
353 | ConfigureJupyter["add"] := ConfigureJupyter["add", Association[]];
354 | ConfigureJupyter["add", assoc_Association] := configureJupyter[assoc, False, False];
355 |
356 | ConfigureJupyter["Remove", args___] := ConfigureJupyter["remove", args];
357 | ConfigureJupyter["remove"] := ConfigureJupyter["remove", Association[]];
358 | ConfigureJupyter["remove", assoc_Association] := configureJupyter[assoc, True, False];
359 |
360 | ConfigureJupyter["Clear", args___] := ConfigureJupyter["clear", args];
361 | ConfigureJupyter["clear"] := ConfigureJupyter["clear", Association[]];
362 | ConfigureJupyter["clear", assoc_Association] := configureJupyter[assoc, True, True];
363 |
364 | ConfigureJupyter[sc_String, ___] /; !StringMatchQ[sc, "add" | "remove" | "clear" | "Add" | "Remove" | "Clear"] := Message[ConfigureJupyter::subcommand];
365 | ConfigureJupyter[Except[_String], ___] := Message[ConfigureJupyter::subcommand];
366 | ConfigureJupyter[args___] := Message[ConfigureJupyter::argx, Length[{args}]];
367 |
368 | End[];
369 | EndPackage[];
370 |
--------------------------------------------------------------------------------
/WolframLanguageForJupyter/Resources/RequestHandlers.wl:
--------------------------------------------------------------------------------
1 | (************************************************
2 | RequestHandlers.wl
3 | *************************************************
4 | Description:
5 | Handlers for message frames of the form
6 | "*_request" arriving from Jupyter
7 | Symbols defined:
8 | isCompleteRequestHandler,
9 | executeRequestHandler,
10 | completeRequestHandler
11 | *************************************************)
12 |
13 | (************************************
14 | Get[] guard
15 | *************************************)
16 |
17 | If[
18 | !TrueQ[WolframLanguageForJupyter`Private`$GotRequestHandlers],
19 |
20 | WolframLanguageForJupyter`Private`$GotRequestHandlers = True;
21 |
22 | (************************************
23 | load required
24 | WolframLanguageForJupyter
25 | files
26 | *************************************)
27 |
28 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* loopState *)
29 |
30 | Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* sendFrame *)
31 | Get[FileNameJoin[{DirectoryName[$InputFileName], "MessagingUtilities.wl"}]]; (* createReplyFrame *)
32 |
33 | Get[FileNameJoin[{DirectoryName[$InputFileName], "EvaluationUtilities.wl"}]]; (* redirectPrint, redirectMessages, simulatedEvaluate *)
34 |
35 | Get[FileNameJoin[{DirectoryName[$InputFileName], "OutputHandlingUtilities.wl"}]]; (* textQ, toOutTextHTML, toOutImageHTML,
36 | toText, containsPUAQ *)
37 |
38 | Get[FileNameJoin[{DirectoryName[$InputFileName], "CompletionUtilities.wl"}]]; (* rewriteNamedCharacters *)
39 |
40 | (************************************
41 | private symbols
42 | *************************************)
43 |
44 | (* begin the private context for WolframLanguageForJupyter *)
45 | Begin["`Private`"];
46 |
47 | (************************************
48 | handler for is_complete_requests
49 | *************************************)
50 |
51 | (* handle is_complete_request message frames received on the shell socket *)
52 | isCompleteRequestHandler[] :=
53 | Module[
54 | {
55 | (* the length of the code string to check completeness for *)
56 | stringLength,
57 | (* the value returned by SyntaxLength[] on the code string to check completeness for *)
58 | syntaxLength
59 | },
60 |
61 | (* mark loopState["isCompleteRequestSent"] as True *)
62 | loopState["isCompleteRequestSent"] = True;
63 |
64 | (* set the appropriate reply type *)
65 | loopState["replyMsgType"] = "is_complete_reply";
66 |
67 | (* determine the length of the code string *)
68 | stringLength = StringLength[loopState["frameAssoc"]["content"]["code"]];
69 | (* determine the SyntaxLength[] value for the code string *)
70 | syntaxLength = SyntaxLength[loopState["frameAssoc"]["content"]["code"]];
71 |
72 | (* test the value of syntaxLength to determine the completeness of the code string,
73 | setting the content of the reply appropriately *)
74 | Which[
75 | (* if the above values could not be correctly determined,
76 | the completeness status of the code string is unknown *)
77 | !IntegerQ[stringLength] || !IntegerQ[syntaxLength],
78 | loopState["replyContent"] = "{\"status\":\"unknown\"}";,
79 | (* if the SyntaxLength[] value for a code string is greater than its actual length,
80 | the code string is incomplete *)
81 | syntaxLength > stringLength,
82 | loopState["replyContent"] = "{\"status\":\"incomplete\"}";,
83 | (* if the SyntaxLength[] value for a code string is less than its actual length,
84 | the code string contains a syntax error (or is "invalid") *)
85 | syntaxLength < stringLength,
86 | loopState["replyContent"] = "{\"status\":\"invalid\"}";,
87 | (* if the SyntaxLength[] value for a code string is equal to its actual length,
88 | the code string is complete and correct *)
89 | syntaxLength == stringLength,
90 | loopState["replyContent"] = "{\"status\":\"complete\"}";
91 | ];
92 | ];
93 |
94 | (************************************
95 | handler for execute_requests
96 | *************************************)
97 |
98 | (* handle execute_request message frames received on the shell socket *)
99 | executeRequestHandler[] :=
100 | Module[
101 | {
102 | (* message formatter function *)
103 | messageFormatter,
104 |
105 | (* content of the desired frame to send on the IO Publish socket *)
106 | ioPubReplyContent,
107 |
108 | (* the HTML form for any generated message *)
109 | errorMessage,
110 |
111 | (* the total result of the evaluation:
112 | an association containing
113 | the result of evaluation ("EvaluationResult"),
114 | indices of the output lines of the result ("EvaluationResultOutputLineIndices"),
115 | the total number of indices consumed by this evaluation ("ConsumedIndices"),
116 | generated messages ("GeneratedMessages")
117 | *)
118 | totalResult,
119 |
120 | (* flag for if there are any unreported error messages after execution of the input *)
121 | unreportedErrorMessages
122 | },
123 |
124 | (* if an is_complete_request has been sent, assume jupyter-console is running the kernel,
125 | redirect messages, and handle any "Quit", "Exit", "quit" or "exit" inputs *)
126 | If[
127 | loopState["isCompleteRequestSent"],
128 | loopState["redirectMessages"] = True;
129 | If[
130 | StringMatchQ[
131 | loopState["frameAssoc"]["content"]["code"],
132 | "Quit" | "Exit" | "quit" | "exit"
133 | ],
134 | loopState["replyMsgType"] = "execute_reply";
135 | (* NOTE: uses payloads *)
136 | loopState["replyContent"] = ExportString[Association["status" -> "ok", "execution_count" -> loopState["executionCount"], "user_expressions" -> {}, "payload" -> {Association["source" -> "ask_exit", "keepkernel" -> False]}], "JSON", "Compact" -> True];
137 | Return[];
138 | ];
139 | ];
140 |
141 | (* redirect Print so that it prints in the Jupyter notebook *)
142 | loopState["printFunction"] = (redirectPrint[loopState["frameAssoc"], #1] &);
143 |
144 | (* if loopState["redirectMessages"] is True,
145 | update Jupyter explicitly with any errors that occur DURING the execution of the input *)
146 | If[
147 | loopState["redirectMessages"],
148 | messageFormatter[messageName_, messageText_] :=
149 | redirectMessages[
150 | loopState["frameAssoc"],
151 | messageName,
152 | messageText,
153 | (* add a newline if loopState["isCompleteRequestSent"] *)
154 | loopState["isCompleteRequestSent"]
155 | ];
156 | SetAttributes[messageFormatter, HoldAll];
157 | Internal`$MessageFormatter = messageFormatter;
158 | ];
159 |
160 | (* evaluate the input, and store the total result in totalResult *)
161 | totalResult = simulatedEvaluate[loopState["frameAssoc"]["content"]["code"]];
162 |
163 | (* restore printFunction to False *)
164 | loopState["printFunction"] = False;
165 |
166 | (* unset messageFormatter and Internal`$MessageFormatter *)
167 | Unset[messageFormatter];
168 | Unset[Internal`$MessageFormatter];
169 |
170 | (* set the appropriate reply type *)
171 | loopState["replyMsgType"] = "execute_reply";
172 |
173 | (* set the content of the reply to information about WolframLanguageForJupyter's execution of the input *)
174 | loopState["replyContent"] =
175 | ExportString[
176 | Association[
177 | "status" -> "ok",
178 | "execution_count" -> loopState["executionCount"],
179 | "user_expressions" -> {},
180 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#payloads-deprecated *)
181 | (* if the "askExit" flag is True, add an "ask_exit" payload *)
182 | (* NOTE: uses payloads *)
183 | "payload" -> If[loopState["askExit"], {Association["source" -> "ask_exit", "keepkernel" -> False]}, {}]
184 | ],
185 | "JSON",
186 | "Compact" -> True
187 | ];
188 |
189 | (* check if there are any unreported error messages *)
190 | unreportedErrorMessages =
191 | (
192 | (* ... because messages are not being redirected *)
193 | (!loopState["redirectMessages"]) &&
194 | (* ... and because at least one message was generated *)
195 | (StringLength[totalResult["GeneratedMessages"]] > 0)
196 | );
197 |
198 | (* if there are no results, or if the "askExit" flag is True,
199 | do not send anything on the IO Publish socket and return *)
200 | If[
201 | (Length[totalResult["EvaluationResultOutputLineIndices"]] == 0) ||
202 | (loopState["askExit"]),
203 | (* set the "askExit" flag to False *)
204 | loopState["askExit"] = False;
205 | (* send any unreported error messages *)
206 | If[unreportedErrorMessages,
207 | redirectMessages[
208 | loopState["frameAssoc"],
209 | "",
210 | totalResult["GeneratedMessages"],
211 | (* do not add a newline *)
212 | False,
213 | (* drop message name *)
214 | True
215 | ];
216 | ];
217 | (* increment loopState["executionCount"] as needed *)
218 | loopState["executionCount"] += totalResult["ConsumedIndices"];
219 | Return[];
220 | ];
221 |
222 | (* generate an HTML form of the message text *)
223 | errorMessage =
224 | If[
225 | !unreportedErrorMessages,
226 | (* if there are no unreported error messages, there is no need to format them *)
227 | {},
228 | (* build the HTML form of the message text *)
229 | {
230 | (* preformatted *)
231 | "",
236 | (* the generated messages *)
237 | StringJoin[{"", ToString[#1], ";"} & /@ ToCharacterCode[totalResult["GeneratedMessages"], "UTF-8"]],
238 | (* end the element *)
239 | ""
240 | }
241 | ];
242 |
243 | (* format output as purely text, image, or cloud interface *)
244 | If[
245 | (* check if the input was wrapped with Interact,
246 | which is used when the output should be displayed as an embedded cloud object *)
247 | TrueQ[totalResult["InteractStatus"]] &&
248 | (* check if we are logged into the Cloud *)
249 | $CloudConnected,
250 | (* prepare the content for a reply message frame to be sent on the IO Publish socket *)
251 | ioPubReplyContent =
252 | ExportString[
253 | Association[
254 | (* the first output index *)
255 | "execution_count" -> First[totalResult["EvaluationResultOutputLineIndices"]],
256 | (* HTML code to embed output uploaded to the Cloud in the Jupyter notebook *)
257 | "data" ->
258 | {
259 | "text/html" ->
260 | StringJoin[
261 | (* display any generated messages as inlined PNG images encoded in base64 *)
262 | "
",
267 | (* embed the cloud object *)
268 | EmbedCode[CloudDeploy[totalResult["EvaluationResult"]], "HTML"][[1]]["CodeSection"]["Content"],
269 | (* end the whole element *)
270 | "
"
271 | ],
272 | "text/plain" -> ""
273 | },
274 | (* no metadata *)
275 | "metadata" -> {"text/html" -> {}, "text/plain" -> {}}
276 | ],
277 | "JSON",
278 | "Compact" -> True
279 | ];
280 | ,
281 | (* if every output line can be formatted as text, use a function that converts the output to text *)
282 | (* otherwise, use a function that converts the output to an image *)
283 | (* TODO: allow for mixing text and image results *)
284 | If[AllTrue[totalResult["EvaluationResult"], textQ],
285 | toOut = toOutTextHTML,
286 | toOut = toOutImageHTML
287 | ];
288 | (* prepare the content for a reply message frame to be sent on the IO Publish socket *)
289 | ioPubReplyContent = ExportByteArray[
290 | Association[
291 | (* the first output index *)
292 | "execution_count" -> First[totalResult["EvaluationResultOutputLineIndices"]],
293 | (* the data representing the results and messages *)
294 | "data" ->
295 | {
296 | (* generate HTML for the results and messages *)
297 | "text/html" ->
298 | If[
299 | loopState["isCompleteRequestSent"],
300 | (* if an is_complete_request has been sent, assume jupyter-console is running the kernel,
301 | and do not generate HTML *)
302 | "",
303 | (* otherwise, output the results in a grid *)
304 | If[
305 | Length[totalResult["EvaluationResult"]] > 1,
306 | StringJoin[
307 | (* add grid style *)
308 | "
314 |
315 | ",
316 | (* display error message *)
317 | errorMessage,
318 | (* start the grid *)
319 | "
",
320 | (* display the output lines *)
321 | Table[
322 | {
323 | (* start the grid item *)
324 | "
",
325 | (* show the output line *)
326 | toOut[totalResult["EvaluationResult"][[outIndex]]],
327 | (* end the grid item *)
328 | "
"
329 | },
330 | {outIndex, 1, Length[totalResult["EvaluationResult"]]}
331 | ],
332 | (* end the element *)
333 | "
"
334 | ],
335 | StringJoin[
336 | (* start the element *)
337 | "",
338 | (* display error message *)
339 | errorMessage,
340 | (* if there are messages, but no results, do not display a result *)
341 | If[
342 | Length[totalResult["EvaluationResult"]] == 0,
343 | "",
344 | (* otherwise, display a result *)
345 | toOut[First[totalResult["EvaluationResult"]]]
346 | ],
347 | (* end the element *)
348 | "
"
349 | ]
350 | ]
351 | ],
352 | (* provide, as a backup, plain text for the results *)
353 | "text/plain" ->
354 | StringJoin[
355 | Table[
356 | {
357 | toText[totalResult["EvaluationResult"][[outIndex]]],
358 | (* -- also, suppress newline if this is the last result *)
359 | If[outIndex != Length[totalResult["EvaluationResult"]], "\n", ""]
360 | },
361 | {outIndex, 1, Length[totalResult["EvaluationResult"]]}
362 | ]
363 | ]
364 | },
365 | (* no metadata *)
366 | "metadata" -> {"text/html" -> {}, "text/plain" -> {}}
367 | ],
368 | "JSON",
369 | "Compact" -> True
370 | ];
371 | ];
372 |
373 | (* create frame from ioPubReplyContent *)
374 | loopState["ioPubReplyFrame"] =
375 | createReplyFrame[
376 | (* use the current source frame *)
377 | loopState["frameAssoc"],
378 | (* the reply message type *)
379 | "execute_result",
380 | (* the reply message content *)
381 | ioPubReplyContent,
382 | (* do not branch off *)
383 | False
384 | ];
385 |
386 | (* increment loopState["executionCount"] as needed *)
387 | loopState["executionCount"] += totalResult["ConsumedIndices"];
388 | ];
389 |
390 | (************************************
391 | handler for complete_requests
392 | *************************************)
393 |
394 | (* handle complete_request message frames received on the shell socket *)
395 | completeRequestHandler[] :=
396 | Module[
397 | {
398 | (* for storing the code string to offer completion suggestions on *)
399 | codeStr
400 | },
401 | (* get the code string to rewrite the named characters of, ending at the cursor *)
402 | codeStr =
403 | StringTake[
404 | loopState["frameAssoc"]["content"]["code"],
405 | {
406 | 1,
407 | loopState["frameAssoc"]["content"]["cursor_pos"]
408 | }
409 | ];
410 | (* set the appropriate reply type *)
411 | loopState["replyMsgType"] = "complete_reply";
412 | (* set the content of the reply to a list of rewrites for any named characters in the code string *)
413 | loopState["replyContent"] =
414 | ByteArrayToString[
415 | ExportByteArray[
416 | Association[
417 | "matches" ->
418 | DeleteDuplicates[
419 | Prepend[
420 | Select[
421 | rewriteNamedCharacters[codeStr],
422 | (!containsPUAQ[#1])&
423 | ],
424 | codeStr
425 | ]
426 | ],
427 | "cursor_start" -> 0,
428 | "cursor_end" -> StringLength[codeStr],
429 | "metadata" -> {},
430 | "status" -> "ok"
431 | ],
432 | "JSON",
433 | "Compact" -> True
434 | ]
435 | ];
436 | ];
437 |
438 | (* end the private context for WolframLanguageForJupyter *)
439 | End[]; (* `Private` *)
440 |
441 | (************************************
442 | Get[] guard
443 | *************************************)
444 |
445 | ] (* WolframLanguageForJupyter`Private`$GotRequestHandlers *)
446 |
--------------------------------------------------------------------------------
/configure-jupyter.wls:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env wolframscript
2 |
3 | Begin["WolframLanguageForJupyter`Private`"];
4 |
5 | notfound = "configure-jupyter.wls: Jupyter installation on Environment[\"PATH\"] not found.";
6 | isdir = "configure-jupyter.wls: Provided Jupyter binary path is a directory. Please provide the path to the Jupyter binary."
7 | nobin = "configure-jupyter.wls: Provided Jupyter binary path does not exist.";
8 | isdirMath = "configure-jupyter.wls: Provided Wolfram Engine binary path is a directory. Please provide the path to the Wolfram Engine binary."
9 | nobinMath = "configure-jupyter.wls: Provided Wolfram Engine binary path does not exist.";
10 | notadded = "configure-jupyter.wls: An error has occurred. The desired Wolfram Engine is not in \"jupyter kernelspec list.\"";
11 | notremoved = "configure-jupyter.wls: An error has occurred: Wolfram Engine(s) still in \"jupyter kernelspec list.\"";
12 | addconflict = "configure-jupyter.wls: An error has occurred. A Wolfram Engine with the same $VersionNumber of the target Wolfram Engine is in \"jupyter kernelspec list.\" Attempting to overwrite ...";
13 | (* removeconflict = "configure-jupyter.wls: An error has occurred. The Wolfram Engine(s) to be removed is/are not in \"jupyter kernelspec list.\""; *)
14 | removeconflict = "";
15 | nopaclet = "configure-jupyter.wls: WolframLanguageForJupyter paclet source not detected. Are you running the script in the root project directory?";
16 | nolink = "configure-jupyter.wls: Communication with provided Wolfram Engine binary could not be established.";
17 |
18 | (*
19 | Dictionary:
20 | mathBin/mathBinSession = WolframKernel binary
21 | kernelspec = Kernel Specification; term used by Jupyter
22 | notProvidedQ = was a Wolfram Engine Binary explicitly specified?
23 | *)
24 |
25 | (* START: Helper symbols *)
26 |
27 | projectHome = If[StringQ[$InputFileName] && $InputFileName != "", DirectoryName[$InputFileName], Directory[]];
28 |
29 | (* establishes link with Wolfram Engine at mathBin and evaluates $Version/$VersionNumber *)
30 | (* returns string form *)
31 | getVersionFromKernel[mathBin_String] :=
32 | Module[{link, res},
33 | link =
34 | LinkLaunch[
35 | StringJoin[
36 | {
37 | "\"",
38 | mathBin,
39 | "\" -wstp"
40 | }
41 | ]
42 | ];
43 | If[FailureQ[link],
44 | Return[$Failed];
45 | ];
46 | (* bleed link *)
47 | While[LinkReadyQ[link, 0.5], LinkRead[link];];
48 | LinkWrite[link, Unevaluated[$VersionNumber]];
49 | res = StringTrim[ToString[LinkRead[link]], "ReturnPacket[" | "]"];
50 | LinkClose[link];
51 | If[!StringContainsQ[res, "[" | "]"],
52 | Return[res];,
53 | Return[$Failed];
54 | ];
55 | ];
56 |
57 | (* determine display name for Jupyter installation from Wolfram Engine $Version/$VersionNumber *)
58 | (* returns {Kernel ID, Display Name} *)
59 | getNames[mathBin_String, notProvidedQ_?BooleanQ] :=
60 | Module[{version, installDir, (* names, hashedKernelUUID *) versionStr},
61 | (* if Wolfram Engine binary not provided, just evaluate $Version in the current session *)
62 | (* otherwise, use MathLink to obtain $Version *)
63 | If[
64 | notProvidedQ,
65 | version = ToString[$VersionNumber];
66 | installDir = $InstallationDirectory;
67 | ,
68 | version = Quiet[getVersionFromKernel[mathBin]];
69 | If[
70 | FailureQ[version],
71 | Return[$Failed];
72 | ];
73 | installDir = mathBin;
74 | ];
75 |
76 | (*
77 |
78 | hashedKernelUUID = StringJoin["wl-script-", Hash[installDir, "SHA", "HexString"]];
79 |
80 | names = StringCases[version, name___ ~~ " for " ~~ ("Mac" | "Microsoft" | "Windows" | "Linux") -> name];
81 | Return[
82 | If[Length[names] > 0,
83 | {
84 | ToLowerCase[StringJoin[
85 | "WolframLanguage-script-",
86 | StringReplace[First[names], Whitespace -> "-"]
87 | ]],
88 | StringJoin[
89 | "Wolfram Language (",
90 | Capitalize[
91 | First[names],
92 | "AllWords"
93 | ],
94 | ") | Script Install"
95 | ]
96 | }
97 | ,
98 | {hashedKernelUUID, "Wolfram Language | Script Install"}
99 | ]
100 | ];
101 |
102 | *)
103 |
104 | versionStr = StringTrim[version, "."];
105 | Return[
106 | {
107 | (* Kernel ID *)
108 | StringJoin["wolframlanguage", versionStr],
109 | (* Display Name *)
110 | StringJoin["Wolfram Language ", versionStr]
111 | }
112 | ];
113 | ];
114 |
115 | (* determine symbols related to finding Wolfram Engine and Jupyter installations *)
116 | (* mathBinSession: WolframKernel location for the current session *)
117 | (* fileExt: file extension for executables *)
118 | (* pathSeperator: delimiter for directories on PATH *)
119 | defineGlobalVars[] :=
120 | Switch[
121 | $OperatingSystem,
122 | "Windows",
123 | mathBinSession = FileNameJoin[{$InstallationDirectory, "wolfram.exe"}];
124 | fileExt = ".exe";
125 | pathSeperator = ";";,
126 | "MacOSX",
127 | mathBinSession = FileNameJoin[{$InstallationDirectory, "MacOS", "WolframKernel"}];
128 | fileExt = "";
129 | pathSeperator = ":";,
130 | "Unix",
131 | mathBinSession = FileNameJoin[{$InstallationDirectory, "Executables", "WolframKernel"}];
132 | fileExt = "";
133 | pathSeperator = ":";
134 | ];
135 |
136 | mathBinSession := (defineGlobalVars[]; mathBinSession);
137 | fileExt := (defineGlobalVars[]; fileExt);
138 | pathSeperator := (defineGlobalVars[]; pathSeperator);
139 |
140 | (* a list of directories in PATH *)
141 | splitPath := StringSplit[Environment["PATH"], pathSeperator];
142 |
143 | (* restore PATH, if due to a bug, it becomes essentially empty; this is relevant to finding the Jupyter installation *)
144 | (* returns above *)
145 | attemptPathRegeneration[] := If[
146 | $OperatingSystem === "MacOSX" && FileType["~/.profile"] === File,
147 | Print["install.wls: Warning: Regenerating PATH ..."];
148 | SetEnvironment[
149 | "PATH" -> StringTrim[
150 | RunProcess[
151 | $SystemShell,
152 | "StandardOutput",
153 | StringJoin[Import["~/.profile", "String"], "\necho $PATH"],
154 | ProcessEnvironment -> {}
155 | ],
156 | "\n"
157 | ]
158 | ];
159 | ];
160 |
161 | (* find Jupyter installation path *)
162 | (* returns kernel IDs in Jupyter *)
163 | findJupyterPath[] :=
164 | SelectFirst[
165 | splitPath,
166 | (* check every directory in PATH to see if a Jupyter binary is a member *)
167 | (FileType[FileNameJoin[{#1, StringJoin["jupyter", fileExt]}]] === File)&
168 | ];
169 |
170 | (* get information about installed kernels in Jupyter *)
171 | (* returns kernel IDs in Jupyter *)
172 | getKernels[jupyterPath_String, processEnvironment_] :=
173 | Module[{json, kernelspecAssoc},
174 | (* obtain information about "jupyter kernelspec list" in JSON *)
175 | json = Quiet[ImportString[RunProcess[{jupyterPath, "kernelspec", "list", "--json"}, "StandardOutput", ProcessEnvironment -> processEnvironment], "JSON"]];
176 | (* transform that JSON information into an Association *)
177 | kernelspecAssoc =
178 | If[
179 | FailureQ[json],
180 | Association[],
181 | Replace[
182 | json,
183 | part_List /; AllTrue[part, Head[#1] === Rule &] -> Association @ part,
184 | {0, Infinity}
185 | ]
186 | ];
187 | Return[
188 | (* if the above process worked, just return the kernel IDs of all the kernelspecs *)
189 | (* otherwise, return an empty list *)
190 | If[
191 | KeyExistsQ[kernelspecAssoc, "kernelspecs"],
192 | Keys[kernelspecAssoc["kernelspecs"]],
193 | {}
194 | ]
195 | ];
196 | ];
197 |
198 |
199 | (* END: Helper symbols *)
200 |
201 | (* main install command *)
202 | (* specs: options \"WolframEngineBinary\" and \"JupyterInstallation\" in an Association, when provided *)
203 | (* removeQ: remove a Jupyter installation or not *)
204 | (* removeAllQ: clear all Jupyter installations or not *)
205 | (* removeQ first, removeAllQ second: "add" is False, False; "remove" is True, False, and "clear" is True, True *)
206 | configureJupyter[specs_Association, removeQ_?BooleanQ, removeAllQ_?BooleanQ] :=
207 | Module[
208 | {
209 | kernelScript,
210 | retrievedNames, kernelID, displayName,
211 | notProvidedQ,
212 | jupyterPath, mathBin,
213 | fileType,
214 | processEnvironment,
215 | baseDir, tempDir,
216 | wlKernels, (* wlKernelsL(owerCase) *) wlKernelsL,
217 | commandArgs,
218 | exitInfo, kernelspecAssoc, kernelspecs,
219 | conflictMessage, failureMessage
220 | },
221 |
222 | kernelScript = FileNameJoin[{projectHome, "WolframLanguageForJupyter", "Resources", "KernelForWolframLanguageForJupyter.wl"}];
223 | (* just check that the REPL script is there *)
224 | If[
225 | !(FileType[kernelScript] === File),
226 | Print[nopaclet];
227 | Return[$Failed];
228 | ];
229 |
230 | jupyterPath = specs["JupyterInstallation"];
231 | (* if no Jupyter installation path provided, determine it from PATH *)
232 | If[
233 | MissingQ[jupyterPath],
234 | jupyterPath = findJupyterPath[];
235 | (* if Jupyter not on PATH, message *)
236 | If[MissingQ[jupyterPath],
237 | Print[notfound];
238 | Return[$Failed];
239 | ];
240 | jupyterPath = FileNameJoin[{jupyterPath, StringJoin["jupyter", fileExt]}];
241 | ];
242 |
243 | mathBin =
244 | Lookup[
245 | specs,
246 | "WolframEngineBinary",
247 | (* if no "WolframEngineBinary" provided, use the session Wolfram Kernel location and set notProvidedQ to True *)
248 | (notProvidedQ = True; mathBinSession)
249 | ];
250 |
251 | (* check that the Jupyter installation path is a file *)
252 | If[
253 | !((fileType = FileType[jupyterPath]) === File),
254 | Switch[
255 | fileType,
256 | Directory,
257 | Print[isdir];,
258 | None,
259 | Print[nobin];
260 | ];
261 | Return[$Failed];
262 | ];
263 |
264 | {kernelID, displayName} = {"", ""};
265 | (* if not clearing, check that the Wolfram Engine installation path is a file, and message appropriately *)
266 | If[
267 | !(removeQ && removeAllQ),
268 | If[
269 | (fileType = FileType[mathBin]) === File,
270 | (* get the "Kernel ID" and "Display Name" for the new Jupyter kernel *)
271 | retrievedNames = getNames[mathBin, TrueQ[notProvidedQ]];
272 | If[FailureQ[retrievedNames], Print[nolink]; Return[$Failed]];
273 | {kernelID, displayName} = retrievedNames;,
274 | Switch[
275 | fileType,
276 | Directory,
277 | Print[isdirMath];,
278 | None,
279 | Print[nobinMath];
280 | ];
281 | Return[$Failed];
282 | ];
283 | ];
284 |
285 | (* as an association for 11.3 compatibility *)
286 | processEnvironment = Association[GetEnvironment[]];
287 | processEnvironment["PATH"] = StringJoin[Environment["PATH"], pathSeperator, DirectoryName[jupyterPath]];
288 |
289 | (* list of kernels in Jupyter to perform an action on *)
290 | wlKernels = {kernelID};
291 | tempDir = "";
292 | (* if adding, ...*)
293 | (* otherwise, when removing or clearing, ...*)
294 | If[
295 | !removeQ,
296 | failureMessage = notadded;
297 | conflictMessage = addconflict;
298 |
299 | (* create staging directory for files needed to register a kernel with Jupyter *)
300 | tempDir = CreateDirectory[
301 | FileNameJoin[{
302 | projectHome,
303 | CreateUUID[],
304 | (* removing this would cause every evalution of addKernelToJupyter adds a new kernel with a different uuid *)
305 | kernelID
306 | }], CreateIntermediateDirectories -> True
307 | ];
308 |
309 | (* export a JSON file to the staging directory that contains all the relevant information on how to run the kernel *)
310 | Export[
311 | FileNameJoin[{tempDir, "kernel.json"}],
312 | Association[
313 | "argv" -> {mathBin, "-script", kernelScript, "{connection_file}", "ScriptInstall" (* , "-noprompt" *)},
314 | "display_name" -> displayName,
315 | "language" -> "Wolfram Language"
316 | ]
317 | ];
318 |
319 | (* create a list of arguments that directs Jupyter to install from the staging directory *)
320 | commandArgs = {jupyterPath, "kernelspec", "install", "--user", tempDir};,
321 | failureMessage = notremoved;
322 | conflictMessage = removeconflict;
323 | (* create a list of arguments that directs Jupyter to remove ... *)
324 | commandArgs = {jupyterPath, "kernelspec", "remove", "-f",
325 | If[
326 | !removeAllQ,
327 | (* just the specified kernel *)
328 | kernelID,
329 | (* all Wolfram Language Jupyter kernels *)
330 | (* select from all kernel IDs in Jupyter those that match the form used by this install *)
331 | Sequence @@ (wlKernels = Select[getKernels[jupyterPath, processEnvironment], StringMatchQ[#1, (* ("WolframLanguage-" | "wl-") *) "WolframLanguage" ~~ ___, IgnoreCase -> True] &])
332 | ]
333 | }
334 | ];
335 | (* if no kernels to act on, quit *)
336 | If[Length[wlKernels] == 0, Return[];];
337 | wlKernelsL = ToLowerCase /@ wlKernels;
338 |
339 | (* for error detection, get a snapshot of kernels before the action is performed *)
340 | kernelspecs = getKernels[jupyterPath, processEnvironment];
341 | (* when adding, if there is a kernel with the same id already in Jupyter, it will be replaced; thus, message, but continue *)
342 | If[Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]], Print[conflictMessage];];
343 |
344 | (* perform the action *)
345 | exitInfo = RunProcess[commandArgs, All, ProcessEnvironment -> processEnvironment];
346 | (* remove temporary directory if it was created *)
347 | If[StringLength[tempDir] > 0, DeleteDirectory[DirectoryName[tempDir], DeleteContents -> True]];
348 |
349 | (* get list of kernels after the action was performed *)
350 | kernelspecs = getKernels[jupyterPath, processEnvironment];
351 | (* message about success with respect to the action that was performed *)
352 | If[
353 | !Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]],
354 | Print[failureMessage];
355 | Print["configure-jupyter.wls: See below for the message that Jupyter returned when attempting to add the Wolfram Engine."];
356 | Print[StringTrim[exitInfo["StandardError"], Whitespace]];
357 | Return[$Failed];
358 | ];
359 | ];
360 |
361 | (* checking RunProcess ..., and messaging appropriately *)
362 | If[
363 | FailureQ[RunProcess[$SystemShell, All, ""]],
364 | (* maybe remove *)
365 | If[
366 | MemberQ[$CommandLine, "-script"],
367 | Print["configure-jupyter.wls: Please use -file instead of -script in WolframScript."];
368 | Quit[];
369 | ,
370 | Print["configure-jupyter.wls: An unknown error has occurred."];
371 | attemptPathRegeneration[];
372 | If[FailureQ[RunProcess[$SystemShell, All, ""]], Quit[]];
373 | ];
374 | ];
375 |
376 | defineGlobalVars[];
377 |
378 | (* maybe remove *)
379 | (* checking PATH ..., and messaging appropriately *)
380 | If[
381 | Length[splitPath] == 1,
382 | Print["configure-jupyter.wls: Warning: This script has encountered a very small PATH environment variable."];
383 | Print["configure-jupyter.wls: Warning: This can occur due to a possible WolframScript bug."];
384 | attemptPathRegeneration[];
385 | ];
386 |
387 |
388 | (* START: Building usage message *)
389 |
390 | templateJupyterPath = StringJoin["\"", FileNameJoin[{"path", "to", "Jupyter-binary"}], "\""];
391 | templateWLPath = StringJoin["\"", FileNameJoin[{"", "absolute", "path", "to", "Wolfram-Engine-binary--not-wolframscript"}], "\""];
392 |
393 | (* helpMessage = StringJoin[
394 | "configure-jupyter.wls add [", templateJupyterPath, "]\n",
395 | "configure-jupyter.wls adds a Wolfram Engine to a Jupyter binary on PATH, or optional provided Jupyter binary path\n",
396 | "configure-jupyter.wls add ", templateJupyterPath, " ", templateWLPath, "\n",
397 | "\tadds the provided absolute Wolfram Engine binary path to the provided Jupyter binary path\n",
398 | "configure-jupyter.wls remove [", templateJupyterPath ,"]\n",
399 | "\tremoves any Wolfram Engines found on a Jupyter binary on PATH, or optional provided Jupyter binary path"
400 | ]; *)
401 |
402 | helpMessage = StringJoin[
403 | "configure-jupyter.wls add [", templateWLPath, "]\n",
404 | "\tadds a Wolfram Engine, either attached to the current invocation, or at the provided absolute Wolfram Engine binary path, to a Jupyter binary on PATH\n",
405 | "configure-jupyter.wls add ", templateWLPath, " ", templateJupyterPath, "\n",
406 | "\tadds the provided absolute Wolfram Engine binary path to the provided Jupyter binary path\n",
407 | "configure-jupyter.wls remove [", templateWLPath ,"]\n",
408 | "\tremoves the Wolfram Engine, either attached to the current invocation, or at the provided absolute Wolfram Engine binary path, from a Jupyter binary on PATH\n",
409 | "configure-jupyter.wls remove ", templateWLPath, " ", templateJupyterPath, "\n",
410 | "\tremoves the provided absolute Wolfram Engine binary path from the provided Jupyter binary path\n",
411 | "configure-jupyter.wls clear [", templateJupyterPath ,"]\n",
412 | "\tremoves all Wolfram Engines found on a Jupyter binary on PATH, or optional provided Jupyter binary path\n",
413 | "configure-jupyter.wls build\n",
414 | "\tbuilds the WolframLanguageForJupyter paclet in the project directory"
415 | ];
416 |
417 | (* END: Building usage message *)
418 |
419 |
420 | (* based off of the script invocation, use configureJupyter or PackPaclet; or display help message *)
421 | If[
422 | Length[$ScriptCommandLine] < 2 ||
423 | Length[$ScriptCommandLine] > 4 ||
424 | $ScriptCommandLine[[2]] === "help",
425 | Print[helpMessage];
426 | ,
427 | Switch[
428 | $ScriptCommandLine[[2]],
429 | "add" | "Add",
430 | command = {False, False};,
431 | "remove" | "Remove",
432 | command = {True, False};,
433 | "clear" | "Clear",
434 | command = {True, True};,
435 | "build",
436 | PackPaclet["WolframLanguageForJupyter"];
437 | Quit[];
438 | ,
439 | _,
440 | Print[helpMessage];
441 | ];
442 |
443 | configureJupyter[
444 | Switch[
445 | Length[$ScriptCommandLine],
446 | 4,
447 | Association[
448 | "WolframEngineBinary" -> $ScriptCommandLine[[3]],
449 | "JupyterInstallation" -> $ScriptCommandLine[[4]]
450 | ],
451 | 3,
452 | If[command === {True, True},
453 | Association["JupyterInstallation" -> $ScriptCommandLine[[3]]],
454 | Association["WolframEngineBinary" -> $ScriptCommandLine[[3]]]
455 | ],
456 | 2,
457 | Association[]
458 | ],
459 | Sequence @@ command
460 | ];
461 | ];
462 |
463 | End[];
464 |
--------------------------------------------------------------------------------
/WolframLanguageForJupyter/Resources/EvaluationUtilities.wl:
--------------------------------------------------------------------------------
1 | (************************************************
2 | EvaluationUtilities.wl
3 | *************************************************
4 | Description:
5 | Utilities for evaluating input arriving
6 | from Jupyter, and for simulating
7 | the Mathematica REPL
8 | Symbols defined:
9 | Print,
10 | redirectPrint,
11 | redirectMessages,
12 | simulatedEvaluate
13 | *************************************************)
14 |
15 | (************************************
16 | Get[] guard
17 | *************************************)
18 |
19 | If[
20 | !TrueQ[WolframLanguageForJupyter`Private`$GotEvaluationUtilities],
21 |
22 | WolframLanguageForJupyter`Private`$GotEvaluationUtilities = True;
23 |
24 | (************************************
25 | load required
26 | WolframLanguageForJupyter
27 | files
28 | *************************************)
29 |
30 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* loopState, applyHook, $canUseFrontEnd *)
31 |
32 | (************************************
33 | wrapper for interacting
34 | with the cloud
35 | *************************************)
36 |
37 | (* Interact is a wrapper, open to the user, for asking that the result of the evaluation
38 | be displayed as an embedded cloud object that can be interacted with *)
39 | (* set Interact to not evaluate its arguments *)
40 | SetAttributes[Interact, HoldAll];
41 |
42 | (************************************
43 | private symbols
44 | *************************************)
45 |
46 | (* begin the private context for WolframLanguageForJupyter *)
47 | Begin["`Private`"];
48 |
49 | (************************************
50 | helper utilities for
51 | diagnosing and removing
52 | command wrappers
53 | (e.g., Interact[])
54 | *************************************)
55 |
56 | (* check if expr is wrapped with Interact *)
57 | interactQ[expr___] := MatchQ[expr, Hold[Interact[___]]];
58 | SetAttributes[interactQ, HoldAll];
59 |
60 | (* remove any Interact wrappers,
61 | apply $Pre,
62 | and bring in the Front End for the evaluation of expr *)
63 | uninteract[Interact[expr_]] := UsingFrontEnd[applyHook[$Pre, expr]];
64 | uninteract[expr_] := UsingFrontEnd[applyHook[$Pre, expr]];
65 | SetAttributes[uninteract, HoldAll];
66 |
67 | (************************************
68 | version of Print that
69 | sends output to Jupyter
70 | *************************************)
71 |
72 | (* redirect Print calls into a message to Jupyter, in order to print in Jupyter *)
73 | (* TODO: review other methods: through EvaluationData or WSTP so we don't redefine Print *)
74 | (* TODO: remove this and just permanently set $Output to {..., loopState["WolframLanguageForJupyter-stdout"], ...} *)
75 | Unprotect[Print];
76 | Print[ourArgs___, opts:OptionsPattern[]] :=
77 | Block[
78 | {
79 | $inPrint = True,
80 | $Output
81 | },
82 | If[
83 | loopState["printFunction"] =!= False,
84 | $Output = {OpenWrite[FormatType -> OutputForm]};
85 | If[
86 | !FailureQ[First[$Output]],
87 | Print[ourArgs, opts];
88 | loopState["printFunction"][
89 | Import[$Output[[1,1]], "String"]
90 | ];
91 | Close[First[$Output]];
92 | ];
93 | ];
94 | ] /; !TrueQ[$inPrint];
95 | Protect[Print];
96 |
97 | (************************************
98 | version of Echo that
99 | sends output to Jupyter
100 | *************************************)
101 |
102 | (* redirect Echo calls into a message to Jupyter, in order to print/echo in Jupyter *)
103 | Echo[""];
104 | Unprotect[Echo];
105 | DownValues[Echo] =
106 | Prepend[
107 | DownValues[Echo],
108 | HoldPattern[Echo[ourArgs___, opts:OptionsPattern[]]] :> Block[{$inEcho = True, $Notebooks = False}, Echo[ourArgs, opts]] /; !TrueQ[$inEcho]
109 | ];
110 | Protect[Echo];
111 |
112 | (************************************
113 | version of Write that
114 | sends output to Jupyter
115 | *************************************)
116 |
117 | (* redirect Write["stdout", ourArgs___] calls to Write[loopState["WolframLanguageForJupyter-stdout"], ourArgs___],
118 | in order to print in Jupyter *)
119 | Unprotect[Write];
120 | Write["stdout", ourArgs___, opts:OptionsPattern[]] :=
121 | Block[
122 | {
123 | $inWrite = True
124 | },
125 | If[
126 | loopState["WolframLanguageForJupyter-stdout"] =!= False,
127 | Write[loopState["WolframLanguageForJupyter-stdout"], ourArgs]
128 | ];
129 | ] /; !TrueQ[$inWrite];
130 | Protect[Write];
131 | (* redirect Write[{before___, "stdout", after___}, ourArgs___] calls to
132 | Write[{before___, "WolframLanguageForJupyter-stdout", after___}, ourArgs___],
133 | in order to print in Jupyter *)
134 | Unprotect[Write];
135 | Write[{before___, "stdout", after___}, ourArgs___, opts:OptionsPattern[]] :=
136 | Block[
137 | {
138 | $inWrite = True
139 | },
140 | If[
141 | loopState["WolframLanguageForJupyter-stdout"] =!= False,
142 | Write[{before, loopState["WolframLanguageForJupyter-stdout"], after}, ourArgs]
143 | ];
144 | ] /; !TrueQ[$inWrite];
145 | Protect[Write];
146 |
147 | (************************************
148 | version of WriteString that
149 | sends output to Jupyter
150 | *************************************)
151 |
152 | (* redirect WriteString["stdout", ourArgs___] calls to WriteString[loopState["WolframLanguageForJupyter-stdout"], ourArgs___],
153 | in order to print in Jupyter *)
154 | Unprotect[WriteString];
155 | WriteString["stdout", ourArgs___, opts:OptionsPattern[]] :=
156 | Block[
157 | {
158 | $inWriteString = True
159 | },
160 | If[
161 | loopState["WolframLanguageForJupyter-stdout"] =!= False,
162 | WriteString[loopState["WolframLanguageForJupyter-stdout"], ourArgs]
163 | ];
164 | ] /; !TrueQ[$inWriteString];
165 | Protect[WriteString];
166 | (* redirect WriteString[{before___, "stdout", after___}, ourArgs___] calls to
167 | WriteString[{before___, "WolframLanguageForJupyter-stdout", after___}, ourArgs___],
168 | in order to print in Jupyter *)
169 | Unprotect[WriteString];
170 | WriteString[{before___, "stdout", after___}, ourArgs___, opts:OptionsPattern[]] :=
171 | Block[
172 | {
173 | $inWriteString = True
174 | },
175 | If[
176 | loopState["WolframLanguageForJupyter-stdout"] =!= False,
177 | WriteString[{before, loopState["WolframLanguageForJupyter-stdout"], after}, ourArgs]
178 | ];
179 | ] /; !TrueQ[$inWriteString];
180 | Protect[WriteString];
181 |
182 | (************************************
183 | versions of Quit and Exit that
184 | ask the Jupyter console
185 | to quit, if running under
186 | a Jupyter console
187 | *************************************)
188 |
189 | Unprotect[Quit];
190 | Quit[ourArgs___] :=
191 | Block[
192 | {$inQuit = True},
193 | If[
194 | loopState["isCompleteRequestSent"],
195 | loopState["askExit"] = True;,
196 | Quit[ourArgs];
197 | ];
198 | ] /;
199 | (
200 | (!TrueQ[$inQuit]) &&
201 | (
202 | (Length[{ourArgs}] == 0) ||
203 | ((Length[{ourArgs}] == 1) && IntegerQ[ourArgs])
204 | )
205 | );
206 | Protect[Quit];
207 |
208 | Unprotect[Exit];
209 | Exit[ourArgs___] :=
210 | Block[
211 | {$inExit = True},
212 | If[
213 | loopState["isCompleteRequestSent"],
214 | loopState["askExit"] = True;,
215 | Exit[ourArgs];
216 | ];
217 | ] /;
218 | (
219 | (!TrueQ[$inExit]) &&
220 | (
221 | (Length[{ourArgs}] == 0) ||
222 | ((Length[{ourArgs}] == 1) && IntegerQ[ourArgs])
223 | )
224 | );
225 | Protect[Exit];
226 |
227 | (************************************
228 | redirection utilities
229 | *************************************)
230 |
231 | (* redirect Print to Jupyter *)
232 | redirectPrint[currentSourceFrame_, printText_] :=
233 | (* send a frame *)
234 | sendFrame[
235 | (* on the IO Publish socket *)
236 | ioPubSocket,
237 | (* create the frame *)
238 | createReplyFrame[
239 | (* using the current source frame *)
240 | currentSourceFrame,
241 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#streams-stdout-stderr-etc *)
242 | (* with a message type of "stream" *)
243 | "stream",
244 | (* and with message content that tells Jupyter what to Print, and to use stdout *)
245 | ExportString[
246 | Association[
247 | "name" -> "stdout",
248 | "text" -> printText
249 | ],
250 | "JSON",
251 | "Compact" -> True
252 | ],
253 | (* and without branching off *)
254 | False
255 | ]
256 | ];
257 |
258 | (* redirect messages to Jupyter *)
259 | redirectMessages[currentSourceFrame_, messageName_, messageText_, addNewline_, dropMessageName_:False] :=
260 | Module[
261 | {
262 | (* string forms of the arguments messageName and messageText *)
263 | messageNameString,
264 | messageTextString
265 | },
266 | (* generate string forms of the arguments *)
267 | messageNameString = ToString[HoldForm[messageName]];
268 | messageTextString = ToString[messageText];
269 | (* send a frame *)
270 | sendFrame[
271 | (* on the IO Publish socket *)
272 | ioPubSocket,
273 | (* create the frame *)
274 | createReplyFrame[
275 | (* using the current source frame *)
276 | currentSourceFrame,
277 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#execution-errors *)
278 | (* with a message type of "error" *)
279 | "error",
280 | (* and with appropriate message content *)
281 | ExportString[
282 | Association[
283 | (* use the provided message name here *)
284 | "ename" -> messageNameString,
285 | (* use the provided message text here *)
286 | "evalue" -> messageTextString,
287 | (* use the provided message name and message text here (unless dropMessageName) *)
288 | "traceback" ->
289 | {
290 | (* output the message in red *)
291 | StringJoin[
292 | "\033[0;31m",
293 | If[
294 | dropMessageName,
295 | (* use only the message text here if dropMessageName is True *)
296 | messageTextString,
297 | (* otherwise, combine the message name and message text *)
298 | ToString[System`ColonForm[HoldForm[messageName], messageText]]
299 | ],
300 | (* if addNewline, add a newline *)
301 | If[addNewline, "\n", ""],
302 | "\033[0m"
303 | ]
304 | }
305 | ],
306 | "JSON",
307 | "Compact" -> True
308 | ],
309 | (* and without branching off *)
310 | False
311 | ]
312 | ];
313 | (* ... and return an empty string to the Wolfram Language message system *)
314 | Return[""];
315 | ];
316 | SetAttributes[redirectMessages, HoldAll];
317 |
318 | (************************************
319 | utilities for splitting
320 | input by Wolfram Language
321 | expression
322 | *************************************)
323 |
324 | (* start parsing the input, and return an Association for keeping track of said parse *)
325 | startParsingInput[codeStr_] :=
326 | Module[
327 | {
328 | (* an Association for keeping track of the parse containing:
329 | "LinesLeft" - the lines of input left to be processed,
330 | "ExpressionsParsed" - the number of expressions parsed out so far,
331 | "ExpressionString" - an expression string generated by a pass of the parser,
332 | "SyntaxError" - a flag for if the expression string contains a syntax error,
333 | "ParseDone" - a flag for if the parse is done *)
334 | parseTrackerInit
335 | },
336 |
337 | (* initialize parseTrackerInit *)
338 | parseTrackerInit = Association[];
339 |
340 | (* add the annotated lines left, and the defaults *)
341 | AssociateTo[
342 | parseTrackerInit,
343 | {
344 | "LinesLeft" -> StringSplit[StringJoin[" ", codeStr], "\r\n" | "\n"],
345 | "ExpressionsParsed" -> 0,
346 | "ExpressionString" -> " ",
347 | "SyntaxError" -> False,
348 | "ParseDone" -> False
349 | }
350 | ];
351 |
352 | (* return the parse tracker *)
353 | Return[parseTrackerInit];
354 | ];
355 |
356 | (* parse out an expression *)
357 | parseIntoExpr[tracker_] :=
358 | Module[
359 | {
360 | (* the parse tracker to be updated with the results of this parse *)
361 | newTracker,
362 |
363 | (* for storing the lines of input left to be processed, annotated with their line numbers *)
364 | annotatedLinesLeft,
365 |
366 | (* for storing the current line position when moving through annotatedLinesLeft *)
367 | linePos,
368 |
369 | (* for storing the result *)
370 | result,
371 |
372 | (* for storing the concatenation of some lines of input *)
373 | exprStr
374 | },
375 |
376 | (* initialize the parse tracker to be updated *)
377 | newTracker = tracker;
378 |
379 | (* if there are no lines of input left to be processed,
380 | set newTracker["ParseDone"] to True and return the updated tracker *)
381 | If[Length[newTracker["LinesLeft"]] == 0,
382 | newTracker["ParseDone"] = True;
383 | (* return the updated tracker *)
384 | Return[newTracker];
385 | ];
386 |
387 | (* annotate the lines of input left to be processed with their line numbers *)
388 | annotatedLinesLeft =
389 | Partition[
390 | Riffle[
391 | newTracker["LinesLeft"],
392 | Range[Length[newTracker["LinesLeft"]]]
393 | ],
394 | 2
395 | ];
396 |
397 | (* start with a line position of 1 *)
398 | linePos = 1;
399 | result =
400 | (* fold until an expression is built, or there are no lines of input left *)
401 | Fold[
402 | (
403 | (* save the new line position *)
404 | linePos = Last[{##}][[2]] + 1;
405 | (* save the new expression string with a new line of input *)
406 | exprStr = StringJoin[First /@ {##}];
407 | (* if exprStr is syntactically correct, it represents a complete expression,
408 | and folding can be stopped *)
409 | If[SyntaxQ[exprStr],
410 | (* return the complete expression string *)
411 | Return[{exprStr, linePos, False}, Fold];,
412 | (* exprStr may be incomplete, keep going if possible *)
413 | {exprStr, linePos, True}
414 | ]
415 | ) &,
416 | (* starting value *)
417 | {"", -1, False},
418 | (* the annotated lines of input left to be processed *)
419 | annotatedLinesLeft
420 | ];
421 |
422 | AssociateTo[
423 | newTracker,
424 | {
425 | (* discard the lines of input processed *)
426 | "LinesLeft" -> newTracker["LinesLeft"][[result[[2]];;]],
427 | (* increment ExpressionsParsed *)
428 | "ExpressionsParsed" -> newTracker["ExpressionsParsed"] + 1,
429 | (* save the result generated *)
430 | "ExpressionString" -> result[[1]],
431 | (* save the syntactic correctness of the result *)
432 | "SyntaxError" -> result[[3]]
433 | }
434 | ];
435 |
436 | (* return the updated tracker *)
437 | Return[newTracker];
438 | ];
439 |
440 | (************************************
441 | main evaluation command
442 | *************************************)
443 |
444 | (* evaluate input, and capture required information such as generated messages *)
445 | (* TODO: review other method: evaluate input through WSTP in another kernel *)
446 | simulatedEvaluate[codeStr_] :=
447 | Module[
448 | {
449 | (* for saving $Messages before changing it *)
450 | oldMessages,
451 | (* the stream used to capture generated messages *)
452 | stream,
453 | (* the string form of the generated messages, obtained from stream *)
454 | generatedMessages,
455 |
456 | (* for keeping track of parsing the input into separate expressions *)
457 | parseTracker,
458 |
459 | (* a raw evaluation result to be built, before Nulls have been removed *)
460 | rawEvaluationResult,
461 |
462 | (* for storing a single expression string *)
463 | exprStr,
464 |
465 | (* the result of evaluation *)
466 | evaluationResult,
467 |
468 | (* for storing final results *)
469 | result,
470 |
471 | (* the total result of the evaluation:
472 | an association containing
473 | the result of evaluation ("EvaluationResult"),
474 | indices of the output lines of the result ("EvaluationResultOutputLineIndices"),
475 | the total number of indices consumed by this evaluation ("ConsumedIndices"),
476 | generated messages ("GeneratedMessages"),
477 | if the input was one expression and wrapped with Interact[] ("InteractStatus")
478 | *)
479 | totalResult
480 | },
481 |
482 | (* create the association for the total result of the evaluation *)
483 | totalResult = Association[];
484 |
485 | (* save $Messages before overwrite *)
486 | oldMessages = $Messages;
487 | (* open stream to write messages into *)
488 | stream = OpenWrite[];
489 |
490 | (* clear the list of generated messages *)
491 | Unprotect[$MessageList]; $MessageList = {}; Protect[$MessageList];
492 |
493 | (* set $Messages to use the new stream *)
494 | $Messages = {stream};
495 |
496 | (* start the parse of the input *)
497 | parseTracker =
498 | startParsingInput[
499 | (* apply $PreRead to the input *)
500 | applyHook[$PreRead, codeStr]
501 | ];
502 |
503 | (* initialize rawEvaluationResult to an empty list *)
504 | rawEvaluationResult = {};
505 | (* while the parse is not done, keep evaluating expressions in the input *)
506 | While[
507 | (
508 | parseTracker = parseIntoExpr[parseTracker];
509 | !parseTracker["ParseDone"]
510 | ),
511 |
512 | (* save the current expression string *)
513 | exprStr = parseTracker["ExpressionString"];
514 |
515 | If[
516 | !parseTracker["SyntaxError"],
517 | (* increment $Line *)
518 | $Line++;
519 | (* set InString *)
520 | Unprotect[InString];
521 | InString[
522 | loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1
523 | ] = exprStr;
524 | Protect[InString];
525 | ];
526 |
527 | (* evaluate the expression string *)
528 | (* regarding Internal`AllowExceptions, we need to generate the results and messages that
529 | are expected when a user evaluation is interrupted by behavior such as an uncaught Throw
530 | statement, while making sure that the simulated evaluation loop is not interrupted by the
531 | same behavior; my hope is that we can achieve this by using Internal`AllowExceptions as
532 | essentially a version of CheckAll that does not silence messages such as Throw::nocatch *)
533 | result =
534 | Internal`AllowExceptions[
535 | ToExpression[
536 | exprStr,
537 | InputForm,
538 | uninteract
539 | ]
540 | ];
541 |
542 | If[
543 | !parseTracker["SyntaxError"],
544 | (* set the In[] for this expression *)
545 | Unprotect[In];
546 | Replace[
547 | ToExpression[exprStr, InputForm, Hold],
548 | Hold[held_] :>
549 | SetDelayed[
550 | In[
551 | loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1
552 | ],
553 | held
554 | ]
555 | ];
556 | Protect[In];
557 | (* apply $Post to the result *)
558 | result = applyHook[$Post, result];
559 | (* set the Out[] for this expression *)
560 | Unprotect[Out];
561 | Out[loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1] = result;
562 | Protect[Out];
563 | (* create the overall result with $PrePrint *)
564 | result = applyHook[$PrePrint, result];
565 | ,
566 | (* syntax error *)
567 | result = $Failed;
568 | ];
569 |
570 | (* save the result in rawEvaluationResult *)
571 | AppendTo[rawEvaluationResult, result];
572 | ];
573 |
574 | (* add the Interact[] wrapper status of the input *)
575 | AssociateTo[
576 | totalResult,
577 | "InteractStatus" ->
578 | (
579 | (* if the input has no syntax errors,
580 | is made up of only one expression,
581 | and is wrapped with Interact[],
582 | mark "InteractStatus" as True
583 | *)
584 | parseTracker["ExpressionsParsed"] == 1 &&
585 | !parseTracker["SyntaxError"] &&
586 | (interactQ @ ToExpression[parseTracker["ExpressionString"], InputForm, Hold])
587 | )
588 | ];
589 |
590 | (* evaluate the input from Jupyter, removing Nulls from the Output *)
591 | evaluationResult = DeleteCases[rawEvaluationResult, Null];
592 |
593 | (* preserve the locations of the output lines with respect to the Nulls *)
594 | AssociateTo[
595 | totalResult,
596 | "EvaluationResultOutputLineIndices" ->
597 | (
598 | (loopState["executionCount"] - 1) +
599 | Flatten[Position[rawEvaluationResult, Except[Null], {1}, Heads -> False]]
600 | )
601 | ];
602 |
603 | (* restore $Messages *)
604 | $Messages = oldMessages;
605 |
606 | (* obtain generated messages *)
607 | generatedMessages = Import[stream[[1]], "String"];
608 | (* close the opened stream *)
609 | Close[stream];
610 |
611 | (* add the total number of indices consumed by this evaluation *)
612 | AssociateTo[
613 | totalResult,
614 | "ConsumedIndices" ->
615 | (* if parseTracker["SyntaxError"] is true, one less index was consumed *)
616 | If[
617 | parseTracker["SyntaxError"],
618 | parseTracker["ExpressionsParsed"] - 1,
619 | parseTracker["ExpressionsParsed"]
620 | ]
621 | ];
622 |
623 | (* add the result of the evaluation and any generated messages to totalResult *)
624 | AssociateTo[totalResult, {"EvaluationResult" -> evaluationResult, "GeneratedMessages" -> generatedMessages}];
625 |
626 | (* return totalResult *)
627 | Return[totalResult];
628 | ];
629 | (* set simulatedEvaluate to not implicitly evaluate its arguments *)
630 | SetAttributes[simulatedEvaluate, HoldAll];
631 |
632 | (* end the private context for WolframLanguageForJupyter *)
633 | End[]; (* `Private` *)
634 |
635 | (************************************
636 | Get[] guard
637 | *************************************)
638 |
639 | ] (* WolframLanguageForJupyter`Private`$GotEvaluationUtilities *)
640 |
--------------------------------------------------------------------------------