Skip to content

Commit 8b97302

Browse files
committed
Simplify Promise handling
1 parent 6c551fb commit 8b97302

File tree

7 files changed

+168
-140
lines changed

7 files changed

+168
-140
lines changed

src/py_mini_racer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from py_mini_racer._context import PyJsFunctionType
43
from py_mini_racer._dll import (
54
DEFAULT_V8_FLAGS,
65
LibAlreadyInitializedError,
@@ -23,6 +22,7 @@
2322
JSSymbol,
2423
JSUndefined,
2524
JSUndefinedType,
25+
PyJsFunctionType,
2626
PythonJSConvertedTypes,
2727
)
2828
from py_mini_racer._value_handle import (

src/py_mini_racer/_context.py

Lines changed: 2 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
from __future__ import annotations
22

3-
from asyncio import Task, create_task, get_running_loop
4-
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator, Iterator
53
from concurrent.futures import Future as SyncFuture
64
from concurrent.futures import TimeoutError as SyncTimeoutError
7-
from contextlib import asynccontextmanager, contextmanager, suppress
8-
from dataclasses import dataclass, field
5+
from contextlib import contextmanager, suppress
96
from itertools import count
10-
from traceback import format_exc
117
from typing import TYPE_CHECKING, Any, cast
128

139
from py_mini_racer._abstract_context import AbstractContext
@@ -26,12 +22,11 @@
2622

2723
if TYPE_CHECKING:
2824
import ctypes
25+
from collections.abc import Callable, Generator, Iterator
2926

3027
from py_mini_racer._abstract_context import AbstractValueHandle
3128
from py_mini_racer._value_handle import RawValueHandleType
3229

33-
PyJsFunctionType = Callable[..., Awaitable[PythonJSConvertedTypes]]
34-
3530

3631
def context_count() -> int:
3732
"""For tests only: how many context handles are still allocated?"""
@@ -351,86 +346,3 @@ def close(self) -> None:
351346

352347
def __del__(self) -> None:
353348
self.close()
354-
355-
356-
@asynccontextmanager
357-
async def wrap_py_function_as_js_function(
358-
context: Context, func: PyJsFunctionType
359-
) -> AsyncGenerator[JSFunction, None]:
360-
js_to_py_callback_processor = _JsToPyCallbackProcessor(func, context)
361-
362-
loop = get_running_loop()
363-
364-
def on_called_from_js(value: PythonJSConvertedTypes | JSEvalException) -> None:
365-
loop.call_soon_threadsafe(
366-
js_to_py_callback_processor.process_one_invocation_from_js, value
367-
)
368-
369-
with context.js_to_py_callback(on_called_from_js) as js_to_py_callback:
370-
# Every time our callback is called from JS, on the JS side we
371-
# instantiate a JS Promise and immediately pass its resolution functions
372-
# into our Python callback function. While we wait on Python's asyncio
373-
# loop to process this call, we can return the Promise to the JS caller,
374-
# thus exposing what looks like an ordinary async function on the JS
375-
# side of things.
376-
wrap_outbound_calls_with_js_promises = cast(
377-
"JSFunction",
378-
context.evaluate(
379-
"""
380-
fn => {
381-
return (...arguments) => {
382-
let p = Promise.withResolvers();
383-
384-
fn(arguments, p.resolve, p.reject);
385-
386-
return p.promise;
387-
}
388-
}
389-
"""
390-
),
391-
)
392-
393-
yield cast(
394-
"JSFunction", wrap_outbound_calls_with_js_promises(js_to_py_callback)
395-
)
396-
397-
398-
@dataclass(frozen=True)
399-
class _JsToPyCallbackProcessor:
400-
"""Processes incoming calls from JS into Python.
401-
402-
Note that this is not thread-safe and is thus suitable for use with only one asyncio
403-
loop."""
404-
405-
_py_func: PyJsFunctionType
406-
_context: Context
407-
_ongoing_callbacks: set[Task[PythonJSConvertedTypes | JSEvalException]] = field(
408-
default_factory=set
409-
)
410-
411-
def process_one_invocation_from_js(
412-
self, params: PythonJSConvertedTypes | JSEvalException
413-
) -> None:
414-
arguments, resolve, reject = cast("JSArray", params)
415-
arguments = cast("JSArray", arguments)
416-
resolve = cast("JSFunction", resolve)
417-
reject = cast("JSFunction", reject)
418-
419-
async def await_into_js_promise_resolvers() -> None:
420-
try:
421-
result = await self._py_func(*arguments)
422-
resolve(result)
423-
except Exception: # noqa: BLE001
424-
# Convert this Python exception into a JS exception so we can send
425-
# it into JS:
426-
err_maker = cast(
427-
"JSFunction", self._context.evaluate("s => new Error(s)")
428-
)
429-
reject(err_maker(f"Error running Python function:\n{format_exc()}"))
430-
431-
# Start a new task to await this invocation:
432-
task = create_task(await_into_js_promise_resolvers())
433-
434-
self._ongoing_callbacks.add(task)
435-
436-
task.add_done_callback(self._ongoing_callbacks.discard)

src/py_mini_racer/_exc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class JSEvalException(MiniRacerBaseException):
1414
"""JavaScript could not be executed."""
1515

1616

17-
class JSTimeoutException(JSEvalException):
17+
class JSTimeoutException(JSEvalException, TimeoutError): # noqa: N818
1818
"""JavaScript execution timed out."""
1919

2020
def __init__(self) -> None:

src/py_mini_racer/_mini_racer.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
from json import JSONEncoder
55
from typing import TYPE_CHECKING, Any, ClassVar
66

7-
from py_mini_racer._context import Context, wrap_py_function_as_js_function
7+
from py_mini_racer._context import Context
88
from py_mini_racer._dll import init_mini_racer
99
from py_mini_racer._exc import MiniRacerBaseException
1010
from py_mini_racer._set_timeout import INSTALL_SET_TIMEOUT
11+
from py_mini_racer._wrap_py_function import wrap_py_function_as_js_function
1112

1213
if TYPE_CHECKING:
1314
from contextlib import AbstractAsyncContextManager
1415
from types import TracebackType
1516

1617
from typing_extensions import Self
1718

18-
from py_mini_racer._context import PyJsFunctionType
19-
from py_mini_racer._types import JSFunction, PythonJSConvertedTypes
19+
from py_mini_racer._types import (
20+
JSFunction,
21+
PyJsFunctionType,
22+
PythonJSConvertedTypes,
23+
)
2024

2125

2226
class WrongReturnTypeException(MiniRacerBaseException):

src/py_mini_racer/_objects.py

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from asyncio import get_running_loop
66
from concurrent.futures import Future as SyncFuture
7-
from contextlib import contextmanager
87
from operator import index as op_index
98
from typing import TYPE_CHECKING, Any, cast
109

@@ -20,10 +19,11 @@
2019
JSUndefinedType,
2120
PythonJSConvertedTypes,
2221
)
22+
from py_mini_racer._wrap_py_function import wrap_py_function_as_js_function
2323

