Commit de3aed63 authored by Romain Casati's avatar Romain Casati
Browse files

Implementing BasthonBase class as an API to support various kernels.

parent 0801f6c1
......@@ -6,7 +6,7 @@ BASTHON_JS = src/basthon-py/build/basthon.js
BASTHON_MODULE = src/basthon-py/basthon
BASTHON_VFS_DEST = /lib/python3.8/site-packages/basthon/
MINIFIED=src/basthon-js/basthon.min.js src/basthon-js/basthon_goodies.min.js
MINIFIED=src/basthon-js/basthon_base.min.js src/basthon-js/basthon_python.min.js src/basthon-js/basthon_goodies.min.js
SOURCEMAPS=$(patsubst %.min.js,%.min.js.map,$(MINIFIED))
minify: $(MINIFIED)
......
......@@ -73,7 +73,7 @@ class SeleniumWrapper:
if not (pathlib.Path(build_dir) / "test.html").exists():
# selenium does not expose HTTP response codes
raise ValueError(
f"{(build_dir / 'test.html').resolve()} " f"does not exist!"
f"{(build_dir / 'test.html').resolve()} does not exist!"
)
self.driver.implicitly_wait(30)
self.driver.get(f"http://{server_hostname}:{server_port}/test.html")
......@@ -122,12 +122,6 @@ class SeleniumWrapper:
def clean_logs(self):
self.driver.execute_script("window.logs = []")
def run(self, code):
return self.run_js("return pyodide.runPython({!r})".format(code))
def run_async(self, code):
return self.run_js("return pyodide.runPythonAsync({!r})".format(code))
def run_js(self, code):
if isinstance(code, str) and code.startswith("\n"):
# we have a multiline string, fix indentation
......@@ -135,7 +129,7 @@ class SeleniumWrapper:
wrapper = """
let cb = arguments[arguments.length - 1];
let run = async () => { %s }
let run = async () => { %s };
(async () => {
try {
let result = await run();
......@@ -153,7 +147,7 @@ class SeleniumWrapper:
} catch (e) {
cb([1, e.toString(), e.stack]);
}
})()
})();
"""
retval = self.driver.execute_async_script(wrapper % code)
......@@ -191,34 +185,6 @@ class SeleniumWrapper:
self.run_basthon_detach(code)
return self.run_basthon_reattach(return_data)
def run_webworker(self, code):
if isinstance(code, str) and code.startswith("\n"):
# we have a multiline string, fix indentation
code = textwrap.dedent(code)
return self.run_js(
"""
let worker = new Worker( '{}' );
worker.postMessage({{ python: {!r} }});
return new Promise((res, rej) => {{
worker.onerror = e => rej(e);
worker.onmessage = e => {{
if (e.data.results) {{
res(e.data.results);
}} else {{
rej(e.data.error);
}}
}};
}})
""".format(
f"http://{self.server_hostname}:{self.server_port}/webworker_dev.js",
code,
)
)
def load_package(self, packages):
self.run_js("await pyodide.loadPackage({!r})".format(packages))
@property
def urls(self):
for handle in self.driver.window_handles:
......@@ -301,7 +267,7 @@ if pytest is not None:
# selenium instance cached at the module level
try:
_selenium_cached.clean_logs()
_selenium_cached.run_js("Basthon.restart()")
_selenium_cached.run_js("Basthon.restart();")
yield _selenium_cached
finally:
print(_selenium_cached.logs)
......
Please, create here a link to ̀pyodide/emsdk/emsdk/fastcomp/emscripten/`.
Please, create here a link to `pyodide/emsdk/emsdk/fastcomp/emscripten/`.
......@@ -9,14 +9,14 @@ this_dir = Path.cwd()
root_dir = this_dir.parent
# get version number from basthon.js
basthonjs = root_dir / "src" / "basthon-js" / "basthon.js"
basthonjs = root_dir / "src" / "basthon-js" / "basthon_python.js"
# trick to get version at wheel build (build is done in /tmp/...)
if not basthonjs.exists():
basthonjs = this_dir / "basthon-kernel" / "data" / "basthon.js"
basthonjs = this_dir / "basthon-kernel" / "data" / "basthon_python.js"
# version number from basthon.js using regex
with open(basthonjs) as f:
BASTHON_KERNEL_VERSION = re.search(
"that.version = \"(?P<version>.*)\";", f.read()).group('version')
"__version__ = \"(?P<version>.*)\";", f.read()).group('version')
# make sure we have a version number
assert BASTHON_KERNEL_VERSION
......
(function (root, factory) {
// requirejs?
if (typeof define === 'function') {
define(['module'], factory);
// TypeScript?
} else if (typeof exports === 'object') {
module.exports = factory();
// default goes to global namespace
} else {
root.Basthon = factory();
}
}(this, function (module) {
'use strict';
let that = {};
/**
* Version number (entry point for setup.py and publication to PyPi).
*
* /!\ Warning: edit this line with care since it is parsed by
* setup.py to get version number.
*/
that.version = "0.15.8";
/**
* Where to find pyodide.js (private).
*/
that._pyodideUrl = "https://cdn.jsdelivr.net/pyodide/v0.16.1/full/pyodide.js";
/**
* Dirname remove basename/filename from url.
*/
that.dirname = function (url) {
return url.substring(0, url.lastIndexOf("/"));
};
/**
* Is Basthon loaded ?
*/
that.loaded = false;
/**
* Get the URL of the current script (usefull for serving basthon-py)
*/
that.urlScript = (function () {
if( module != null ) {
let url = window.location.origin + window.location.pathname;
url = url.substring(0, url.lastIndexOf('/')) + "/";
return url + module.uri;
} else
return document.currentScript.src;
})();
/**
* Get the URL of Basthon kernel root dir.
* Root path contains version number for cache busting.
*/
that.basthonRoot = that.dirname(that.urlScript) + "/" + that.version;
/**
* Get the URL of Basthon modules dir.
*/
that.basthonModulesRoot = that.basthonRoot + "/modules";
/**
* Downloading data (bytes array) as filename (opening browser dialog).
*/
that.download = function (data, filename) {
let blob = new Blob([data], { type: "application/octet-stream" });
let anchor = document.createElement("a");
anchor.download = filename;
anchor.href = window.URL.createObjectURL(blob);
anchor.target ="_blank";
anchor.style.display = "none"; // just to be safe!
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
};
/**
* Dynamically load a script asynchronously.
*/
that.loadScript = function (url) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.onload = resolve;
script.onerror = reject;
script.src = url;
document.head.appendChild(script);
});
};
/**
* A promise that resolve once the page is loaded.
*/
that.pageLoad = (function () {
return new Promise(function (resolve, reject) {
if( document.readyState === 'complete' ) {
resolve();
} else {
window.addEventListener("load", function() {
// just to be safe
window.removeEventListener("load", this);
resolve();
});
}
});
})();
/**
* What to do when loaded (private).
*/
that._onload = function() {
that.loaded = true;
// connecting eval to basthon.eval.request event.
that.addEventListener("eval.request", that.evalFromEvent);
// get the version of Python from Python
that.pythonVersion = pyodide.runPython("import platform ; platform.python_version()");
// this is for avoiding "Unknown package 'basthon'" error
// but can be removed with 0.17.0 since it is fixed upstream
const consoleErrorBck = console.error;
console.error = () => {};
return pyodide.loadPackage(that.basthonRoot + "/basthon-py/basthon.js").then(() => {
console.error = consoleErrorBck;
// importing basthon to get it's kernel
return pyodide.runPythonAsync("import basthon as __basthon__");
}).then(() => {
// kernel lookup
that.__kernel__ = pyodide.globals.__basthon__.__kernel__;
// removing basthon from global namespace
return pyodide.runPythonAsync("del __basthon__");
});
};
/**
* Wrapper around XHR through a promise.
*/
that.xhr = function (params) {
const xhr = new XMLHttpRequest();
xhr.open(params.method, params.url, true);
if( params.responseType != null )
xhr.responseType = params.responseType;
const promise = new Promise(function(resolve, reject) {
xhr.onload = function() {
if( xhr.status >= 200 && xhr.status < 300 ) {
resolve(xhr.response);
} else {
reject(xhr);
}
};
xhr.onerror = function () { reject(xhr); };
});
// headers
if (params.headers) {
Object.keys(params.headers).forEach(function (key) {
xhr.setRequestHeader(key, params.headers[key]);
});
}
// data
let data = params.data;
if (data && typeof data === 'object') {
data = JSON.stringify(data);
}
xhr.send(data);
return promise;
};
/**
* Start the Basthon kernel asynchronously.
*/
that.load = (async function () {
/* testing if Pyodide is installed locally */
try {
const url = that.basthonRoot + "/pyodide/pyodide.js";
await that.xhr({method: "HEAD", url: url});
that._pyodideUrl = url;
} catch (e) {}
// forcing Pyodide to look at the right location for other files
window.languagePluginUrl = that._pyodideUrl.substr(0, that._pyodideUrl.lastIndexOf('/')) + '/';
// avoid conflict with requirejs and use it when available.
try {
if( typeof requirejs !== 'undefined' ) {
requirejs.config({paths: {pyodide: that._pyodideUrl.slice(0, -3)}});
await new Promise(function (resolve, reject) {
require(['pyodide'], resolve, reject);
});
} else {
await that.loadScript(that._pyodideUrl);
}
} catch (error) {
console.log(error);
console.error("Can't load pyodide.js");
}
await languagePluginLoader.then(
that._onload,
function() { console.error("Can't load Python from Pyodide"); });
// waiting until page is loaded
await that.pageLoad;
})();
/**
* Ease the creation of events.
*/
that.dispatchEvent = function (eventName, data) {
const event = new CustomEvent("basthon." + eventName, { detail: data });
document.dispatchEvent(event);
};
/**
* Ease the event processing.
*/
that.addEventListener = function (eventName, callback) {
document.addEventListener(
"basthon." + eventName,
function (event) { callback(event.detail); });
};
/**
* Cloning function.
*/
that.clone = function (obj) {
// simple trick that is enough for our purpose.
return JSON.parse(JSON.stringify(obj));
};
/**
* Basthon async code evaluation function.
*/
that.evalAsync = async function (code, outCallback, errCallback,
loadPackageCallback, data=null) {
if( !that.loaded ) { return ; }
if( typeof outCallback === 'undefined' ) {
outCallback = function (text) { console.log(text); };
}
if( typeof errCallback === 'undefined' ) {
errCallback = function (text) { console.error(text); };
}
// loading dependencies are loaded by eval
return await that.__kernel__.eval(code, outCallback, errCallback, data);
};
/**
* Basthon evaluation function callback.
* It is not used directly but through basthon.eval.request event.
* A Python error throw basthon.eval.error event.
* Output on stdout/stderr throw basthon.eval.output.
* Matplotlib display throw basthon.eval.display event.
* When computation is finished, basthon.eval.finished is thrown.
*/
that.evalFromEvent = function (data) {
if( !that.loaded ) { return ; }
let stdCallback = function (std) { return function (text) {
let dataEvent = that.clone(data);
dataEvent.stream = std;
dataEvent.content = text;
that.dispatchEvent("eval.output", dataEvent);
}};
let outCallback = stdCallback("stdout");
let errCallback = stdCallback("stderr");
return that.evalAsync(data.code, outCallback, errCallback,
that.hackPackagesCallback, data)
.then(function (args) {
const result = args[0];
const executionCount = args[1];
let dataEvent = that.clone(data);
dataEvent.execution_count = executionCount;
if( typeof result !== 'undefined' ) {
dataEvent.result = result;
}
that.dispatchEvent("eval.finished", dataEvent);
}, function (error) {
errCallback(error.toString());
let dataEvent = that.clone(data);
dataEvent.error = error;
dataEvent.execution_count = that.__kernel__.execution_count();
that.dispatchEvent("eval.error", dataEvent);
});
};
/**
* Restart the kernel.
*/
that.restart = function () {
if( !that.loaded ) { return ; }
return that.__kernel__.restart();
};
/**
* Put a file on the local (emulated) filesystem.
*/
that.putFile = function (filename, content) {
that.__kernel__.put_file(filename, content);
};
/**
* Put an importable module on the local (emulated) filesystem
* and load dependencies.
*/
that.putModule = function (filename, content) {
return that.__kernel__.put_module(filename, content);
};
/**
* List modules launched via putModule.
*/
that.userModules = function () {
return that.__kernel__.user_modules();
};
/**
* Put a ressource (file or *.py module) on the local (emulated)
* filesystem. Detection is based on extension.
*/
that.putRessource = function (filename, content) {
const ext = filename.split('.').pop();
if( ext === 'py' ) {
return that.putModule(filename, content);
} else {
return that.putFile(filename, content);
}
};
/**
* Download a file from the VFS.
*/
that.getFile = function (path) {
return that.__kernel__.get_file(path);
};
/**
* Download a user module file.
*/
that.getUserModuleFile = function (filename) {
return that.__kernel__.get_user_module_file(filename);
};
/**
* Is the source ready to be evaluated or want we more?
* Usefull to set ps1/ps2 for teminal prompt.
*/
that.more = function (source) {
return that.__kernel__.more(source);
};
/**
* Mimic the CPython's REPL banner.
*/
that.banner = function () {
return that.__kernel__.banner();
};
/**
* Complete a code at the end (usefull for tab completion).
*
* Returns an array of two elements: the list of completions
* and the start index.
*/
that.complete = function (code) {
return that.__kernel__.complete(code);
}
return that;
}));
(function (root, factory) {
// requirejs?
if (typeof define === 'function') {
define(['module'], factory);
// TypeScript?
} else if (typeof exports === 'object') {
module.exports = factory();
// default goes to global namespace
} else {
root.BasthonBase = factory();
}
}(this, function (module) {
'use strict';
/**
* An error thrown by not implemented API functions.
*/
function NotImplementedError(func) {
this.message = `Function ${func} not implemented!`;
};
/**
* API that any Basthon kernel should fill to be supported
* in console/noetbook.
*/
return class {
constructor() {
/** Is the kernel loaded? */
this.loaded = false;
/** The current script URL. */
this. urlScript = (function () {
if( module != null ) {
let url = window.location.origin + window.location.pathname;
url = url.substring(0, url.lastIndexOf('/')) + "/";
return url + module.uri;
} else
return document.currentScript.src;
})();
/** Promise that resolve when the page is loaded. */
this.pageLoad = this._pageLoad();
/** Promie that resolve when the kernel is loaded. */
this.load = this.launch().then( () => {
this.loaded = true;
// connecting eval to basthon.eval.request event.
this.addEventListener("eval.request",
this.evalFromEvent.bind(this));
});
}
/**
* Kernel version number (string).
*/
version () { throw new NotImplementedError("version"); };
/**
*
*/
async launch () { throw new NotImplementedError("launch"); };
/**
* Async code evaluation that resolves with the result.
*/
evalAsync (code, outCallback, errCallback, data=null) {
throw new NotImplementedError("evalAsync");
};
restart () { throw new NotImplementedError("restart"); };
putFile (filename, content) {
throw new NotImplementedError("putFile");
};
putModule (filename, content) {
throw new NotImplementedError("putModule");
};
putRessource (filename, content) {
throw new NotImplementedError("putRessource");
};
userModules () { throw new NotImplementedError("userModules"); };
getFile (path) { throw new NotImplementedError("getFile"); };
getUserModuleFile (filename) {
throw new NotImplementedError("getUserModuleFile");
};
more (source) { throw new NotImplementedError("more"); };
banner () { throw new NotImplementedError("banner"); };
complete (code) { return []; };
/**
* An URL without it's basename.
*/
dirname (url) {
return url.substring(0, url.lastIndexOf("/"));
};
/**
* Root for kernel files. This is always the versnion number
* directory inside the current script directory.
*/
basthonRoot () {
return this.dirname(this.urlScript) + "/" + this.version();
};
/**
* Downloading data (bytes array) as filename
* (opening browser dialog).
*/
download (data, filename) {
let blob = new Blob([data], { type: "application/octet-stream" });
let anchor = document.createElement("a");
anchor.download = filename;
anchor.href = window.URL.createObjectURL(blob);
anchor.target ="_blank";
anchor.style.display = "none"; // just to be safe!
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
};