Skip to content

Commit

Permalink
Remove 'Implicit Async', Don't Await runtime.run() (#928)
Browse files Browse the repository at this point in the history
* Revert to runPython instead of await runPythonAsync

* "Implicit Coroutines" are no longer permitted in py-script tags

* Tests added for the above

* xfail test_importmap (See #938)
  • Loading branch information
JeffersGlass committed Nov 16, 2022
1 parent 41ebaaf commit 0b23310
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 77 deletions.
35 changes: 35 additions & 0 deletions docs/guides/asyncio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Using Async/Await and Asyncio

## {bdg-warning-line}`Deprecated` Implicit Coroutine Scheduling / Top-Level Await

In PyScript versions 2022.09.1 and earlier, \<py-script\> tags could be written in a way that enabled "Implicit Coroutine Scheduling." The keywords `await`, `async for` and `await with` were permitted to be used outside of `async` functions. Any \<py-script\> tags with these keywords at the top level were compiled into coroutines and automatically scheuled to run in the browser's event loop. This functionality was deprecated, and these keywords are no longer allowed outside of `async` functions.

To transition code from using top-level await statements to the currently-acceptable syntax, wrap the code into a coroutine using `async def()` and schedule it to run in the browser's event looping using `asyncio.ensure_future()` or `asyncio.create_task()`.

The following two pieces of code are functionally equivalent - the first only works in versions 2022.09.1, the latter is the currently acceptable equivalent.

```python
# This version is deprecated, since
# it uses 'await' outside an async function
<py-script>
import asyncio

for i in range(3):
print(i)
await asyncio.sleep(1)
</py-script>
```

```python
# This version is acceptable
<py-script>
import asyncio

async def main():
for i in range(3):
print(i)
await asyncio.sleep(1)

asyncio.ensure_future(main())
</py-script>
```
1 change: 1 addition & 0 deletions docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ caption: 'Contents:'
---
passing-objects
http-requests
asyncio
```
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ Check out our [getting started guide](tutorials/getting-started.md)!
You already know the basics and want to learn specifics!

[Passing Objects between JavaScript and Python](guides/passing-objects.md)

[Making async HTTP requests in pure Python](guides/http-requests.md)

[Async/Await and Asyncio](guides/asyncio.md)

:::
:::{grid-item-card} [Concepts](concepts/index.md)

Expand Down
2 changes: 1 addition & 1 deletion examples/bokeh_interactive.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ <h1>Bokeh Example</h1>
jsdoc = views[0].model.document
_link_docs(pydoc, jsdoc)

await show(row, 'myplot')
asyncio.ensure_future(show(row, 'myplot'))
</py-script>

</body>
Expand Down
13 changes: 8 additions & 5 deletions examples/numpy_canvas_fractals.html
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,14 @@

import asyncio

_ = await asyncio.gather(
draw_mandelbrot(),
draw_julia(),
draw_newton(),
)
async def main():
_ = await asyncio.gather(
draw_mandelbrot(),
draw_julia(),
draw_newton(),
)

asyncio.ensure_future(main())
</py-script>

</body>
Expand Down
62 changes: 32 additions & 30 deletions examples/webgl/raycaster/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,36 +158,38 @@
time = 0.0003;
camera.lookAt(scene.position)

while True:
time = performance.now() * 0.0003;
i = 0
while i < particularGroup.children.length:
newObject = particularGroup.children[i];
newObject.rotation.x += newObject.speedValue/10;
newObject.rotation.y += newObject.speedValue/10;
newObject.rotation.z += newObject.speedValue/10;
i += 1

i = 0
while i < modularGroup.children.length:
newCubes = modularGroup.children[i];
newCubes.rotation.x += 0.008;
newCubes.rotation.y += 0.005;
newCubes.rotation.z += 0.003;

newCubes.position.x = Math.sin(time * newCubes.positionZ) * newCubes.positionY;
newCubes.position.y = Math.cos(time * newCubes.positionX) * newCubes.positionZ;
newCubes.position.z = Math.sin(time * newCubes.positionY) * newCubes.positionX;
i += 1

particularGroup.rotation.y += 0.005;

modularGroup.rotation.y -= ((mouse.x * 4) + modularGroup.rotation.y) * uSpeed;
modularGroup.rotation.x -= ((-mouse.y * 4) + modularGroup.rotation.x) * uSpeed;

renderer.render( scene, camera )
await asyncio.sleep(0.02)

async def main():
while True:
time = performance.now() * 0.0003;
i = 0
while i < particularGroup.children.length:
newObject = particularGroup.children[i];
newObject.rotation.x += newObject.speedValue/10;
newObject.rotation.y += newObject.speedValue/10;
newObject.rotation.z += newObject.speedValue/10;
i += 1

i = 0
while i < modularGroup.children.length:
newCubes = modularGroup.children[i];
newCubes.rotation.x += 0.008;
newCubes.rotation.y += 0.005;
newCubes.rotation.z += 0.003;

newCubes.position.x = Math.sin(time * newCubes.positionZ) * newCubes.positionY;
newCubes.position.y = Math.cos(time * newCubes.positionX) * newCubes.positionZ;
newCubes.position.z = Math.sin(time * newCubes.positionY) * newCubes.positionX;
i += 1

particularGroup.rotation.y += 0.005;

modularGroup.rotation.y -= ((mouse.x * 4) + modularGroup.rotation.y) * uSpeed;
modularGroup.rotation.x -= ((-mouse.y * 4) + modularGroup.rotation.x) * uSpeed;

renderer.render( scene, camera )
await asyncio.sleep(0.02)

asyncio.ensure_future(main())

</py-script>
</body>
Expand Down
4 changes: 2 additions & 2 deletions pyscriptjs/src/components/pyrepl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function make_PyRepl(runtime: Runtime) {
/** Execute the python code written in the editor, and automatically
* display() the last evaluated expression
*/
async execute(): Promise<void> {
execute(): void {
const pySrc = this.getPySrc();

// determine the output element
Expand All @@ -166,7 +166,7 @@ export function make_PyRepl(runtime: Runtime) {
outEl.innerHTML = '';

// execute the python code
const pyResult = await pyExec(runtime, pySrc, outEl);
const pyResult = pyExec(runtime, pySrc, outEl);

// display the value of the last evaluated expression (REPL-style)
if (pyResult !== undefined) {
Expand Down
2 changes: 1 addition & 1 deletion pyscriptjs/src/components/pyscript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function make_PyScript(runtime: Runtime) {
ensureUniqueId(this);
const pySrc = await this.getPySrc();
this.innerHTML = '';
await pyExec(runtime, pySrc, this);
pyExec(runtime, pySrc, this);
}

async getPySrc(): Promise<string> {
Expand Down
20 changes: 16 additions & 4 deletions pyscriptjs/src/pyexec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { getLogger } from './logger';
import { ensureUniqueId } from './utils';
import { ensureUniqueId, ltrim } from './utils';
import { UserError } from './exceptions';
import type { Runtime } from './runtime';

const logger = getLogger('pyexec');

export async function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
// this is the python function defined in pyscript.py
const set_current_display_target = runtime.globals.get('set_current_display_target');
ensureUniqueId(outElem);
set_current_display_target(outElem.id);
//This is the python function defined in pyscript.py
const usesTopLevelAwait = runtime.globals.get('uses_top_level_await')
try {
try {
return await runtime.run(pysrc);
} catch (err) {
if (usesTopLevelAwait(pysrc)){
throw new UserError(
'The use of top-level "await", "async for", and ' +
'"async with" is deprecated.' +
'\nPlease write a coroutine containing ' +
'your code and schedule it using asyncio.ensure_future() or similar.' +
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.'
)
}
return runtime.run(pysrc);
} catch (err) {
// XXX: currently we display exceptions in the same position as
// the output. But we probably need a better way to do that,
// e.g. allowing plugins to intercept exceptions and display them
Expand Down
4 changes: 2 additions & 2 deletions pyscriptjs/src/pyodide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ export class PyodideRuntime extends Runtime {
logger.info('pyodide loaded and initialized');
}

async run(code: string): Promise<any> {
return await this.interpreter.runPythonAsync(code);
run(code: string) {
return this.interpreter.runPython(code);
}

registerJsModule(name: string, module: object): void {
Expand Down
25 changes: 25 additions & 0 deletions pyscriptjs/src/python/pyscript.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ast
import asyncio
import base64
import html
Expand Down Expand Up @@ -404,4 +405,28 @@ def child_appended(self, child):
pass


class TopLevelAsyncFinder(ast.NodeVisitor):
def is_source_top_level_await(self, source):
self.async_found = False
node = ast.parse(source)
self.generic_visit(node)
return self.async_found

def visit_Await(self, node):
self.async_found = True

def visit_AsyncFor(self, node):
self.async_found = True

def visit_AsyncWith(self, node):
self.async_found = True

def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
pass # Do not visit children of async function defs


def uses_top_level_await(source: str) -> bool:
return TopLevelAsyncFinder().is_source_top_level_await(source)


pyscript = PyScript()
17 changes: 11 additions & 6 deletions pyscriptjs/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export abstract class Runtime extends Object {
* (asynchronously) which can call its own API behind the scenes.
* Python exceptions are turned into JS exceptions.
* */
abstract run(code: string): Promise<unknown>;
abstract run(code: string);

/**
* Same as run, but Python exceptions are not propagated: instead, they
Expand All @@ -64,11 +64,16 @@ export abstract class Runtime extends Object {
* This is a bad API and should be killed/refactored/changed eventually,
* but for now we have code which relies on it.
* */
async runButDontRaise(code: string): Promise<unknown> {
return this.run(code).catch(err => {
const error = err as Error;
logger.error('Error:', error);
});
runButDontRaise(code: string) {
let result
try{
result = this.run(code)
}
catch (err){
const error = err as Error
logger.error('Error:', error)
}
return result
}

/**
Expand Down
Loading

0 comments on commit 0b23310

Please sign in to comment.