2424
if TYPE_CHECKING:
2525
from asyncio import Future
26-
from collections.abc import Callable, Generator, Iterator
26+
from collections.abc import Generator, Iterator
2727

2828
from py_mini_racer._abstract_context import AbstractContext, AbstractValueHandle
2929
from py_mini_racer._exc import JSEvalException
@@ -171,56 +171,55 @@ def get(self, *, timeout: float | None = None) -> PythonJSConvertedTypes:
171171
This is deprecated; use timeout_sec instead.
172172
"""
173173

174-
future: SyncFuture[PythonJSConvertedTypes] = SyncFuture()
174+
future: SyncFuture[JSArray] = SyncFuture()
175+
is_rejected = False
175176

176-
def future_caller(value: PythonJSConvertedTypes) -> None:
177-
future.set_result(value)
177+
def on_resolved(value: PythonJSConvertedTypes | JSEvalException) -> None:
178+
future.set_result(cast("JSArray", value))
178179

179-
with self._attach_callbacks_to_promise(future_caller):
180-
return self._unpack_promise_results(future.result(timeout=timeout))
180+
def on_rejected(value: PythonJSConvertedTypes | JSEvalException) -> None:
181+
nonlocal is_rejected
182+
is_rejected = True
183+
future.set_result(cast("JSArray", value))
181184

182-
def __await__(self) -> Generator[Any, None, Any]:
183-
return self._do_await().__await__()
184-
185-
async def _do_await(self) -> PythonJSConvertedTypes:
186-
loop = get_running_loop()
187-
future: Future[PythonJSConvertedTypes] = loop.create_future()
188-
189-
def future_caller(value: PythonJSConvertedTypes) -> None:
190-
loop.call_soon_threadsafe(future.set_result, value)
185+
with (
186+
self._ctx.js_to_py_callback(on_resolved) as on_resolved_js_func,
187+
self._ctx.js_to_py_callback(on_rejected) as on_rejected_js_func,
188+
):
189+
self._ctx.promise_then(self, on_resolved_js_func, on_rejected_js_func)
191190

192-
with self._attach_callbacks_to_promise(future_caller):
193-
return self._unpack_promise_results(await future)
191+
result = future.result(timeout=timeout)
194192

195-
@contextmanager
196-
def _attach_callbacks_to_promise(
197-
self, future_caller: Callable[[Any], None]
198-
) -> Generator[None, None, None]:
199-
"""Attach the given Python callbacks to a JS Promise."""
193+
if is_rejected:
194+
msg = _get_exception_msg(result[0])
195+
raise JSPromiseError(msg)
200196

201-
def on_resolved_and_cleanup(
202-
value: PythonJSConvertedTypes | JSEvalException,
203-
) -> None:
204-
future_caller([False, cast("JSArray", value)])
197+
return result[0]
205198

206-
def on_rejected_and_cleanup(
207-
value: PythonJSConvertedTypes | JSEvalException,
208-
) -> None:
209-
future_caller([True, cast("JSArray", value)])
199+
def __await__(self) -> Generator[Any, None, Any]:
200+
return self._do_await().__await__()
210201

211-
with (
212-
self._ctx.js_to_py_callback(on_resolved_and_cleanup) as on_resolved_js_func,
213-
self._ctx.js_to_py_callback(on_rejected_and_cleanup) as on_rejected_js_func,
202+
async def _do_await(self) -> PythonJSConvertedTypes:
203+
future: Future[PythonJSConvertedTypes] = get_running_loop().create_future()
204+
205+
async def on_resolved(value: PythonJSConvertedTypes | JSEvalException) -> None:
206+
future.set_result(cast("PythonJSConvertedTypes", value))
207+
208+
async def on_rejected(value: PythonJSConvertedTypes | JSEvalException) -> None:
209+
future.set_exception(
210+
JSPromiseError(
211+
_get_exception_msg(cast("PythonJSConvertedTypes", value))
212+
)
213+
)
214+
215+
async with (
216+
wrap_py_function_as_js_function(
217+
self._ctx, on_resolved
218+
) as on_resolved_js_func,
219+
wrap_py_function_as_js_function(
220+
self._ctx, on_rejected
221+
) as on_rejected_js_func,
214222
):
215223
self._ctx.promise_then(self, on_resolved_js_func, on_rejected_js_func)
216-
yield
217224

218-
def _unpack_promise_results(
219-
self, results: PythonJSConvertedTypes
220-
) -> PythonJSConvertedTypes:
221-
is_rejected, argv = cast("JSArray", results)
222-
result = cast("JSArray", argv)[0]
223-
if is_rejected:
224-
msg = _get_exception_msg(result)
225-
raise JSPromiseError(msg)
226-
return result
225+
return await future

src/py_mini_racer/_types.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
from __future__ import annotations
44

55
from abc import ABC
6-
from collections.abc import Generator, MutableMapping, MutableSequence
6+
from collections.abc import (
7+
Awaitable,
8+
Callable,
9+
Generator,
10+
MutableMapping,
11+
MutableSequence,
12+
)
713
from datetime import datetime
814
from typing import Any, TypeAlias
915

@@ -100,3 +106,5 @@ async def _do_await(self) -> PythonJSConvertedTypes:
100106
| JSArray
101107
| None
102108
)
109+
110+
PyJsFunctionType = Callable[..., Awaitable[PythonJSConvertedTypes]]

0 commit comments

Comments
 (0)