8000 Fix test_async and test_stdio_handling by hoodmane · Pull Request #1319 · pyscript/pyscript · GitHub
[go: up one dir, main page]

Skip to content

Fix test_async and test_stdio_handling #1319

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

Merged
merged 12 commits into from
Mar 30, 2023
Merged
1 change: 1 addition & 0 deletions pyscriptjs/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ====================
Expand Down
76 changes: 73 additions & 3 deletions pyscriptjs/src/python/pyscript/__init__.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -709,6 +714,71 @@ def deprecate(name, obj, instead):
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.

Expand All @@ -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)
4 changes: 3 additions & 1 deletion pyscriptjs/src/remote_interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/*
Expand Down Expand Up @@ -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...');
Expand Down
13 changes: 6 additions & 7 deletions pyscriptjs/tests/integration/test_01_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,15 @@ def test_py_script_src_attribute(self):
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)

Expand Down
4 changes: 0 additions & 4 deletions pyscriptjs/tests/integration/test_async.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import pytest

from .support import PyScriptTest


Expand Down Expand Up @@ -124,7 +122,6 @@ async def a_func():
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(
"""
Expand All @@ -151,7 +148,6 @@ async def a_func():
== "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:
Expand Down
4 changes: 0 additions & 4 deletions pyscriptjs/tests/integration/test_stdio_handling.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import pytest

from .support import PyScriptTest


Expand Down Expand Up @@ -100,7 +98,6 @@ def test_targeted_stdio_linebreaks(self):

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(
Expand Down Expand Up @@ -149,7 +146,6 @@ async def coro(value, delay):

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
Expand Down
1 change: 1 addition & 0 deletions pyscriptjs/tests/py-unit/js.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

document = Mock()
console = Mock()
setTimeout = Mock()
0