8000 Fix test_async and test_stdio_handling (#1319) · patrickloeber/pyscript@e9122bc · GitHub
[go: up one dir, main page]

Skip to content

Commit e9122bc

Browse files
authored
Fix test_async and test_stdio_handling (pyscript#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.
1 parent b61e843 commit e9122bc

File tree

7 files changed

+84
-19
lines changed

7 files changed

+84
-19
lines changed

pyscriptjs/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ modules must contain a "plugin" attribute. For more information check the plugin
414414
this.incrementPendingTags();
415415
this.decrementPendingTags();
416416
await this.scriptTagsPromise;
417+
await this.interpreter._remote.pyscript_py._schedule_deferred_tasks();
417418
}
418419

419420
// ================= registraton API ====================

pyscriptjs/src/python/pyscript/__init__.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import ast
22
import asyncio
33
import base64
4+
import contextvars
45
import html
56
import io
67
import re
78
import time
89
from collections import namedtuple
10+
from collections.abc import Callable
911
from contextlib import contextmanager
1012
from textwrap import dedent
13+
from typing import Any
1114

1215
import js
16+
from js import setTimeout
17+
from pyodide.webloop import WebLoop
1318

1419
try:
1520
from pyodide.code import eval_code
16-
from pyodide.ffi import JsProxy, create_proxy
21+
from pyodide.ffi import JsProxy, create_once_callable, create_proxy
1722
except ImportError:
18-
from pyodide import JsProxy, create_proxy, eval_code
23+
from pyodide import JsProxy, create_once_callable, create_proxy, eval_code
1924

2025

2126
loop = asyncio.get_event_loop()
@@ -709,6 +714,71 @@ def deprecate(name, obj, instead):
709714
ns["PyScript"] = DeprecatedGlobal("PyScript", PyScript, message)
710715

711716

717+
class _PyscriptWebLoop(WebLoop):
718+
def __init__(self):
719+
super().__init__()
720+
self._ready = False
721+
self._usercode = False
722+
self._deferred_handles = []
723+
724+
def call_later(
725+
self,
726+
delay: float,
727+
callback: Callable[..., Any],
728+
*args: Any,
729+
context: contextvars.Context | None = None,
730+
) -> asyncio.Handle:
731+
"""Based on call_later from Pyodide's webloop
732+
733+
With some unneeded stuff removed and a mechanism for deferring tasks
734+
scheduled from user code.
735+
"""
736+
if delay < 0:
737+
raise ValueError("Can't schedule in the past")
738+
h = asyncio.Handle(callback, args, self, context=context)
739+
740+
def run_handle():
741+
if h.cancelled():
742+
return
743+
h._run()
744+
745+
if self._ready or not self._usercode:
746+
setTimeout(create_once_callable(run_handle), delay * 1000)
747+
else:
748+
self._deferred_handles.append((run_handle, self.time() + delay))
749+
return h
750+
751+
def _schedule_deferred_tasks(self):
752+
asyncio._set_running_loop(self)
753+
t = self.time()
754+
for [run_handle, delay] in self._deferred_handles:
755+
delay = delay - t
756+
if delay < 0:
757+
delay = 0
758+
setTimeout(create_once_callable(run_handle), delay * 1000)
759+
self._ready = True
760+
self._deferred_handles = []
761+
762+
763+
def _install_pyscript_loop():
764+
global _LOOP
765+
_LOOP = _PyscriptWebLoop()
766+
asyncio.set_event_loop(_LOOP)
767+
768+
769+
def _schedule_deferred_tasks():
770+
_LOOP._schedule_deferred_tasks()
771+
772+
773+
@contextmanager
774+
def _defer_user_asyncio():
775+
_LOOP._usercode = True
776+
try:
777+
yield
778+
finally:
779+
_LOOP._usercode = False
780+
781+
712782
def _run_pyscript(code: str, id: str = None) -> JsProxy:
713783
"""Execute user code inside context managers.
714784
@@ -732,7 +802,7 @@ def _run_pyscript(code: str, id: str = None) -> JsProxy:
732802
"""
733803
import __main__
734804

735-
with _display_target(id):
805+
with _display_target(id), _defer_user_asyncio():
736806
result = eval_code(code, globals=__main__.__dict__)
737807

738808
return js.Object.new(result=result)

pyscriptjs/src/remote_interpreter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ type PATHInterface = {
3535
type PyScriptPyModule = ProxyMarked & {
3636
_set_version_info(ver: string): void;
3737
uses_top_level_await(code: string): boolean;
38-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3938
_run_pyscript(code: string, display_target_id?: string): { result: any };
39+
_install_pyscript_loop(): void;
40+
_schedule_deferred_tasks(): void;
4041
};
4142

4243
/*
@@ -128,6 +129,7 @@ export class RemoteInterpreter extends Object {
128129
this.globals = Synclink.proxy(this.interface.globals as PyProxyDict);
129130
logger.info('importing pyscript');
130131
this.pyscript_py = Synclink.proxy(this.interface.pyimport('pyscript')) as PyProxy & typeof this.pyscript_py;
132+
this.pyscript_py._install_pyscript_loop();
131133

132134
if (config.packages) {
133135
logger.info('Found packages in configuration to install. Loading micropip...');

pyscriptjs/tests/integration/test_01_basic.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,15 @@ def test_py_script_src_attribute(self):
192192
assert self.console.log.lines[-1] == "hello from foo"
193193

194194
def test_py_script_src_not_found(self):
195-
self.pyscript_run(
196-
"""
197-
<py-script src="foo.py"></py-script>
198-
"""
199-
)
195+
with pytest.raises(JsErrors) as exc:
196+
self.pyscript_run(
197+
"""
198+
<py-script src="foo.py"></py-script>
199+
"""
200+
)
200201
assert self.PY_COMPLETE in self.console.log.lines
201202

202203
assert "Failed to load resource" in self.console.error.lines[0]
203-
with pytest.raises(JsErrors) as exc:
204-
self.check_js_errors()
205204

206205
error_msgs = str(exc.value)
207206

pyscriptjs/tests/integration/test_async.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import pytest
2-
31
from .support import PyScriptTest
42

53

@@ -124,7 +122,6 @@ async def a_func():
124122
inner_text = self.page.inner_text("html")
125123
assert "A0\nA1\nB0\nB1" in inner_text
126124

127-
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
128125
def test_async_display_untargeted(self):
129126
self.pyscript_run(
130127
"""
@@ -151,7 +148,6 @@ async def a_func():
151148
== "Implicit target not allowed here. Please use display(..., target=...)"
152149
)
153150

154-
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
155151
def test_sync_and_async_order(self):
156152
"""
157153
The order of execution is defined as follows:

pyscriptjs/tests/integration/test_stdio_handling.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import pytest
2-
31
from .support import PyScriptTest
42

53

@@ -100,7 +98,6 @@ def test_targeted_stdio_linebreaks(self):
10098

10199
self.assert_no_banners()
102100

103-
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
104101
def test_targeted_stdio_async(self):
105102
# Test the behavior of stdio capture in async contexts
106103
self.pyscript_run(
@@ -149,7 +146,6 @@ async def coro(value, delay):
149146

150147
self.assert_no_banners()
151148

152-
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
153149
def test_targeted_stdio_interleaved(self):
154150
# Test that synchronous writes to stdout are placed correctly, even
155151
# While interleaved with scheduling coroutines in the same tag

pyscriptjs/tests/py-unit/js.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33

44
document = Mock()
55
console = Mock()
6+
setTimeout = Mock()

0 commit comments

Comments
 (0)
0