Skip to content

WIP original Brython implementation (with DOM access) #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Client-side Python runner

Supported python runners so far: [Pyodide][pyodide], [Skulpt][skulpt], [Brython][brython]
Supported python runners so far: [Pyodide][pyodide], [Skulpt][skulpt], [Brython][brython], [brythonWebWorker (BrythonRunner)][brython-runner]

Try it out [here](https://niklasmh.github.io/client-side-python-runner/).

Expand Down Expand Up @@ -368,6 +368,8 @@ _DISCLAIMER: The numbers in the table are not scientifically calculated, thus th
[skulpt-t]: https://skulpt.org/
[brython]: https://brython.info/
[brython-t]: https://brython.info/tests/editor.html
[brython-runner]: https://github.com/pythonpad/brython-runner
[brython-runner-t]: https://pythonpad.github.io/brython-runner/
[rustpython]: https://brython.info/
[rustpython-t]: https://rustpython.github.io/demo/
[pypyjs]: https://github.com/pypyjs/pypyjs
Expand Down
34 changes: 31 additions & 3 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
title: 'Loading example',
jsCode: `// This is not necessary unless you want to
// load the engines separately from the execution
await loadEngines(['skulpt', 'pyodide', 'brython'])
await loadEngines(['skulpt', 'pyodide', 'brython', 'brythonWebWorker'])

// Use setOptions to set loading hooks (do this before the line above)
setOptions({
Expand All @@ -107,7 +107,12 @@
engines: ['none'],
showDebug: true,
run: async (input) => {
await loadEngines(['skulpt', 'pyodide', 'brython']);
await loadEngines([
'skulpt',
'pyodide',
'brython',
'brythonWebWorker',
]);
},
},
{
Expand Down Expand Up @@ -488,6 +493,27 @@ <h1 className="text-2xl text-bold my-4" {...props}>
});
}}
/>
<Button
label="Run using Brython (WebWorker)"
onClick={async () => {
setInput(`await setEngine('brythonWebWorker');
await runCode(\`${code}\`);`);
setOutput('');
setError('');
await setOptions({
output: (arg) => setOutput((o) => [...o, arg]),
error: (arg) => {
console.error(arg);
setError(arg);
},
});
await setEngine('brythonWebWorker');
await runCode(code, {
loadVariablesBeforeRun,
storeVariablesAfterRun,
});
}}
/>
{input && (
<Button
color="bg-red-600"
Expand Down Expand Up @@ -516,7 +542,9 @@ <h1 className="text-2xl text-bold my-4" {...props}>
}) {
const [output, setOutput] = React.useState('');
const [error, setError] = React.useState('');
const buttons = engines ? engines : ['skulpt', 'pyodide', 'brython'];
const buttons = engines
? engines
: ['skulpt', 'pyodide', 'brython', 'brythonWebWorker'];
const buttonNames = {
skulpt: 'Skulpt',
pyodide: 'Pyodide',
Expand Down
171 changes: 167 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @typedef {"skulpt" | "pyodide" | "brython"} Engine
* @typedef {"skulpt" | "pyodide" | "brython" | "brythonWebWorker"} Engine
*/
/** @type {Engine} */
const defaultPythonEngine = 'pyodide';
Expand All @@ -14,6 +14,12 @@ const engines = {
library: 'https://cdn.jsdelivr.net/npm/skulpt@latest/dist/skulpt-stdlib.js',
},
brython: {
loader:
'https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython.min.js',
library:
'https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython_stdlib.js',
},
brythonWebWorker: {
loader:
'https://cdn.jsdelivr.net/npm/brython-runner/lib/brython-runner.bundle.js',
},
Expand Down Expand Up @@ -300,7 +306,7 @@ export function interpretErrorMessage(error, code, engine) {
// As we now have the line number we can get the line too
line = codeLines[lineNumber - 1];
} catch (ex) {}
} else if (engine === 'brython') {
} else if (engine === 'brython' || engine === 'brythonWebWorker') {
// Get error type
const trimmed = error.trim();
const lines = trimmed.split('\n');
Expand Down Expand Up @@ -405,13 +411,26 @@ export async function loadEngine(
}

case 'brython': {
const scriptWasLoaded = await loadScript(engines.brython.loader);
if (!scriptWasLoaded)
const script1WasLoaded = await loadScript(engines.brython.loader);
const script2WasLoaded = await loadScript(engines.brython.library);
if (!script1WasLoaded)
throw new Error('Could not reach "' + engines.brython.loader + '"');
if (!script2WasLoaded)
throw new Error('Could not reach "' + engines.brython.library + '"');
await createBrythonRunner();
break;
}

case 'brythonWebWorker': {
const scriptWasLoaded = await loadScript(engines.brythonWebWorker.loader);
if (!scriptWasLoaded)
throw new Error(
'Could not reach "' + engines.brythonWebWorker.loader + '"'
);
await createBrythonWebWorkerRunner();
break;
}

default:
if (pythonRunner.debug) log('Could not find ' + engine);
throw new Error('Could not load "' + engine + '" as it did not exist');
Expand Down Expand Up @@ -812,6 +831,150 @@ async function createSkulptRunner() {

async function createBrythonRunner() {
const engine = 'brython';
pythonRunner.loadedEngines[engine] = {
engine,
runnerReference: window.__BRYTHON__,
predefinedVariables: [
'__class__',
'__doc__',
'__file__',
'__name__',
'__package__',
'__annotations__',
'__BRYTHON__',
],
variables: {},
runCode: async (code, options = {}) => {
const {
loadVariablesBeforeRun = pythonRunner.options.loadVariablesBeforeRun,
storeVariablesAfterRun = pythonRunner.options.storeVariablesAfterRun,
variables = null,
} = options;

const prependedCode = [];

if (loadVariablesBeforeRun) {
Object.entries(pythonRunner.loadedEngines[engine].variables).forEach(
([name, value]) => {
if (['number', 'string'].includes(typeof value))
prependedCode.push(name + '=' + value);
}
);
} else {
await pythonRunner.loadedEngines[engine].clearVariables();
}

// Add new variables
if (variables) {
Object.entries(variables).forEach(([name, value]) => {
try {
if (['number', 'string'].includes(typeof value))
prependedCode.push(name + '=' + value);
} catch (ex) {}
});
}

pythonRunner.loadedEngines[engine].currentCode = code;
window.brythonVariables = {};
if (storeVariablesAfterRun) {
try {
window.nice = 'cool';
const composedCode =
prependedCode.join(';') +
`import browser
class Out:
def write(self, text):
browser.window.nice += text
def flush(self):
pass
import sys # This fails
sys.stdout = Out()
sys.stderr = Out()
`;
code + '\n#browser.window.brythonVariables = globals()\n';
console.log(composedCode);
const js = window.__BRYTHON__.python_to_js(composedCode);
console.log(js);
window.eval(js);
pythonRunner.loadedEngines[engine].variables = {
...pythonRunner.loadedEngines[engine].variables,
...Object.entries(window.brythonVariables)
.filter(
([name]) =>
!pythonRunner.loadedEngines[
engine
].predefinedVariables.includes(name) && name.charAt(0) !== '$'
)
.reduce(
(acc, [name, value]) => ({
...acc,
[name]: value,
}),
{}
),
};
} catch (ex) {
console.log(ex);
}
} else {
const js = window.__BRYTHON__.python_to_js(
prependedCode.join(';') + '\n' + code + '\n'
);
window.eval(js);
}
},

getVariable: async (name) =>
pythonRunner.loadedEngines[engine].variables[name],

getVariables: async (
includeValues = true,
filter = null,
onlyShowNewVariables = true
) => {
if (includeValues) {
if (filter) {
return Object.keys(pythonRunner.loadedEngines[engine].variables)
.filter(filter)
.reduce((acc, name) => {
return {
...acc,
[name]: pythonRunner.loadedEngines[engine].variables[name],
};
}, {});
}
return pythonRunner.loadedEngines[engine].variables;
}
if (filter) {
return Object.keys(pythonRunner.loadedEngines[engine].variables).filter(
filter
);
}
return Object.keys(pythonRunner.loadedEngines[engine].variables);
},

setVariable: async (name, value) => {
pythonRunner.loadedEngines[engine].variables[name] = value;
},

setVariables: async (variables) => {
Object.entries(variables).forEach(([name, value]) => {
pythonRunner.loadedEngines[engine].variables[name] = value;
});
},

clearVariable: async (name) => {
delete pythonRunner.loadedEngines[engine].variables[name];
},

clearVariables: async () => {
pythonRunner.loadedEngines[engine].variables = {};
},
};
}

async function createBrythonWebWorkerRunner() {
const engine = 'brythonWebWorker';
await new Promise((resolve) => {
const runner = new BrythonRunner({
onInit: () => {
Expand Down