magicpatch
Version:
Adds IPython / Jupyter magic commands to IJavascript
378 lines (319 loc) • 12.3 kB
JavaScript
/*
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
uuuuuuu
uu$$$$$$$$$$$uu
uu$$$$$$$$$$$$$$$$$uu
u$$$$$$$$$$$$$$$$$$$$$u
u$$$$$$$$$$$$$$$$$$$$$$$u
u$$$$$$$$$$$$$$$$$$$$$$$$$u
u$$$$$$$$$$$$$$$$$$$$$$$$$u
u$$$$$$" "$$$" "$$$$$$u
"$$$$" u$u $$$$"
$$$u u$u u$$$
$$$u u$$$u u$$$
"$$$$uu$$$ $$$uu$$$$"
"$$$$$$$" "$$$$$$$"
u$$$$$$$u$$$$$$$u
u$"$"$"$"$"$"$u
uuu $$u$ $ $ $ $u$$ uuu
u$$$$ $$$$$u$u$u$$$ u$$$$
$$$$$uu "$$$$$$$$$" uu$$$$$$
u$$$$$$$$$$$uu """"" uuuu$$$$$$$$$$
$$$$"""$$$$$$$$$$uuu uu$$$$$$$$$"""$$$"
""" ""$$$$$$$$$$$uu ""$"""
uuuu ""$$$$$$$$$$uuu
u$$$uuu$$$$$$$$$uu ""$$$$$$$$$$$uuu$$$
$$$$$$$$$$"""" ""$$$$$$$$$$$"
"$$$$$" ""$$$$""
$$$" $$$$"
8b d8 8b d8 8b d8
Y8, ,8P Y8, ,8P Y8, ,8P
`8b d8' `8b d8' `8b d8'
Y88P Y88P Y88P
d88b d88b d88b
,8P Y8, ,8P Y8, ,8P Y8,
d8' `8b d8' `8b d8' `8b
8P Y8 8P Y8 8P Y8
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
This monkey patches vm.runInThisContext with our own interpreter.
*/
const utils = require("./utilities");
const {exec, varSubst, setHiddenGlobal, parserRegExps} = utils;
const {startOfString, optionalAssignmentStmt} = parserRegExps;
const origVmRunInThisContext = global.vm.runInThisContext;
function vmMonkeyPatch() {
global.vm.runInThisContext = ijavascriptMonkeyPatch;
// private variables
setHiddenGlobal("__magicpatchInternal", global.__magicpatchInternal || {});
global.__magicpatchInternal.$$ = global.$$;
global.__magicpatchInternal.addMagic = addMagic;
global.__magicpatchInternal.initialized = true;
global.__magicpatchInternal.kernelDir = __dirname;
global.__magicpatchInternal.origVmRunInThisContext = origVmRunInThisContext;
global.__magicpatchInternal.debugInterp = false;
global.__magicpatchInternal.initialDir = process.cwd();
global.__magicpatchInternal.ijavascriptMonkeyPatch = ijavascriptMonkeyPatch;
}
function ijavascriptMonkeyPatch(code, ... args) {
// if we are re-loading the kernel, we may be executing code before initialization
if (global.__magicpatchInternal.initialized) {
try {
code = code.trim();
return magicInterpreter(code)
.then((ret) => {
// save the command to history
saveInput(code);
// save the output of the command
saveOutput(ret);
return ret;
})
.catch((err) => {
// save the command to history
saveInput(code);
// save the output of the command
saveOutput(err);
printError(err);
});
} catch (err) {
// save the command to history
saveInput(code);
// save the output of the command
saveOutput(err);
printError(err);
return undefined;
}
}
return origVmRunInThisContext(code, ... args);
}
function printError(err) {
if (!(err instanceof TypeError)) {
// print the whole stack if it's not a type error
console.error(err);
} else if (err.message !== "") {
// empty error message gives us a way to pop out of the stack without printing a message
// used for commander, which prints its own error messages
console.error(err.message);
}
}
/* XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX */
/* global $$ */
// add our built-in magics
// XXX: do this first so that the rest of the magics can use it
const {addMagic, magicMap} = require("./magics/addmagic");
require("./magics");
// config
let config = {
requireMagic: true,
kernelDir: __dirname,
origVmRunInThisContext: origVmRunInThisContext,
};
let debugInterpLog;
// Magic Interpreter
async function magicInterpreter(code) {
// Split string into lines
let lines = code.split("\n");
let codeLines = [];
let ret;
// enable / disable debugging
if (global.__magicpatchInternal.debugInterp) {
debugInterpLog = console.log;
} else {
debugInterpLog = () => {};
}
// See if any line is a magic or command
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let found = false;
// the $$ global gets rebuilt on every 'execute' call, so we have to reassign it
// see also: https://github.com/n-riesco/nel/blob/ea42faf8170813b89eadbf00d4696cdd8adbc51b/lib/server/context.js#L322
global.$$.addMagic = addMagic;
// See if string matches a magic command
for (let magicObj of magicMap.values()) {
let {hasMatch, hasAssignment, showDoc, showCode, matchParts} = tryMatchMagic(magicObj, line);
if (hasMatch) {
found = true;
// if there is any code that hasn't been executed, run it now
ret = await runCode(codeLines, ret);
// build the 'this' context
let ctx = {
interpreter: magicInterpreter,
lineNo: i,
magicObj,
exec,
varSubst,
utils,
magicMap,
history: global.In,
config,
code,
line,
showDoc,
showCode,
hasAssignment,
matchParts,
};
// copy over magicObj.ctx properties to ctx (overwriting where properties exist)
Object.keys(magicObj.ctx).forEach((key) => ctx[key] = magicObj.ctx[key]);
// run the magic
ret = await runMagic(magicObj, ctx, ret);
// if this was a cell magic on the first line, we're done
if (i === 0 && magicObj.cellMagic) {
return ret;
}
}
}
// looks like a cell magic but wasn't found
let magicMatch = line.match(new RegExp(`${startOfString}${optionalAssignmentStmt}(?<potentialMagic>%%\\w+)`));
if (!found && magicMatch) {
throw new TypeError(`UsageError: Cell magic '${magicMatch.groups.potentialMagic}' not found.`);
}
// looks like a line magic but wasn't found
magicMatch = line.match(new RegExp(`${startOfString}${optionalAssignmentStmt}(?<potentialMagic>%\\w+)`));
if (!found && magicMatch) {
throw new TypeError(`UsageError: Line magic function '${magicMatch.groups.potentialMagic}' not found.`);
}
// if it's not a magic, it must be code...
if (!found) {
codeLines.push(line);
}
}
// if there is still code that hasn't been run, run it now
ret = await runCode(codeLines, ret);
// Reassemble string and return new code
return ret;
}
function tryMatchMagic(magicObj, line) {
let res = line.match(magicObj.matcher);
debugInterpLog("line", line);
debugInterpLog("magicObj.name", magicObj.name);
debugInterpLog("matcher", magicObj.matcher);
debugInterpLog("match", res);
// if res is null, there's no match
let hasMatch = !!res;
res = res || {};
let parts = res.groups || {};
debugInterpLog("parts", parts);
// automagic
if (config.requireMagic && !parts.magicSymbol) {
debugInterpLog("match rejected due to automagic");
hasMatch = false;
}
// save doc bits
let getDoc = parts.getDocFront || parts.getDocBack;
let showDoc = getDoc === "?";
let showCode = getDoc === "??";
// has assignment
let hasAssignment = !!parts.assignmentStmt;
debugInterpLog("hasAssignment", hasAssignment);
debugInterpLog("hasMatch", hasMatch);
return {
hasMatch,
hasAssignment,
showDoc,
showCode,
matchParts: parts,
};
}
async function runMagic(magicObj, ctx, ret) {
let args = buildArgs(ctx);
// don't allow cell magic after line 0
if (magicObj.cellMagic && ctx.lineNo !== 0) {
throw new TypeError(`cell magic '${magicObj.name}' is only valid on first line of cell`);
}
// show documentation and return
if (ctx.showDoc) {
let output = `__${magicObj.name} Documentation:__\n\n`;
output += `${magicObj.doc}\n\n`;
output += `__File:__ ${magicObj.file}`;
$$.mime({"text/markdown": output});
return ret;
}
// show code and return
if (ctx.showCode) {
let output = `__${magicObj.name} Source:__\n\n`;
output += "``` js\n";
output += `${magicObj.fn.toString()}\n`;
output += "```\n\n";
output += `__File:__ ${magicObj.file}`;
$$.mime({"text/markdown": output});
return ret;
}
// run the magic!
ret = await magicObj.fn.call(ctx, ... args);
// assign the result to a variable
if (ctx.hasAssignment) {
global.__magicpatchInternal.intermediateValue = ret;
ret = await runCode([`${ctx.matchParts.assignmentStmt}global.__magicpatchInternal.intermediateValue;`], ret);
delete global.__magicpatchInternal.intermediateValue;
}
return ret;
}
function buildArgs(ctx) {
let {line, matchParts} = ctx;
let magicSymbol = matchParts.magicSymbol || "";
let cmdName = matchParts.cmdName || "";
ctx.startArgs = magicSymbol.length + cmdName.length + 1;
// strip the variable assignment off the front of the line
if (ctx.hasAssignment) {
ctx.startArgs += ctx.matchParts.assignmentStmt.length;
line = line.substr(ctx.matchParts.assignmentStmt.length);
}
// split the line into arguments and do variable substitution
let args = line.trim().replace(/\s+/g, " ").split(" ").map(varSubst);
// run the parser if there is one
if (typeof ctx.magicObj.argsParser === "function") {
ctx.args = ctx.magicObj.argsParser(... args);
}
debugInterpLog("line", line);
debugInterpLog("args", args);
return args;
}
async function runCode(codeLines, ret) {
// make sure there is code to run
if (!codeLines.length) {
return ret;
}
// re-assemble the array of code lines into a code string
let code = codeLines.join("\n");
// clear out / consume the code lines
codeLines.length = 0;
// run the code!
debugInterpLog("running code:", code);
return origVmRunInThisContext(code);
}
function saveOutput(scriptResult) {
Promise.resolve(scriptResult).then((val) => {
// NOTE: the docs say that output is also cached in _<n> and Out[n]
// https://ipython.readthedocs.io/en/stable/interactive/reference.html#output-caching-system
// This doesn't appear to be on by default, presumably due to the "heavy memory demands"
setHiddenGlobal("___", global.__);
setHiddenGlobal("__", global._);
setHiddenGlobal("_", val);
});
}
function saveInput(val) {
setHiddenGlobal("_iii", global._ii);
setHiddenGlobal("_ii", global._i);
setHiddenGlobal("_i", val);
if (!Array.isArray(global.In)) {
setHiddenGlobal("In", []);
}
if (!Array.isArray(global._ih)) {
setHiddenGlobal("_ih", []);
}
let newValName = `_i${global.In.length}`;
setHiddenGlobal(newValName, val);
// Note: history not implemented in protocol
// https://github.com/n-riesco/jp-kernel/blob/0bc2665470bfd2350ef8d0450b4a4c48f865904c/lib/handlers_v5.js#L340
// That's okay, I think we can implement it here just fine
global.In.push(val);
global._ih.push(val);
}
vmMonkeyPatch();
module.exports = {
vmMonkeyPatch,
magicInterpreter,
ijavascriptMonkeyPatch,
magicMap,
utils: utils,
};