Export Runtime from PyScript Module (#868)

* export 'pyscript' JS module with runtime attribute, to allow accessing (Pyodide) runtime and globals from JS.
* add docs to explain the js module
* add integration tests for the js module

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jeff Glass
2022-10-23 13:46:19 -05:00
committed by GitHub
parent aa85f5f596
commit d9b8b48972
7 changed files with 157 additions and 6 deletions

View File

@@ -34,9 +34,46 @@ We can use the syntax `from js import ...` to import JavaScript objects directly
## PyScript to JavaScript
Since [PyScript doesn't export its instance of Pyodide](https://github.com/pyscript/pyscript/issues/494) and only one instance of Pyodide can be running in a browser window at a time, there isn't currently a way for Javascript to access Objects defined inside PyScript tags "directly".
### Using Pyodide's globals access
We can work around this limitation using [JavaScript's eval() function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval), which executes a string as code much like [Python's eval()](https://docs.python.org/3/library/functions.html#eval). First, we create a JS function `createObject` which takes an object and a string, then uses `eval()` to create a variable named after the string and bind it to that object. By calling this function from PyScript (where we have access to the Pyodide global namespace), we can bind JavaScript variables to Python objects without having direct access to that global namespace.
The [PyScript JavaScript module](../reference/pyscript-module.md) exposes its underlying Pyodide runtime as `PyScript.runtime`, and maintains a reference to the [globals()](https://docs.python.org/3/library/functions.html#globals) dictionary of the Python namespace. Thus, any global variables in python are accessible in JavaScript at `PyScript.runtime.globals.get('my_variable_name')`
```html
<body>
<py-script>x = 42</py-script>
<button onclick="showX()">Click Me to Get 'x' from Python</button>
<script>
function showX(){
console.log(`In Python right now, x = ${PyScript.globals.get('x')}`)
}
</script>
</body>
```
Since [everything is an object](https://docs.python.org/3/reference/datamodel.html) in Python, this applies not only to user created variables, but also to classes, functions, built-ins, etc. If we want, we can even apply Python functions to JavaScript data and variables:
```html
<body>
<!-- Click this button to log 'Apple', 'Banana', 'Candy', 'Donut' by sorting in Python-->
<button onclick="sortInPython(['Candy', 'Donut', 'Apple', 'Banana'])">Sort In Python And Log</button>
<script>
function sortInPython(data){
js_sorted = PyScript.runtime.globals.get('sorted') //grab python's 'sorted' function
const sorted_data = js_sorted(data) //apply the function to the 'data' argument
for (const item of sorted_data){
console.log(item)
}
}
</script>
</body>
```
### Using JavaScript's eval()
There may be some situations where it isn't possible or ideal to use `PyScript.runtime.globals.get()` to retrieve a variable from the Pyodide global dictionary. For example, some JavaScript frameworks may take a function/Callable as an html attribute in a context where code execution isn't allowed (i.e. `get()` fails). In these cases, you can create JavaScript proxies of Python objects more or less "manually" using [JavaScript's eval() function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval), which executes a string as code much like [Python's eval()](https://docs.python.org/3/library/functions.html#eval).
First, we create a JS function `createObject` which takes an object and a string, then uses `eval()` to create a variable named after the string and bind it to that object. By calling this function from PyScript (where we have access to the Pyodide global namespace), we can bind JavaScript variables to Python objects without having direct access to that global namespace.
Include the following script tag anywhere in your html document:

View File

@@ -34,6 +34,8 @@ You already know the basics and want to learn specifics!
[Frequently asked questions](reference/faq.md)
[The PyScript JS Module](reference/pyscript-module.md)
:::{toctree}
:maxdepth: 1

View File

@@ -9,3 +9,4 @@ maxdepth: 1
glob:
---
faq
pyscript-module

View File

@@ -0,0 +1,66 @@
# The PyScript Module
The code underlying PyScript is a TypeScript/JavaScript module, which is loaded and executed by the browser. This is what loads when you include, for example, `<script defer src="https://pyscript.net/latest/pyscript.js">` in your HTML.
The module is exported to the browser as `pyscript`. The exports from this module are:
## pyscript.runtime
The RunTime object which is responsible for executing Python code in the Browser. Currently, all runtimes are assumed to be Pyodide runtimes, but there is flexibility to expand this to other web-based Python runtimes in future versions.
The RunTime object has the following attributes
| attribute | type | description |
|---------------------|---------------------|-----------------------------------------------------------------------------|
| **src** | string | The URL from which the current runtime was fetched |
| **interpreter** | RuntimeInterpretter | A reference to the runtime object itself |
| **globals** | any | The globals dictionary of the runtime, if applicable/accessible |
| **name (optional)** | string | A user-designated name for the runtime |
| **lang (optional)** | string | A user-designation for the language the runtime runs ('Python', 'C++', etc) |
### pyscript.runtime.src
The URL from which the current runtime was fetched.
### pyscript.runtime.interpreter
A reference to the Runtime wrapper that PyScript uses to execute code. object itself. This allows other frameworks, modules etc to interact with the same [(Pyodide) runtime instance](https://pyodide.org/en/stable/usage/api/js-api.html) that PyScript uses.
For example, assuming we've loaded Pyodide, we can access the methods of the Pyodide runtime as follows:
```html
<button onclick="logFromPython()">Click Me to Run Some Python</button>
<script>
function logFromPython(){
pyscript.runtime.interpreter.runPython(`
animal = "Python"
sound = "sss"
console.warn(f"{animal}s go " + sound * 5)
`)
}
</script>
```
### pyscript.runtime.globals
A proxy for the runtime's `globals()` dictionary. For example:
```html
<body>
<py-script>x = 42</py-script>
<button onclick="showX()">Click Me to Get 'x' from Python</button>
<script>
function showX(){
console.log(`In Python right now, x = ${PyScript.runtime.globals.get('x')}`)
}
</script>
</body>
```
### pyscript.runtime.name
A user-supplied string for the runtime given at its creation. For user reference only - does not affect the operation of the runtime or PyScript.
### PyScript.runtime.lang
A user-supplied string for the language the runtime uses given at its creation. For user reference only - does not affect the operation of the runtime or PyScript.

View File

@@ -27,7 +27,7 @@ export default {
sourcemap: true,
format: "iife",
inlineDynamicImports: true,
name: "app",
name: "pyscript",
file: "build/pyscript.js",
},
{

View File

@@ -57,6 +57,7 @@ class PyScriptApp {
config: AppConfig;
loader: PyLoader;
runtime: Runtime;
// lifecycle (1)
main() {
@@ -111,13 +112,13 @@ class PyScriptApp {
"Only the first will be used");
}
const runtime_cfg = this.config.runtimes[0];
const runtime: Runtime = new PyodideRuntime(this.config, runtime_cfg.src,
this.runtime = new PyodideRuntime(this.config, runtime_cfg.src,
runtime_cfg.name, runtime_cfg.lang);
this.loader.log(`Downloading ${runtime_cfg.name}...`);
const script = document.createElement('script'); // create a script DOM node
script.src = runtime.src;
script.src = this.runtime.src;
script.addEventListener('load', () => {
void this.afterRuntimeLoad(runtime);
void this.afterRuntimeLoad(this.runtime);
});
document.head.appendChild(script);
}
@@ -209,3 +210,5 @@ globalExport('pyscript_get_config', pyscript_get_config);
// main entry point of execution
const globalApp = new PyScriptApp();
globalApp.main();
export const runtime = globalApp.runtime

View File

@@ -0,0 +1,42 @@
from .support import PyScriptTest
class TestRuntimeAccess(PyScriptTest):
"""Test accessing Python objects from JS via pyscript.runtime"""
def test_runtime_python_access(self):
self.pyscript_run(
"""
<py-script>
x = 1
def py_func():
return 2
</py-script>
"""
)
self.page.add_script_tag(
content="""
console.log(`x is ${pyscript.runtime.globals.get('x')}`);
console.log(`py_func() returns ${pyscript.runtime.globals.get('py_func')()}`);
"""
)
assert self.console.log.lines == [
self.PY_COMPLETE,
"x is 1",
"py_func() returns 2",
]
def test_runtime_script_execution(self):
"""Test running Python code from js via pyscript.runtime"""
self.pyscript_run("")
self.page.add_script_tag(
content="""
const interpreter = pyscript.runtime.interpreter;
interpreter.runPython('console.log("Interpreter Ran This")');
"""
)
assert self.console.log.lines == [self.PY_COMPLETE, "Interpreter Ran This"]