Skip to content
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

Remove 'Implicit Async', Don't Await runtime.run() #928

Merged
merged 17 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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())
JeffersGlass marked this conversation as resolved.
Show resolved Hide resolved
</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);
JeffersGlass marked this conversation as resolved.
Show resolved Hide resolved
}

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)

JeffersGlass marked this conversation as resolved.
Show resolved Hide resolved

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