mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Fix test_async and test_stdio_handling (#1319)
Resolves pyscript#1313 and pyscript#1314. On top of pyscript#1318. The point of these tests is to define the execution order of Tasks that are scheduled in <py-script> tags: first all the py-script tags are executed and their related lifecycle events. Once all of this is done, we schedule any enqueued tasks. To delay the execution of these tasks, we use a custom event loop for pyExec with this defer behavior. Until schedule_deferred_tasks is called, we defer tasks started by user code. schedule_deferred_tasks starts all deferred user tasks and switches to immediately scheduling any further user tasks.
This commit is contained in:
@@ -414,6 +414,7 @@ modules must contain a "plugin" attribute. For more information check the plugin
|
||||
this.incrementPendingTags();
|
||||
this.decrementPendingTags();
|
||||
await this.scriptTagsPromise;
|
||||
await this.interpreter._remote.pyscript_py._schedule_deferred_tasks();
|
||||
}
|
||||
|
||||
// ================= registraton API ====================
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import contextvars
|
||||
import html
|
||||
import io
|
||||
import re
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
import js
|
||||
from js import setTimeout
|
||||
from pyodide.webloop import WebLoop
|
||||
|
||||
try:
|
||||
from pyodide.code import eval_code
|
||||
from pyodide.ffi import JsProxy, create_proxy
|
||||
from pyodide.ffi import JsProxy, create_once_callable, create_proxy
|
||||
except ImportError:
|
||||
from pyodide import JsProxy, create_proxy, eval_code
|
||||
from pyodide import JsProxy, create_once_callable, create_proxy, eval_code
|
||||
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
@@ -709,6 +714,71 @@ def _install_deprecated_globals_2022_12_1(ns):
|
||||
ns["PyScript"] = DeprecatedGlobal("PyScript", PyScript, message)
|
||||
|
||||
|
||||
class _PyscriptWebLoop(WebLoop):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ready = False
|
||||
self._usercode = False
|
||||
self._deferred_handles = []
|
||||
|
||||
def call_later(
|
||||
self,
|
||||
delay: float,
|
||||
callback: Callable[..., Any],
|
||||
*args: Any,
|
||||
context: contextvars.Context | None = None,
|
||||
) -> asyncio.Handle:
|
||||
"""Based on call_later from Pyodide's webloop
|
||||
|
||||
With some unneeded stuff removed and a mechanism for deferring tasks
|
||||
scheduled from user code.
|
||||
"""
|
||||
if delay < 0:
|
||||
raise ValueError("Can't schedule in the past")
|
||||
h = asyncio.Handle(callback, args, self, context=context)
|
||||
|
||||
def run_handle():
|
||||
if h.cancelled():
|
||||
return
|
||||
h._run()
|
||||
|
||||
if self._ready or not self._usercode:
|
||||
setTimeout(create_once_callable(run_handle), delay * 1000)
|
||||
else:
|
||||
self._deferred_handles.append((run_handle, self.time() + delay))
|
||||
return h
|
||||
|
||||
def _schedule_deferred_tasks(self):
|
||||
asyncio._set_running_loop(self)
|
||||
t = self.time()
|
||||
for [run_handle, delay] in self._deferred_handles:
|
||||
delay = delay - t
|
||||
if delay < 0:
|
||||
delay = 0
|
||||
setTimeout(create_once_callable(run_handle), delay * 1000)
|
||||
self._ready = True
|
||||
self._deferred_handles = []
|
||||
|
||||
|
||||
def _install_pyscript_loop():
|
||||
global _LOOP
|
||||
_LOOP = _PyscriptWebLoop()
|
||||
asyncio.set_event_loop(_LOOP)
|
||||
|
||||
|
||||
def _schedule_deferred_tasks():
|
||||
_LOOP._schedule_deferred_tasks()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _defer_user_asyncio():
|
||||
_LOOP._usercode = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_LOOP._usercode = False
|
||||
|
||||
|
||||
def _run_pyscript(code: str, id: str = None) -> JsProxy:
|
||||
"""Execute user code inside context managers.
|
||||
|
||||
@@ -732,7 +802,7 @@ def _run_pyscript(code: str, id: str = None) -> JsProxy:
|
||||
"""
|
||||
import __main__
|
||||
|
||||
with _display_target(id):
|
||||
with _display_target(id), _defer_user_asyncio():
|
||||
result = eval_code(code, globals=__main__.__dict__)
|
||||
|
||||
return js.Object.new(result=result)
|
||||
|
||||
@@ -35,8 +35,9 @@ type PATHInterface = {
|
||||
type PyScriptPyModule = ProxyMarked & {
|
||||
_set_version_info(ver: string): void;
|
||||
uses_top_level_await(code: string): boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
_run_pyscript(code: string, display_target_id?: string): { result: any };
|
||||
_install_pyscript_loop(): void;
|
||||
_schedule_deferred_tasks(): void;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -128,6 +129,7 @@ export class RemoteInterpreter extends Object {
|
||||
this.globals = Synclink.proxy(this.interface.globals as PyProxyDict);
|
||||
logger.info('importing pyscript');
|
||||
this.pyscript_py = Synclink.proxy(this.interface.pyimport('pyscript')) as PyProxy & typeof this.pyscript_py;
|
||||
this.pyscript_py._install_pyscript_loop();
|
||||
|
||||
if (config.packages) {
|
||||
logger.info('Found packages in configuration to install. Loading micropip...');
|
||||
|
||||
@@ -192,16 +192,15 @@ class TestBasic(PyScriptTest):
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
def test_py_script_src_not_found(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script src="foo.py"></py-script>
|
||||
"""
|
||||
)
|
||||
with pytest.raises(JsErrors) as exc:
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script src="foo.py"></py-script>
|
||||
"""
|
||||
)
|
||||
assert self.PY_COMPLETE in self.console.log.lines
|
||||
|
||||
assert "Failed to load resource" in self.console.error.lines[0]
|
||||
with pytest.raises(JsErrors) as exc:
|
||||
self.check_js_errors()
|
||||
|
||||
error_msgs = str(exc.value)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
@@ -124,7 +122,6 @@ class TestAsync(PyScriptTest):
|
||||
inner_text = self.page.inner_text("html")
|
||||
assert "A0\nA1\nB0\nB1" in inner_text
|
||||
|
||||
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
|
||||
def test_async_display_untargeted(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
@@ -151,7 +148,6 @@ class TestAsync(PyScriptTest):
|
||||
== "Implicit target not allowed here. Please use display(..., target=...)"
|
||||
)
|
||||
|
||||
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
|
||||
def test_sync_and_async_order(self):
|
||||
"""
|
||||
The order of execution is defined as follows:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
@@ -100,7 +98,6 @@ class TestOutputHandling(PyScriptTest):
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
|
||||
def test_targeted_stdio_async(self):
|
||||
# Test the behavior of stdio capture in async contexts
|
||||
self.pyscript_run(
|
||||
@@ -149,7 +146,6 @@ class TestOutputHandling(PyScriptTest):
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
|
||||
def test_targeted_stdio_interleaved(self):
|
||||
# Test that synchronous writes to stdout are placed correctly, even
|
||||
# While interleaved with scheduling coroutines in the same tag
|
||||
|
||||
@@ -3,3 +3,4 @@ from unittest.mock import Mock
|
||||
|
||||
document = Mock()
|
||||
console = Mock()
|
||||
setTimeout = Mock()
|
||||
|
||||
Reference in New Issue
Block a user