stock-tracker
This commit is contained in:
commit
6087341a48
6463 files changed
+1929869
No files matched your search
@@ -0,0 +1,3 @@
|
||||
__author__ = "bpcreech"
|
||||
__email__ = "mini-racer@bpcreech.com"
|
||||
__version__ = "0.12.4"
|
||||
@@ -0,0 +1,62 @@
|
||||
from py_mini_racer._context import (
|
||||
PyJsFunctionType,
|
||||
)
|
||||
from py_mini_racer._dll import (
|
||||
DEFAULT_V8_FLAGS,
|
||||
LibAlreadyInitializedError,
|
||||
LibNotFoundError,
|
||||
init_mini_racer,
|
||||
)
|
||||
from py_mini_racer._mini_racer import (
|
||||
MiniRacer,
|
||||
StrictMiniRacer,
|
||||
)
|
||||
from py_mini_racer._objects import (
|
||||
JSArray,
|
||||
JSArrayIndexError,
|
||||
JSFunction,
|
||||
JSPromise,
|
||||
JSPromiseError,
|
||||
JSSymbol,
|
||||
)
|
||||
from py_mini_racer._types import (
|
||||
JSEvalException,
|
||||
JSObject,
|
||||
JSTimeoutException,
|
||||
JSUndefined,
|
||||
JSUndefinedType,
|
||||
PythonJSConvertedTypes,
|
||||
)
|
||||
from py_mini_racer._value_handle import (
|
||||
JSKeyError,
|
||||
JSOOMException,
|
||||
JSParseException,
|
||||
JSValueError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_V8_FLAGS",
|
||||
"JSKeyError",
|
||||
"JSOOMException",
|
||||
"JSParseException",
|
||||
"JSValueError",
|
||||
"LibAlreadyInitializedError",
|
||||
"LibNotFoundError",
|
||||
"init_mini_racer",
|
||||
"MiniRacer",
|
||||
"StrictMiniRacer",
|
||||
"JSArray",
|
||||
"JSArrayIndexError",
|
||||
"JSFunction",
|
||||
"JSPromise",
|
||||
"JSPromiseError",
|
||||
"JSSymbol",
|
||||
"JSEvalException",
|
||||
"JSObject",
|
||||
"JSTimeoutException",
|
||||
"JSUndefined",
|
||||
"JSUndefinedType",
|
||||
"PythonJSConvertedTypes",
|
||||
"PyJsFunctionType",
|
||||
"AsyncCleanupType",
|
||||
]
|
||||
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
)
|
||||
|
||||
from py_mini_racer._types import JSUndefined
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
from py_mini_racer._numeric import Numeric
|
||||
from py_mini_racer._objects import (
|
||||
JSArray,
|
||||
JSFunction,
|
||||
JSPromise,
|
||||
)
|
||||
from py_mini_racer._types import (
|
||||
JSEvalException,
|
||||
JSObject,
|
||||
JSUndefinedType,
|
||||
PythonJSConvertedTypes,
|
||||
)
|
||||
|
||||
|
||||
class AbstractValueHandle(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def raw(self) -> object:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_python(self) -> PythonJSConvertedTypes | JSEvalException:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_python_or_raise(self) -> PythonJSConvertedTypes:
|
||||
pass
|
||||
|
||||
|
||||
class AbstractContext(ABC):
|
||||
"""A Context provides Pythonic wrappers around the MiniRacer C API.
|
||||
|
||||
This is intended for internal usage by py_mini_racer. MiniRacerContext provides
|
||||
a further wrapper around this interface.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_identity_hash(self, obj: JSObject) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_own_property_names(
|
||||
self, obj: JSObject
|
||||
) -> tuple[PythonJSConvertedTypes, ...]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_object_item(
|
||||
self, obj: JSObject, key: PythonJSConvertedTypes
|
||||
) -> PythonJSConvertedTypes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_object_item(
|
||||
self, obj: JSObject, key: PythonJSConvertedTypes, val: PythonJSConvertedTypes
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def del_object_item(self, obj: JSObject, key: PythonJSConvertedTypes) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def del_from_array(self, arr: JSArray, index: int) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def array_insert(
|
||||
self, arr: JSArray, index: int, new_val: PythonJSConvertedTypes
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def call_function(
|
||||
self,
|
||||
func: JSFunction,
|
||||
*args: PythonJSConvertedTypes,
|
||||
this: JSObject | JSUndefinedType = JSUndefined,
|
||||
timeout_sec: Numeric | None = None,
|
||||
) -> PythonJSConvertedTypes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def js_callback(
|
||||
self, func: Callable[[PythonJSConvertedTypes | JSEvalException], None]
|
||||
) -> AbstractContextManager[JSFunction]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def promise_then(
|
||||
self, promise: JSPromise, on_resolved: JSFunction, on_rejected: JSFunction
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_intish_val(self, val: int, typ: int) -> AbstractValueHandle:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_doublish_val(self, val: float, typ: int) -> AbstractValueHandle:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_string_val(self, val: str, typ: int) -> AbstractValueHandle:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def free(self, val_handle: AbstractValueHandle) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def evaluate(
|
||||
self,
|
||||
code: str,
|
||||
timeout_sec: Numeric | None = None,
|
||||
) -> PythonJSConvertedTypes:
|
||||
pass
|
||||
@@ -0,0 +1,473 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import (
|
||||
FIRST_COMPLETED,
|
||||
Task,
|
||||
create_task,
|
||||
get_running_loop,
|
||||
wait,
|
||||
)
|
||||
from contextlib import asynccontextmanager, contextmanager, suppress
|
||||
from itertools import count
|
||||
from traceback import format_exc
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterator,
|
||||
cast,
|
||||
)
|
||||
|
||||
from py_mini_racer._abstract_context import AbstractContext
|
||||
from py_mini_racer._dll import (
|
||||
MR_CALLBACK,
|
||||
init_mini_racer,
|
||||
)
|
||||
from py_mini_racer._objects import (
|
||||
JSArray,
|
||||
JSFunction,
|
||||
JSPromise,
|
||||
)
|
||||
from py_mini_racer._sync_future import SyncFuture
|
||||
from py_mini_racer._types import (
|
||||
JSEvalException,
|
||||
JSObject,
|
||||
JSUndefined,
|
||||
JSUndefinedType,
|
||||
PythonJSConvertedTypes,
|
||||
)
|
||||
from py_mini_racer._value_handle import (
|
||||
ValueHandle,
|
||||
python_to_value_handle,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ctypes
|
||||
from asyncio import Future
|
||||
|
||||
from py_mini_racer._abstract_context import AbstractValueHandle
|
||||
from py_mini_racer._numeric import Numeric
|
||||
from py_mini_racer._value_handle import RawValueHandleType
|
||||
|
||||
PyJsFunctionType = Callable[..., Awaitable[PythonJSConvertedTypes]]
|
||||
AsyncCleanupType = Callable[[], Awaitable[None]]
|
||||
|
||||
|
||||
def context_count() -> int:
|
||||
"""For tests only: how many context handles are still allocated?"""
|
||||
|
||||
dll = init_mini_racer(ignore_duplicate_init=True)
|
||||
return int(dll.mr_context_count())
|
||||
|
||||
|
||||
class _CallbackRegistry:
|
||||
def __init__(
|
||||
self, raw_handle_wrapper: Callable[[RawValueHandleType], AbstractValueHandle]
|
||||
):
|
||||
self._active_callbacks: dict[
|
||||
int, Callable[[PythonJSConvertedTypes | JSEvalException], None]
|
||||
] = {}
|
||||
|
||||
# define an all-purpose callback:
|
||||
@MR_CALLBACK # type: ignore[misc]
|
||||
def mr_callback(callback_id: int, raw_val_handle: RawValueHandleType) -> None:
|
||||
val_handle = raw_handle_wrapper(raw_val_handle)
|
||||
callback = self._active_callbacks[callback_id]
|
||||
callback(val_handle.to_python())
|
||||
|
||||
self.mr_callback = mr_callback
|
||||
|
||||
self._next_callback_id = count()
|
||||
|
||||
def register(
|
||||
self, func: Callable[[PythonJSConvertedTypes | JSEvalException], None]
|
||||
) -> int:
|
||||
callback_id = next(self._next_callback_id)
|
||||
|
||||
self._active_callbacks[callback_id] = func
|
||||
|
||||
return callback_id
|
||||
|
||||
def cleanup(self, callback_id: int) -> None:
|
||||
self._active_callbacks.pop(callback_id)
|
||||
|
||||
|
||||
class Context(AbstractContext):
|
||||
"""Wrapper for all operations involving the DLL and C++ MiniRacer::Context."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dll: ctypes.CDLL,
|
||||
) -> None:
|
||||
self._dll: ctypes.CDLL | None = dll
|
||||
|
||||
self._callback_registry = _CallbackRegistry(self._wrap_raw_handle)
|
||||
self._ctx = dll.mr_init_context(self._callback_registry.mr_callback)
|
||||
|
||||
def _get_dll(self) -> ctypes.CDLL:
|
||||
if self._dll is None:
|
||||
msg = "Operation on closed Context"
|
||||
raise ValueError(msg)
|
||||
|
||||
return self._dll
|
||||
|
||||
def v8_version(self) -> str:
|
||||
return str(self._get_dll().mr_v8_version().decode("utf-8"))
|
||||
|
||||
def v8_is_using_sandbox(self) -> bool:
|
||||
"""Checks for enablement of the V8 Sandbox. See https://v8.dev/blog/sandbox."""
|
||||
|
||||
return bool(self._get_dll().mr_v8_is_using_sandbox())
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
code: str,
|
||||
timeout_sec: Numeric | None = None,
|
||||
) -> PythonJSConvertedTypes:
|
||||
code_handle = python_to_value_handle(self, code)
|
||||
|
||||
with self._run_mr_task(
|
||||
self._get_dll().mr_eval, self._ctx, code_handle.raw
|
||||
) as future:
|
||||
return future.get(timeout=timeout_sec)
|
||||
|
||||
def promise_then(
|
||||
self, promise: JSPromise, on_resolved: JSFunction, on_rejected: JSFunction
|
||||
) -> None:
|
||||
promise_handle = python_to_value_handle(self, promise)
|
||||
then_name_handle = python_to_value_handle(self, "then")
|
||||
then_func = self._wrap_raw_handle(
|
||||
self._get_dll().mr_get_object_item(
|
||||
self._ctx,
|
||||
promise_handle.raw,
|
||||
then_name_handle.raw,
|
||||
)
|
||||
).to_python_or_raise()
|
||||
|
||||
then_func = cast(JSFunction, then_func)
|
||||
then_func(on_resolved, on_rejected, this=promise)
|
||||
|
||||
def get_identity_hash(self, obj: JSObject) -> int:
|
||||
obj_handle = python_to_value_handle(self, obj)
|
||||
|
||||
ret = self._wrap_raw_handle(
|
||||
self._get_dll().mr_get_identity_hash(self._ctx, obj_handle.raw)
|
||||
).to_python_or_raise()
|
||||
return cast(int, ret)
|
||||
|
||||
def get_own_property_names(
|
||||
self, obj: JSObject
|
||||
) -> tuple[PythonJSConvertedTypes, ...]:
|
||||
obj_handle = python_to_value_handle(self, obj)
|
||||
|
||||
names = self._wrap_raw_handle(
|
||||
self._get_dll().mr_get_own_property_names(self._ctx, obj_handle.raw)
|
||||
).to_python_or_raise()
|
||||
if not isinstance(names, JSArray):
|
||||
raise TypeError
|
||||
return tuple(names)
|
||||
|
||||
def get_object_item(
|
||||
self, obj: JSObject, key: PythonJSConvertedTypes
|
||||
) -> PythonJSConvertedTypes:
|
||||
obj_handle = python_to_value_handle(self, obj)
|
||||
key_handle = python_to_value_handle(self, key)
|
||||
|
||||
return self._wrap_raw_handle(
|
||||
self._get_dll().mr_get_object_item(
|
||||
self._ctx,
|
||||
obj_handle.raw,
|
||||
key_handle.raw,
|
||||
)
|
||||
).to_python_or_raise()
|
||||
|
||||
def set_object_item(
|
||||
self, obj: JSObject, key: PythonJSConvertedTypes, val: PythonJSConvertedTypes
|
||||
) -> None:
|
||||
obj_handle = python_to_value_handle(self, obj)
|
||||
key_handle = python_to_value_handle(self, key)
|
||||
val_handle = python_to_value_handle(self, val)
|
||||
|
||||
# Convert the value just to convert any exceptions (and GC the result)
|
||||
self._wrap_raw_handle(
|
||||
self._get_dll().mr_set_object_item(
|
||||
self._ctx,
|
||||
obj_handle.raw,
|
||||
key_handle.raw,
|
||||
val_handle.raw,
|
||||
)
|
||||
).to_python_or_raise()
|
||||
|
||||
def del_object_item(self, obj: JSObject, key: PythonJSConvertedTypes) -> None:
|
||||
obj_handle = python_to_value_handle(self, obj)
|
||||
key_handle = python_to_value_handle(self, key)
|
||||
|
||||
# Convert the value just to convert any exceptions (and GC the result)
|
||||
self._wrap_raw_handle(
|
||||
self._get_dll().mr_del_object_item(
|
||||
self._ctx,
|
||||
obj_handle.raw,
|
||||
key_handle.raw,
|
||||
)
|
||||
).to_python_or_raise()
|
||||
|
||||
def del_from_array(self, arr: JSArray, index: int) -> None:
|
||||
arr_handle = python_to_value_handle(self, arr)
|
||||
|
||||
# Convert the value just to convert any exceptions (and GC the result)
|
||||
self._wrap_raw_handle(
|
||||
self._get_dll().mr_splice_array(self._ctx, arr_handle.raw, index, 1, None)
|
||||
).to_python_or_raise()
|
||||
|
||||
def array_insert(
|
||||
self, arr: JSArray, index: int, new_val: PythonJSConvertedTypes
|
||||
) -> None:
|
||||
arr_handle = python_to_value_handle(self, arr)
|
||||
new_val_handle = python_to_value_handle(self, new_val)
|
||||
|
||||
# Convert the value just to convert any exceptions (and GC the result)
|
||||
self._wrap_raw_handle(
|
||||
self._get_dll().mr_splice_array(
|
||||
self._ctx,
|
||||
arr_handle.raw,
|
||||
index,
|
||||
0,
|
||||
new_val_handle.raw,
|
||||
)
|
||||
).to_python_or_raise()
|
||||
|
||||
def call_function(
|
||||
self,
|
||||
func: JSFunction,
|
||||
*args: PythonJSConvertedTypes,
|
||||
this: JSObject | JSUndefinedType = JSUndefined,
|
||||
timeout_sec: Numeric | None = None,
|
||||
) -> PythonJSConvertedTypes:
|
||||
argv = cast(JSArray, self.evaluate("[]"))
|
||||
for arg in args:
|
||||
argv.append(arg)
|
||||
|
||||
func_handle = python_to_value_handle(self, func)
|
||||
this_handle = python_to_value_handle(self, this)
|
||||
argv_handle = python_to_value_handle(self, argv)
|
||||
|
||||
with self._run_mr_task(
|
||||
self._get_dll().mr_call_function,
|
||||
self._ctx,
|
||||
func_handle.raw,
|
||||
this_handle.raw,
|
||||
argv_handle.raw,
|
||||
) as future:
|
||||
return future.get(timeout=timeout_sec)
|
||||
|
||||
def set_hard_memory_limit(self, limit: int) -> None:
|
||||
self._get_dll().mr_set_hard_memory_limit(self._ctx, limit)
|
||||
|
||||
def set_soft_memory_limit(self, limit: int) -> None:
|
||||
self._get_dll().mr_set_soft_memory_limit(self._ctx, limit)
|
||||
|
||||
def was_hard_memory_limit_reached(self) -> bool:
|
||||
return bool(self._get_dll().mr_hard_memory_limit_reached(self._ctx))
|
||||
|
||||
def was_soft_memory_limit_reached(self) -> bool:
|
||||
return bool(self._get_dll().mr_soft_memory_limit_reached(self._ctx))
|
||||
|
||||
def low_memory_notification(self) -> None:
|
||||
self._get_dll().mr_low_memory_notification(self._ctx)
|
||||
|
||||
def heap_stats(self) -> str:
|
||||
with self._run_mr_task(self._get_dll().mr_heap_stats, self._ctx) as future:
|
||||
return cast(str, future.get())
|
||||
|
||||
def heap_snapshot(self) -> str:
|
||||
"""Return a snapshot of the V8 isolate heap."""
|
||||
|
||||
with self._run_mr_task(self._get_dll().mr_heap_snapshot, self._ctx) as future:
|
||||
return cast(str, future.get())
|
||||
|
||||
def value_count(self) -> int:
|
||||
"""For tests only: how many value handles are still allocated?"""
|
||||
|
||||
return int(self._get_dll().mr_value_count(self._ctx))
|
||||
|
||||
@contextmanager
|
||||
def js_callback(
|
||||
self, func: Callable[[PythonJSConvertedTypes | JSEvalException], None]
|
||||
) -> Iterator[JSFunction]:
|
||||
"""Make a JS callback which forwards to the given Python function.
|
||||
|
||||
Note that it's crucial that the given Python function *not* call back
|
||||
into the C++ MiniRacer context, or it will deadlock. Instead it should
|
||||
signal another thread; e.g., by putting received data onto a queue or
|
||||
future.
|
||||
"""
|
||||
|
||||
callback_id = self._callback_registry.register(func)
|
||||
|
||||
cb = self._wrap_raw_handle(
|
||||
self._get_dll().mr_make_js_callback(self._ctx, callback_id)
|
||||
)
|
||||
cb_py = cast(JSFunction, cb.to_python_or_raise())
|
||||
|
||||
yield cb_py
|
||||
|
||||
self._callback_registry.cleanup(callback_id)
|
||||
|
||||
def _wrap_raw_handle(self, raw: RawValueHandleType) -> ValueHandle:
|
||||
return ValueHandle(self, raw)
|
||||
|
||||
def create_intish_val(self, val: int, typ: int) -> AbstractValueHandle:
|
||||
return self._wrap_raw_handle(
|
||||
self._get_dll().mr_alloc_int_val(
|
||||
self._ctx,
|
||||
val,
|
||||
typ,
|
||||
)
|
||||
)
|
||||
|
||||
def create_doublish_val(self, val: float, typ: int) -> AbstractValueHandle:
|
||||
return self._wrap_raw_handle(
|
||||
self._get_dll().mr_alloc_double_val(
|
||||
self._ctx,
|
||||
val,
|
||||
typ,
|
||||
)
|
||||
)
|
||||
|
||||
def create_string_val(self, val: str, typ: int) -> AbstractValueHandle:
|
||||
b = val.encode("utf-8")
|
||||
return self._wrap_raw_handle(
|
||||
self._get_dll().mr_alloc_string_val(
|
||||
self._ctx,
|
||||
b,
|
||||
len(b),
|
||||
typ,
|
||||
)
|
||||
)
|
||||
|
||||
def free(self, val_handle: AbstractValueHandle) -> None:
|
||||
dll = self._dll
|
||||
if dll is not None:
|
||||
dll.mr_free_value(self._ctx, val_handle.raw)
|
||||
|
||||
@contextmanager
|
||||
def _run_mr_task(self, dll_method: Any, *args: Any) -> Iterator[SyncFuture]:
|
||||
"""Manages those tasks which generate callbacks from the MiniRacer DLL.
|
||||
|
||||
Several MiniRacer functions (JS evaluation and 2 heap stats calls) are
|
||||
asynchronous. They take a function callback and callback data parameter, and
|
||||
they return a task handle.
|
||||
|
||||
In this method, we create a future for each callback to get the right data to
|
||||
the right caller, and we manage the lifecycle of the task and task handle.
|
||||
"""
|
||||
|
||||
future = SyncFuture()
|
||||
|
||||
def callback(value: PythonJSConvertedTypes | JSEvalException) -> None:
|
||||
if isinstance(value, JSEvalException):
|
||||
future.set_exception(value)
|
||||
else:
|
||||
future.set_result(value)
|
||||
|
||||
callback_id = self._callback_registry.register(callback)
|
||||
|
||||
# Start the task:
|
||||
task_id = dll_method(*args, callback_id)
|
||||
try:
|
||||
# Let the caller handle waiting on the result:
|
||||
yield future
|
||||
finally:
|
||||
# Cancel the task if it's not already done (this call is ignored if it's
|
||||
# already done)
|
||||
self._get_dll().mr_cancel_task(self._ctx, task_id)
|
||||
|
||||
# If the caller gives up on waiting, let's at least await the
|
||||
# cancelation error for GC purposes:
|
||||
with suppress(Exception):
|
||||
future.get()
|
||||
|
||||
self._callback_registry.cleanup(callback_id)
|
||||
|
||||
@asynccontextmanager
|
||||
async def wrap_py_function(
|
||||
self, func: PyJsFunctionType
|
||||
) -> AsyncIterator[JSFunction]:
|
||||
async def run_one(params: JSArray) -> None:
|
||||
arguments, resolve, reject = params
|
||||
arguments = cast(JSArray, arguments)
|
||||
resolve = cast(JSFunction, resolve)
|
||||
reject = cast(JSFunction, reject)
|
||||
try:
|
||||
result = await func(*arguments)
|
||||
resolve(result)
|
||||
except Exception: # noqa: BLE001
|
||||
# Convert this Python exception into a JS exception so we can send it
|
||||
# into JS:
|
||||
s = f"Error running Python function:\n{format_exc()}"
|
||||
err_maker = self.evaluate("s => new Error(s)")
|
||||
err_maker = cast(JSFunction, err_maker)
|
||||
err = err_maker(s)
|
||||
reject(err)
|
||||
|
||||
pending: set[Task[PythonJSConvertedTypes | JSEvalException] | Future[bool]] = (
|
||||
set()
|
||||
)
|
||||
|
||||
def process(params: PythonJSConvertedTypes | JSEvalException) -> None:
|
||||
params = cast(JSArray, params)
|
||||
|
||||
# Start a new task to run the new task:
|
||||
task = create_task(run_one(params))
|
||||
pending.add(task)
|
||||
|
||||
loop = get_running_loop()
|
||||
|
||||
def on_called(value: PythonJSConvertedTypes | JSEvalException) -> None:
|
||||
loop.call_soon_threadsafe(process, value)
|
||||
|
||||
async def await_pending() -> None:
|
||||
nonlocal pending
|
||||
while pending:
|
||||
done, pending = await wait(pending, return_when=FIRST_COMPLETED)
|
||||
for coro in done:
|
||||
await coro
|
||||
|
||||
shutdown: Future[bool] = loop.create_future()
|
||||
pending.add(shutdown)
|
||||
pending_awaiter = create_task(await_pending())
|
||||
try:
|
||||
with self.js_callback(on_called) as callback:
|
||||
wrapper = self.evaluate(
|
||||
"""
|
||||
callback => {
|
||||
return (...arguments) => {
|
||||
let p = Promise.withResolvers();
|
||||
|
||||
callback(arguments, p.resolve, p.reject);
|
||||
|
||||
return p.promise;
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
wrapper = cast(JSFunction, wrapper)
|
||||
|
||||
wrapped = wrapper(callback)
|
||||
wrapped = cast(JSFunction, wrapped)
|
||||
|
||||
yield wrapped
|
||||
finally:
|
||||
# Stop accepting calls:
|
||||
shutdown.set_result(True)
|
||||
await pending_awaiter
|
||||
|
||||
def close(self) -> None:
|
||||
dll, self._dll = self._dll, None
|
||||
if dll:
|
||||
dll.mr_free_context(self._ctx)
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.close()
|
||||
@@ -0,0 +1,279 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from importlib import resources
|
||||
from os.path import exists
|
||||
from os.path import join as pathjoin
|
||||
from sys import platform, version_info
|
||||
from threading import Lock
|
||||
from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
)
|
||||
|
||||
from py_mini_racer._types import (
|
||||
MiniRacerBaseException,
|
||||
)
|
||||
from py_mini_racer._value_handle import (
|
||||
RawValueHandle,
|
||||
)
|
||||
|
||||
|
||||
def _get_lib_filename(name: str) -> str:
|
||||
"""Return the path of the library called `name` on the current system."""
|
||||
if platform == "darwin":
|
||||
prefix, ext = "lib", ".dylib"
|
||||
elif platform == "win32":
|
||||
prefix, ext = "", ".dll"
|
||||
else:
|
||||
prefix, ext = "lib", ".so"
|
||||
|
||||
return prefix + name + ext
|
||||
|
||||
|
||||
MR_CALLBACK = ctypes.CFUNCTYPE(None, ctypes.c_uint64, RawValueHandle)
|
||||
|
||||
|
||||
def _build_dll_handle(dll_path: str) -> ctypes.CDLL:
|
||||
handle = ctypes.CDLL(dll_path)
|
||||
|
||||
handle.mr_init_v8.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
|
||||
|
||||
handle.mr_init_context.argtypes = [MR_CALLBACK]
|
||||
handle.mr_init_context.restype = ctypes.c_uint64
|
||||
|
||||
handle.mr_eval.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
ctypes.c_uint64,
|
||||
]
|
||||
handle.mr_eval.restype = ctypes.c_uint64
|
||||
|
||||
handle.mr_free_value.argtypes = [ctypes.c_uint64, RawValueHandle]
|
||||
|
||||
handle.mr_alloc_int_val.argtypes = [ctypes.c_uint64, ctypes.c_int64, ctypes.c_uint8]
|
||||
handle.mr_alloc_int_val.restype = RawValueHandle
|
||||
|
||||
handle.mr_alloc_double_val.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_double,
|
||||
ctypes.c_uint8,
|
||||
]
|
||||
handle.mr_alloc_double_val.restype = RawValueHandle
|
||||
|
||||
handle.mr_alloc_string_val.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_uint8,
|
||||
]
|
||||
handle.mr_alloc_string_val.restype = RawValueHandle
|
||||
|
||||
handle.mr_free_context.argtypes = [ctypes.c_uint64]
|
||||
|
||||
handle.mr_context_count.argtypes = []
|
||||
handle.mr_context_count.restype = ctypes.c_size_t
|
||||
|
||||
handle.mr_cancel_task.argtypes = [ctypes.c_uint64, ctypes.c_uint64]
|
||||
|
||||
handle.mr_heap_stats.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_uint64,
|
||||
]
|
||||
handle.mr_heap_stats.restype = ctypes.c_uint64
|
||||
|
||||
handle.mr_low_memory_notification.argtypes = [ctypes.c_uint64]
|
||||
|
||||
handle.mr_make_js_callback.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_uint64,
|
||||
]
|
||||
handle.mr_make_js_callback.restype = RawValueHandle
|
||||
|
||||
handle.mr_heap_snapshot.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_uint64,
|
||||
]
|
||||
handle.mr_heap_snapshot.restype = ctypes.c_uint64
|
||||
|
||||
handle.mr_get_identity_hash.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
]
|
||||
handle.mr_get_identity_hash.restype = RawValueHandle
|
||||
|
||||
handle.mr_get_own_property_names.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
]
|
||||
handle.mr_get_own_property_names.restype = RawValueHandle
|
||||
|
||||
handle.mr_get_object_item.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
RawValueHandle,
|
||||
]
|
||||
handle.mr_get_object_item.restype = RawValueHandle
|
||||
|
||||
handle.mr_set_object_item.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
RawValueHandle,
|
||||
RawValueHandle,
|
||||
]
|
||||
handle.mr_set_object_item.restype = RawValueHandle
|
||||
|
||||
handle.mr_del_object_item.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
RawValueHandle,
|
||||
]
|
||||
handle.mr_del_object_item.restype = RawValueHandle
|
||||
|
||||
handle.mr_splice_array.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
ctypes.c_int32,
|
||||
ctypes.c_int32,
|
||||
RawValueHandle,
|
||||
]
|
||||
handle.mr_splice_array.restype = RawValueHandle
|
||||
|
||||
handle.mr_call_function.argtypes = [
|
||||
ctypes.c_uint64,
|
||||
RawValueHandle,
|
||||
RawValueHandle,
|
||||
RawValueHandle,
|
||||
ctypes.c_uint64,
|
||||
]
|
||||
handle.mr_call_function.restype = ctypes.c_uint64
|
||||
|
||||
handle.mr_set_hard_memory_limit.argtypes = [ctypes.c_uint64, ctypes.c_size_t]
|
||||
|
||||
handle.mr_set_soft_memory_limit.argtypes = [ctypes.c_uint64, ctypes.c_size_t]
|
||||
handle.mr_set_soft_memory_limit.restype = None
|
||||
|
||||
handle.mr_hard_memory_limit_reached.argtypes = [ctypes.c_uint64]
|
||||
handle.mr_hard_memory_limit_reached.restype = ctypes.c_bool
|
||||
|
||||
handle.mr_soft_memory_limit_reached.argtypes = [ctypes.c_uint64]
|
||||
handle.mr_soft_memory_limit_reached.restype = ctypes.c_bool
|
||||
|
||||
handle.mr_v8_version.argtypes = []
|
||||
handle.mr_v8_version.restype = ctypes.c_char_p
|
||||
|
||||
handle.mr_v8_is_using_sandbox.argtypes = []
|
||||
handle.mr_v8_is_using_sandbox.restype = ctypes.c_bool
|
||||
|
||||
handle.mr_value_count.argtypes = [ctypes.c_uint64]
|
||||
handle.mr_value_count.restype = ctypes.c_size_t
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
# V8 internationalization data:
|
||||
_ICU_DATA_FILENAME = "icudtl.dat"
|
||||
|
||||
# V8 fast-startup snapshot; a dump of the heap after loading built-in JS
|
||||
# modules:
|
||||
_SNAPSHOT_FILENAME = "snapshot_blob.bin"
|
||||
|
||||
DEFAULT_V8_FLAGS = ("--single-threaded",)
|
||||
|
||||
|
||||
class LibNotFoundError(MiniRacerBaseException):
|
||||
"""MiniRacer-wrapped V8 build not found."""
|
||||
|
||||
def __init__(self, path: str):
|
||||
super().__init__(f"Native library or dependency not available at {path}")
|
||||
|
||||
|
||||
class LibAlreadyInitializedError(MiniRacerBaseException):
|
||||
"""MiniRacer-wrapped V8 build not found."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"MiniRacer was already initialized before the call to init_mini_racer"
|
||||
)
|
||||
|
||||
|
||||
def _open_resource_file(filename: str, exit_stack: ExitStack) -> str:
|
||||
if version_info >= (3, 9):
|
||||
# resources.as_file was added in Python 3.9
|
||||
resource_path = resources.files("py_mini_racer") / filename
|
||||
|
||||
context_manager = resources.as_file(resource_path)
|
||||
else:
|
||||
# now-deprecated API for Pythons older than 3.9
|
||||
context_manager = resources.path("py_mini_racer", filename)
|
||||
|
||||
return str(exit_stack.enter_context(context_manager))
|
||||
|
||||
|
||||
def _check_path(path: str) -> None:
|
||||
if path is None or not exists(path):
|
||||
raise LibNotFoundError(path)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _open_dll(flags: Iterable[str]) -> Iterator[ctypes.CDLL]:
|
||||
dll_filename = _get_lib_filename("mini_racer")
|
||||
|
||||
with ExitStack() as exit_stack:
|
||||
# Find the dll and its external dependency files:
|
||||
meipass = getattr(sys, "_MEIPASS", None)
|
||||
if meipass is not None:
|
||||
# We are running under PyInstaller
|
||||
dll_path = pathjoin(meipass, dll_filename)
|
||||
icu_data_path = pathjoin(meipass, _ICU_DATA_FILENAME)
|
||||
snapshot_path = pathjoin(meipass, _SNAPSHOT_FILENAME)
|
||||
else:
|
||||
dll_path = _open_resource_file(dll_filename, exit_stack)
|
||||
icu_data_path = _open_resource_file(_ICU_DATA_FILENAME, exit_stack)
|
||||
snapshot_path = _open_resource_file(_SNAPSHOT_FILENAME, exit_stack)
|
||||
|
||||
_check_path(dll_path)
|
||||
_check_path(icu_data_path)
|
||||
_check_path(snapshot_path)
|
||||
|
||||
handle = _build_dll_handle(dll_path)
|
||||
|
||||
handle.mr_init_v8(
|
||||
" ".join(flags).encode("utf-8"),
|
||||
icu_data_path.encode("utf-8"),
|
||||
snapshot_path.encode("utf-8"),
|
||||
)
|
||||
|
||||
yield handle
|
||||
|
||||
|
||||
_init_lock = Lock()
|
||||
_dll_handle_context_manager = None
|
||||
_dll_handle = None
|
||||
|
||||
|
||||
def init_mini_racer(
|
||||
*, flags: Iterable[str] = DEFAULT_V8_FLAGS, ignore_duplicate_init: bool = False
|
||||
) -> ctypes.CDLL:
|
||||
"""Initialize py_mini_racer (and V8).
|
||||
|
||||
This function can optionally be used to set V8 flags. This function can be called
|
||||
at most once, before any instances of MiniRacer are initialized. Instances of
|
||||
MiniRacer will automatically call this function to initialize MiniRacer and V8.
|
||||
"""
|
||||
|
||||
global _dll_handle_context_manager # noqa: PLW0603
|
||||
global _dll_handle # noqa: PLW0603
|
||||
|
||||
with _init_lock:
|
||||
if _dll_handle is None:
|
||||
_dll_handle_context_manager = _open_dll(flags)
|
||||
_dll_handle = _dll_handle_context_manager.__enter__()
|
||||
# Note: we never call _dll_handle_context_manager.__exit__() because it's
|
||||
# designed as a singleton. But we could if we wanted to!
|
||||
elif not ignore_duplicate_init:
|
||||
raise LibAlreadyInitializedError
|
||||
|
||||
return _dll_handle
|
||||
@@ -0,0 +1,261 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from json import JSONEncoder
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
ClassVar,
|
||||
)
|
||||
|
||||
from py_mini_racer._context import Context
|
||||
from py_mini_racer._dll import init_mini_racer
|
||||
from py_mini_racer._set_timeout import INSTALL_SET_TIMEOUT
|
||||
from py_mini_racer._types import MiniRacerBaseException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from types import TracebackType
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from py_mini_racer._context import PyJsFunctionType
|
||||
from py_mini_racer._numeric import Numeric
|
||||
from py_mini_racer._objects import JSFunction
|
||||
from py_mini_racer._types import PythonJSConvertedTypes
|
||||
|
||||
|
||||
class WrongReturnTypeException(MiniRacerBaseException):
|
||||
"""Invalid type returned by the JavaScript runtime."""
|
||||
|
||||
def __init__(self, typ: type):
|
||||
super().__init__(f"Unexpected return value type {typ}")
|
||||
|
||||
|
||||
class MiniRacer:
|
||||
"""
|
||||
MiniRacer evaluates JavaScript code using a V8 isolate.
|
||||
|
||||
A MiniRacer instance can be explicitly closed using the close() method, or by using
|
||||
the MiniRacer as a context manager, i.e,:
|
||||
|
||||
with MiniRacer() as mr:
|
||||
...
|
||||
|
||||
The MiniRacer instance will otherwise clean up the underlying V8 resource upon
|
||||
garbage collection.
|
||||
|
||||
Attributes:
|
||||
json_impl: JSON module used by helper methods default is
|
||||
[json](https://docs.python.org/3/library/json.html)
|
||||
"""
|
||||
|
||||
json_impl: ClassVar[Any] = json
|
||||
|
||||
def __init__(self) -> None:
|
||||
dll = init_mini_racer(ignore_duplicate_init=True)
|
||||
|
||||
self._ctx = Context(dll)
|
||||
|
||||
self.eval(INSTALL_SET_TIMEOUT)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close this MiniRacer instance.
|
||||
|
||||
It is an error to use this MiniRacer instance or any JS objects returned by it
|
||||
after calling this method.
|
||||
"""
|
||||
self._ctx.close()
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
del exc_type
|
||||
del exc_val
|
||||
del exc_tb
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def v8_version(self) -> str:
|
||||
"""Return the V8 version string."""
|
||||
return self._ctx.v8_version()
|
||||
|
||||
def eval(
|
||||
self,
|
||||
code: str,
|
||||
timeout: Numeric | None = None,
|
||||
timeout_sec: Numeric | None = None,
|
||||
max_memory: int | None = None,
|
||||
) -> PythonJSConvertedTypes:
|
||||
"""Evaluate JavaScript code in the V8 isolate.
|
||||
|
||||
Side effects from the JavaScript evaluation is persisted inside a context
|
||||
(meaning variables set are kept for the next evaluation).
|
||||
|
||||
The JavaScript value returned by the last expression in `code` is converted to
|
||||
a Python value and returned by this method. Only primitive types are supported
|
||||
(numbers, strings, buffers...). Use the
|
||||
[py_mini_racer.MiniRacer.execute][] method to return more complex
|
||||
types such as arrays or objects.
|
||||
|
||||
The evaluation can be interrupted by an exception for several reasons: a limit
|
||||
was reached, the code could not be parsed, a returned value could not be
|
||||
converted to a Python value.
|
||||
|
||||
Args:
|
||||
code: JavaScript code
|
||||
timeout: number of milliseconds after which the execution is interrupted.
|
||||
This is deprecated; use timeout_sec instead.
|
||||
timeout_sec: number of seconds after which the execution is interrupted
|
||||
max_memory: hard memory limit, in bytes, after which the execution is
|
||||
interrupted.
|
||||
"""
|
||||
|
||||
if max_memory is not None:
|
||||
self.set_hard_memory_limit(max_memory)
|
||||
|
||||
if timeout:
|
||||
# PyMiniRacer unfortunately uses milliseconds while Python and
|
||||
# Système international d'unités use seconds.
|
||||
timeout_sec = timeout / 1000
|
||||
|
||||
return self._ctx.evaluate(code=code, timeout_sec=timeout_sec)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
expr: str,
|
||||
timeout: Numeric | None = None,
|
||||
timeout_sec: Numeric | None = None,
|
||||
max_memory: int | None = None,
|
||||
) -> Any:
|
||||
"""Helper to evaluate a JavaScript expression and return composite types.
|
||||
|
||||
Returned value is serialized to JSON inside the V8 isolate and deserialized
|
||||
using `json_impl`.
|
||||
|
||||
Args:
|
||||
expr: JavaScript expression
|
||||
timeout: number of milliseconds after which the execution is interrupted.
|
||||
This is deprecated; use timeout_sec instead.
|
||||
timeout_sec: number of seconds after which the execution is interrupted
|
||||
max_memory: hard memory limit, in bytes, after which the execution is
|
||||
interrupted.
|
||||
"""
|
||||
|
||||
if timeout:
|
||||
# PyMiniRacer unfortunately uses milliseconds while Python and
|
||||
# Système international d'unités use seconds.
|
||||
timeout_sec = timeout / 1000
|
||||
|
||||
wrapped_expr = f"JSON.stringify((function(){{return ({expr})}})())"
|
||||
ret = self.eval(wrapped_expr, timeout_sec=timeout_sec, max_memory=max_memory)
|
||||
if not isinstance(ret, str):
|
||||
raise WrongReturnTypeException(type(ret))
|
||||
return self.json_impl.loads(ret)
|
||||
|
||||
def call(
|
||||
self,
|
||||
expr: str,
|
||||
*args: Any,
|
||||
encoder: JSONEncoder | None = None,
|
||||
timeout: Numeric | None = None,
|
||||
timeout_sec: Numeric | None = None,
|
||||
max_memory: int | None = None,
|
||||
) -> Any:
|
||||
"""Helper to call a JavaScript function and return compositve types.
|
||||
|
||||
The `expr` argument refers to a JavaScript function in the current V8
|
||||
isolate context. Further positional arguments are serialized using the JSON
|
||||
implementation `json_impl` and passed to the JavaScript function as arguments.
|
||||
|
||||
Returned value is serialized to JSON inside the V8 isolate and deserialized
|
||||
using `json_impl`.
|
||||
|
||||
Args:
|
||||
expr: JavaScript expression referring to a function
|
||||
encoder: Custom JSON encoder
|
||||
timeout: number of milliseconds after which the execution is
|
||||
interrupted.
|
||||
timeout_sec: number of seconds after which the execution is interrupted
|
||||
max_memory: hard memory limit, in bytes, after which the execution is
|
||||
interrupted
|
||||
"""
|
||||
|
||||
if timeout:
|
||||
# PyMiniRacer unfortunately uses milliseconds while Python and
|
||||
# Système international d'unités use seconds.
|
||||
timeout_sec = timeout / 1000
|
||||
|
||||
json_args = self.json_impl.dumps(args, separators=(",", ":"), cls=encoder)
|
||||
js = f"{expr}.apply(this, {json_args})"
|
||||
return self.execute(js, timeout_sec=timeout_sec, max_memory=max_memory)
|
||||
|
||||
def wrap_py_function(
|
||||
self,
|
||||
func: PyJsFunctionType,
|
||||
) -> AbstractAsyncContextManager[JSFunction]:
|
||||
"""Wrap a Python function such that it can be called from JS.
|
||||
|
||||
To be wrapped and exposed in JavaScript, a Python function should:
|
||||
|
||||
1. Be async,
|
||||
2. Accept variable positional arguments each of type PythonJSConvertedTypes,
|
||||
and
|
||||
3. Return one value of type PythonJSConvertedTypes (a type union which
|
||||
includes None).
|
||||
|
||||
The function is rendered on the JavaScript side as an async function (i.e., a
|
||||
function which returns a Promise).
|
||||
|
||||
Returns:
|
||||
An async context manager which, when entered, yields a JS Function which
|
||||
can be passed into MiniRacer and called by JS code.
|
||||
"""
|
||||
|
||||
return self._ctx.wrap_py_function(func)
|
||||
|
||||
def set_hard_memory_limit(self, limit: int) -> None:
|
||||
"""Set a hard memory limit on this V8 isolate.
|
||||
|
||||
JavaScript execution will be terminated when this limit is reached.
|
||||
|
||||
:param int limit: memory limit in bytes or 0 to reset the limit
|
||||
"""
|
||||
self._ctx.set_hard_memory_limit(limit)
|
||||
|
||||
def set_soft_memory_limit(self, limit: int) -> None:
|
||||
"""Set a soft memory limit on this V8 isolate.
|
||||
|
||||
The Garbage Collection will use a more aggressive strategy when
|
||||
the soft limit is reached but the execution will not be stopped.
|
||||
|
||||
:param int limit: memory limit in bytes or 0 to reset the limit
|
||||
"""
|
||||
self._ctx.set_soft_memory_limit(limit)
|
||||
|
||||
def was_hard_memory_limit_reached(self) -> bool:
|
||||
"""Return true if the hard memory limit was reached on the V8 isolate."""
|
||||
return self._ctx.was_hard_memory_limit_reached()
|
||||
|
||||
def was_soft_memory_limit_reached(self) -> bool:
|
||||
"""Return true if the soft memory limit was reached on the V8 isolate."""
|
||||
return self._ctx.was_soft_memory_limit_reached()
|
||||
|
||||
def low_memory_notification(self) -> None:
|
||||
"""Ask the V8 isolate to collect memory more aggressively."""
|
||||
self._ctx.low_memory_notification()
|
||||
|
||||
def heap_stats(self) -> Any:
|
||||
"""Return the V8 isolate heap statistics."""
|
||||
|
||||
return self.json_impl.loads(self._ctx.heap_stats())
|
||||
|
||||
|
||||
# Compatibility with versions 0.4 & 0.5
|
||||
StrictMiniRacer = MiniRacer
|
||||
@@ -0,0 +1,5 @@
|
||||
from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
Numeric = Union[int, float]
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Python wrappers for JavaScript object types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import get_running_loop
|
||||
from contextlib import ExitStack
|
||||
from operator import index as op_index
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generator,
|
||||
Iterator,
|
||||
MutableMapping,
|
||||
MutableSequence,
|
||||
cast,
|
||||
)
|
||||
|
||||
from py_mini_racer._sync_future import SyncFuture
|
||||
from py_mini_racer._types import (
|
||||
JSEvalException,
|
||||
JSObject,
|
||||
JSUndefined,
|
||||
JSUndefinedType,
|
||||
MiniRacerBaseException,
|
||||
PythonJSConvertedTypes,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Future
|
||||
|
||||
from py_mini_racer._abstract_context import AbstractContext, AbstractValueHandle
|
||||
from py_mini_racer._numeric import Numeric
|
||||
|
||||
|
||||
def _get_exception_msg(reason: PythonJSConvertedTypes) -> str:
|
||||
if not isinstance(reason, JSMappedObject):
|
||||
return str(reason)
|
||||
|
||||
if "stack" in reason:
|
||||
return cast(str, reason["stack"])
|
||||
|
||||
return str(reason)
|
||||
|
||||
|
||||
class JSPromiseError(MiniRacerBaseException):
|
||||
"""JavaScript rejected a promise."""
|
||||
|
||||
def __init__(self, reason: PythonJSConvertedTypes) -> None:
|
||||
super().__init__(
|
||||
f"JavaScript rejected promise with reason: {_get_exception_msg(reason)}\n"
|
||||
)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class JSArrayIndexError(IndexError, MiniRacerBaseException):
|
||||
"""Invalid index into a JSArray."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("JSArray deletion out of range")
|
||||
|
||||
|
||||
class JSObjectImpl(JSObject):
|
||||
"""A JavaScript object."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ctx: AbstractContext,
|
||||
handle: AbstractValueHandle,
|
||||
):
|
||||
self._ctx = ctx
|
||||
self._handle = handle
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self._ctx.get_identity_hash(self)
|
||||
|
||||
@property
|
||||
def raw_handle(self) -> AbstractValueHandle:
|
||||
return self._handle
|
||||
|
||||
|
||||
class JSMappedObject(
|
||||
JSObjectImpl,
|
||||
MutableMapping[PythonJSConvertedTypes, PythonJSConvertedTypes],
|
||||
):
|
||||
"""A JavaScript object with Pythonic MutableMapping methods (`keys()`,
|
||||
`__getitem__()`, etc).
|
||||
|
||||
`keys()` and `__iter__()` will return properties from any prototypes as well as this
|
||||
object, as if using a for-in statement in JavaScript.
|
||||
"""
|
||||
|
||||
def __iter__(self) -> Iterator[PythonJSConvertedTypes]:
|
||||
return iter(self._get_own_property_names())
|
||||
|
||||
def __getitem__(self, key: PythonJSConvertedTypes) -> PythonJSConvertedTypes:
|
||||
return self._ctx.get_object_item(self, key)
|
||||
|
||||
def __setitem__(
|
||||
self, key: PythonJSConvertedTypes, val: PythonJSConvertedTypes
|
||||
) -> None:
|
||||
self._ctx.set_object_item(self, key, val)
|
||||
|
||||
def __delitem__(self, key: PythonJSConvertedTypes) -> None:
|
||||
self._ctx.del_object_item(self, key)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._get_own_property_names())
|
||||
|
||||
def _get_own_property_names(self) -> tuple[PythonJSConvertedTypes, ...]:
|
||||
return self._ctx.get_own_property_names(self)
|
||||
|
||||
|
||||
class JSArray(MutableSequence[PythonJSConvertedTypes], JSObjectImpl):
|
||||
"""JavaScript array.
|
||||
|
||||
Has Pythonic MutableSequence methods (e.g., `insert()`, `__getitem__()`, ...).
|
||||
"""
|
||||
|
||||
def __len__(self) -> int:
|
||||
ret = self._ctx.get_object_item(self, "length")
|
||||
return cast(int, ret)
|
||||
|
||||
def __getitem__(self, index: int | slice) -> Any:
|
||||
if not isinstance(index, int):
|
||||
raise TypeError
|
||||
|
||||
index = op_index(index)
|
||||
if index < 0:
|
||||
index += len(self)
|
||||
|
||||
if 0 <= index < len(self):
|
||||
return self._ctx.get_object_item(self, index)
|
||||
|
||||
raise IndexError
|
||||
|
||||
def __setitem__(self, index: int | slice, val: Any) -> None:
|
||||
if not isinstance(index, int):
|
||||
raise TypeError
|
||||
|
||||
self._ctx.set_object_item(self, index, val)
|
||||
|
||||
def __delitem__(self, index: int | slice) -> None:
|
||||
if not isinstance(index, int):
|
||||
raise TypeError
|
||||
|
||||
if index >= len(self) or index < -len(self):
|
||||
# JavaScript Array.prototype.splice() just ignores deletion beyond the
|
||||
# end of the array, meaning if you pass a very large value here it would
|
||||
# do nothing. Likewise, it just caps negative values at the length of the
|
||||
# array, meaning if you pass a very negative value here it would just
|
||||
# delete element 0.
|
||||
# For consistency with Python lists, let's tell the caller they're out of
|
||||
# bounds:
|
||||
raise JSArrayIndexError
|
||||
|
||||
return self._ctx.del_from_array(self, index)
|
||||
|
||||
def insert(self, index: int, new_obj: PythonJSConvertedTypes) -> None:
|
||||
return self._ctx.array_insert(self, index, new_obj)
|
||||
|
||||
def __iter__(self) -> Iterator[PythonJSConvertedTypes]:
|
||||
for i in range(len(self)):
|
||||
yield self[i]
|
||||
|
||||
|
||||
class JSFunction(JSMappedObject):
|
||||
"""JavaScript function.
|
||||
|
||||
You can call this object from Python, passing in positional args to match what the
|
||||
JavaScript function expects, along with a keyword argument, `timeout_sec`.
|
||||
"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*args: PythonJSConvertedTypes,
|
||||
this: JSObjectImpl | JSUndefinedType = JSUndefined,
|
||||
timeout_sec: Numeric | None = None,
|
||||
) -> PythonJSConvertedTypes:
|
||||
return self._ctx.call_function(self, *args, this=this, timeout_sec=timeout_sec)
|
||||
|
||||
|
||||
class JSSymbol(JSMappedObject):
|
||||
"""JavaScript symbol."""
|
||||
|
||||
|
||||
class JSPromise(JSObjectImpl):
|
||||
"""JavaScript Promise.
|
||||
|
||||
To get a value, call `promise.get()` to block, or `await promise` from within an
|
||||
`async` coroutine. Either will raise a Python exception if the JavaScript Promise
|
||||
is rejected.
|
||||
"""
|
||||
|
||||
def get(self, *, timeout: Numeric | None = None) -> PythonJSConvertedTypes:
|
||||
"""Get the value, or raise an exception. This call blocks.
|
||||
|
||||
Args:
|
||||
timeout: number of milliseconds after which the execution is interrupted.
|
||||
This is deprecated; use timeout_sec instead.
|
||||
"""
|
||||
|
||||
future = SyncFuture()
|
||||
|
||||
def future_caller(value: Any) -> None:
|
||||
future.set_result(value)
|
||||
|
||||
self._attach_callbacks_to_promise(future_caller)
|
||||
|
||||
results = future.get(timeout=timeout)
|
||||
return self._unpack_promise_results(results)
|
||||
|
||||
def __await__(self) -> Generator[Any, None, Any]:
|
||||
return self._do_await().__await__()
|
||||
|
||||
async def _do_await(self) -> PythonJSConvertedTypes:
|
||||
loop = get_running_loop()
|
||||
future: Future[PythonJSConvertedTypes] = loop.create_future()
|
||||
|
||||
def future_caller(value: Any) -> None:
|
||||
loop.call_soon_threadsafe(future.set_result, value)
|
||||
|
||||
self._attach_callbacks_to_promise(future_caller)
|
||||
|
||||
results = await future
|
||||
return self._unpack_promise_results(results)
|
||||
|
||||
def _attach_callbacks_to_promise(
|
||||
self,
|
||||
future_caller: Callable[[Any], None],
|
||||
) -> None:
|
||||
"""Attach the given Python callbacks to a JS Promise."""
|
||||
|
||||
exit_stack = ExitStack()
|
||||
|
||||
def on_resolved_and_cleanup(
|
||||
value: PythonJSConvertedTypes | JSEvalException,
|
||||
) -> None:
|
||||
exit_stack.__exit__(None, None, None)
|
||||
future_caller([False, cast(JSArray, value)])
|
||||
|
||||
on_resolved_js_func = exit_stack.enter_context(
|
||||
self._ctx.js_callback(on_resolved_and_cleanup)
|
||||
)
|
||||
|
||||
def on_rejected_and_cleanup(
|
||||
value: PythonJSConvertedTypes | JSEvalException,
|
||||
) -> None:
|
||||
exit_stack.__exit__(None, None, None)
|
||||
future_caller([True, cast(JSArray, value)])
|
||||
|
||||
on_rejected_js_func = exit_stack.enter_context(
|
||||
self._ctx.js_callback(on_rejected_and_cleanup)
|
||||
)
|
||||
|
||||
self._ctx.promise_then(self, on_resolved_js_func, on_rejected_js_func)
|
||||
|
||||
def _unpack_promise_results(self, results: Any) -> PythonJSConvertedTypes:
|
||||
is_rejected, argv = results
|
||||
result = cast(PythonJSConvertedTypes, cast(JSArray, argv)[0])
|
||||
if is_rejected:
|
||||
raise JSPromiseError(result)
|
||||
return result
|
||||
@@ -0,0 +1,49 @@
|
||||
"""An implementation of setTimeout and clearTimeout using only the ECMA standard lib.
|
||||
|
||||
V8 does not include an implementation of the setTimeout or clearTimeout APIs, because
|
||||
they are web APIs (defined in the DOM and Web Worker spec), not ECMAScript APIs.
|
||||
Nonetheless they are hugely useful and generally taken for granted, so let's go ahead
|
||||
and install a basic implementation.
|
||||
|
||||
We base this on the Atomics.waitAsync API, which *is* part of the ECMAScript spec.
|
||||
(It would be possible for MiniRacer to implement this on the C++ side instead, but
|
||||
this way seems simpler.)
|
||||
"""
|
||||
|
||||
INSTALL_SET_TIMEOUT = """
|
||||
class __TimerManager {
|
||||
constructor() {
|
||||
this.next_idx = 0;
|
||||
this.pending = new Set();
|
||||
}
|
||||
|
||||
setTimeout(func, delay) {
|
||||
const id = this.next_idx++;
|
||||
|
||||
const shared = new SharedArrayBuffer(8);
|
||||
const view = new Int32Array(shared);
|
||||
|
||||
const args = Array.prototype.slice.call(arguments, 2);
|
||||
|
||||
let callback = () => {
|
||||
if (this.pending.has(id)) {
|
||||
this.pending.delete(id);
|
||||
func(...args);
|
||||
}
|
||||
};
|
||||
|
||||
this.pending.add(id);
|
||||
Atomics.waitAsync(view, 0, 0, delay).value.then(() => callback());
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
clearTimeout(timeout_id) {
|
||||
this.pending.delete(timeout_id);
|
||||
}
|
||||
}
|
||||
|
||||
var __timer_manager = new __TimerManager();
|
||||
var setTimeout = (...arguments) => __timer_manager.setTimeout(...arguments);
|
||||
var clearTimeout = (...arguments) => __timer_manager.clearTimeout(...arguments);
|
||||
"""
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from threading import Condition
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from py_mini_racer._types import JSTimeoutException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from py_mini_racer._numeric import Numeric
|
||||
from py_mini_racer._types import PythonJSConvertedTypes
|
||||
|
||||
|
||||
class SyncFuture:
|
||||
"""A blocking synchronization object for function return values.
|
||||
|
||||
This is like asyncio.Future but blocking, or like
|
||||
concurrent.futures.Future but without an executor.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cv = Condition()
|
||||
self._settled: bool = False
|
||||
self._res: PythonJSConvertedTypes = None
|
||||
self._exc: Exception | None = None
|
||||
|
||||
def get(self, *, timeout: Numeric | None = None) -> PythonJSConvertedTypes:
|
||||
with self._cv:
|
||||
while not self._settled:
|
||||
if not self._cv.wait(timeout=timeout):
|
||||
raise JSTimeoutException
|
||||
|
||||
if self._exc:
|
||||
raise self._exc
|
||||
return self._res
|
||||
|
||||
def set_result(self, res: PythonJSConvertedTypes) -> None:
|
||||
with self._cv:
|
||||
self._res = res
|
||||
self._settled = True
|
||||
self._cv.notify()
|
||||
|
||||
def set_exception(self, exc: Exception) -> None:
|
||||
with self._cv:
|
||||
self._exc = exc
|
||||
self._settled = True
|
||||
self._cv.notify()
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Declarations for Python renderings of basic JavaScript types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
class JSUndefinedType:
|
||||
"""The JavaScript undefined type.
|
||||
|
||||
Where JavaScript null is represented as None, undefined is represented as this
|
||||
type."""
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "JSUndefined"
|
||||
|
||||
|
||||
JSUndefined = JSUndefinedType()
|
||||
|
||||
|
||||
class JSObject:
|
||||
pass
|
||||
|
||||
|
||||
PythonJSConvertedTypes = Union[
|
||||
None,
|
||||
JSUndefinedType,
|
||||
bool,
|
||||
int,
|
||||
float,
|
||||
str,
|
||||
JSObject,
|
||||
datetime,
|
||||
memoryview,
|
||||
]
|
||||
|
||||
|
||||
class MiniRacerBaseException(Exception): # noqa: N818
|
||||
"""Base MiniRacer exception."""
|
||||
|
||||
|
||||
class JSEvalException(MiniRacerBaseException):
|
||||
"""JavaScript could not be executed."""
|
||||
|
||||
|
||||
class JSTimeoutException(JSEvalException):
|
||||
"""JavaScript execution timed out."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("JavaScript was terminated by timeout")
|
||||
@@ -0,0 +1,278 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
from datetime import datetime, timezone
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
ClassVar,
|
||||
)
|
||||
|
||||
from py_mini_racer._abstract_context import AbstractContext, AbstractValueHandle
|
||||
from py_mini_racer._objects import (
|
||||
JSArray,
|
||||
JSFunction,
|
||||
JSMappedObject,
|
||||
JSObjectImpl,
|
||||
JSPromise,
|
||||
JSSymbol,
|
||||
)
|
||||
from py_mini_racer._types import (
|
||||
JSEvalException,
|
||||
JSUndefined,
|
||||
MiniRacerBaseException,
|
||||
PythonJSConvertedTypes,
|
||||
)
|
||||
|
||||
|
||||
class _RawValueUnion(ctypes.Union):
|
||||
_fields_: ClassVar[list[tuple[str, object]]] = [
|
||||
("value_ptr", ctypes.c_void_p),
|
||||
("bytes_val", ctypes.POINTER(ctypes.c_char)),
|
||||
("char_p_val", ctypes.c_char_p),
|
||||
("int_val", ctypes.c_int64),
|
||||
("double_val", ctypes.c_double),
|
||||
]
|
||||
|
||||
|
||||
class _RawValue(ctypes.Structure):
|
||||
_fields_: ClassVar[list[tuple[str, object]]] = [
|
||||
("value", _RawValueUnion),
|
||||
("len", ctypes.c_size_t),
|
||||
("type", ctypes.c_uint8),
|
||||
]
|
||||
_pack_ = 1
|
||||
|
||||
|
||||
RawValueHandle = ctypes.POINTER(_RawValue)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
RawValueHandleType = ctypes._Pointer[_RawValue] # noqa: SLF001
|
||||
|
||||
|
||||
class _ArrayBufferByte(ctypes.Structure):
|
||||
# Cannot use c_ubyte directly because it uses <B
|
||||
# as an internal type but we need B for memoryview.
|
||||
_fields_: ClassVar[list[tuple[str, object]]] = [
|
||||
("b", ctypes.c_ubyte),
|
||||
]
|
||||
_pack_ = 1
|
||||
|
||||
|
||||
class JSParseException(JSEvalException):
|
||||
"""JavaScript could not be parsed."""
|
||||
|
||||
|
||||
class JSKeyError(JSEvalException, KeyError):
|
||||
"""No such key found."""
|
||||
|
||||
|
||||
class JSOOMException(JSEvalException):
|
||||
"""JavaScript execution ran out of memory."""
|
||||
|
||||
|
||||
class JSTerminatedException(JSEvalException):
|
||||
"""JavaScript execution terminated."""
|
||||
|
||||
|
||||
class JSValueError(JSEvalException, ValueError):
|
||||
"""Bad value passed to JavaScript engine."""
|
||||
|
||||
|
||||
class JSConversionException(MiniRacerBaseException):
|
||||
"""Type could not be converted to or from JavaScript."""
|
||||
|
||||
|
||||
class MiniRacerTypes:
|
||||
"""MiniRacer types identifier
|
||||
|
||||
Note: it needs to be coherent with mini_racer.cc.
|
||||
"""
|
||||
|
||||
invalid = 0
|
||||
null = 1
|
||||
bool = 2
|
||||
integer = 3
|
||||
double = 4
|
||||
str_utf8 = 5
|
||||
array = 6
|
||||
# deprecated:
|
||||
hash = 7
|
||||
date = 8
|
||||
symbol = 9
|
||||
object = 10
|
||||
undefined = 11
|
||||
|
||||
function = 100
|
||||
shared_array_buffer = 101
|
||||
array_buffer = 102
|
||||
promise = 103
|
||||
|
||||
execute_exception = 200
|
||||
parse_exception = 201
|
||||
oom_exception = 202
|
||||
timeout_exception = 203
|
||||
terminated_exception = 204
|
||||
value_exception = 205
|
||||
key_exception = 206
|
||||
|
||||
|
||||
_ERRORS: dict[int, tuple[type[JSEvalException], str]] = {
|
||||
MiniRacerTypes.parse_exception: (
|
||||
JSParseException,
|
||||
"Unknown JavaScript error during parse",
|
||||
),
|
||||
MiniRacerTypes.execute_exception: (
|
||||
JSEvalException,
|
||||
"Uknown JavaScript error during execution",
|
||||
),
|
||||
MiniRacerTypes.oom_exception: (JSOOMException, "JavaScript memory limit reached"),
|
||||
MiniRacerTypes.terminated_exception: (
|
||||
JSTerminatedException,
|
||||
"JavaScript was terminated",
|
||||
),
|
||||
MiniRacerTypes.key_exception: (
|
||||
JSKeyError,
|
||||
"No such key found in object",
|
||||
),
|
||||
MiniRacerTypes.value_exception: (
|
||||
JSValueError,
|
||||
"Bad value passed to JavaScript engine",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ValueHandle(AbstractValueHandle):
|
||||
"""An object which holds open a Python reference to a _RawValue owned by
|
||||
a C++ MiniRacer context."""
|
||||
|
||||
def __init__(self, ctx: AbstractContext, raw: RawValueHandleType):
|
||||
self.ctx = ctx
|
||||
self._raw = raw
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.ctx.free(self)
|
||||
|
||||
@property
|
||||
def raw(self) -> RawValueHandleType:
|
||||
return self._raw
|
||||
|
||||
def to_python_or_raise(self) -> PythonJSConvertedTypes:
|
||||
val = self.to_python()
|
||||
if isinstance(val, JSEvalException):
|
||||
raise val
|
||||
return val
|
||||
|
||||
def to_python(self) -> PythonJSConvertedTypes | JSEvalException:
|
||||
"""Convert a binary value handle from the C++ side into a Python object."""
|
||||
|
||||
# A MiniRacer binary value handle is a pointer to a structure which, for some
|
||||
# simple types like ints, floats, and strings, is sufficient to describe the
|
||||
# data, enabling us to convert the value immediately and free the handle.
|
||||
|
||||
# For more complex types, like Objects and Arrays, the handle is just an opaque
|
||||
# pointer to a V8 object. In these cases, we retain the binary value handle,
|
||||
# wrapping it in a Python object. We can then use the handle in follow-on API
|
||||
# calls to work with the underlying V8 object.
|
||||
|
||||
# In either case the handle is owned by the C++ side. It's the responsibility
|
||||
# of the Python side to call mr_free_value() when done with with the handle
|
||||
# to free up memory, but the C++ side will eventually free it on context
|
||||
# teardown either way.
|
||||
|
||||
typ = self._raw.contents.type
|
||||
val = self._raw.contents.value
|
||||
length = self._raw.contents.len
|
||||
|
||||
error_info = _ERRORS.get(self._raw.contents.type)
|
||||
if error_info:
|
||||
klass, generic_msg = error_info
|
||||
|
||||
msg = val.bytes_val[0:length].decode("utf-8")
|
||||
msg = msg or generic_msg
|
||||
|
||||
return klass(msg)
|
||||
|
||||
if typ == MiniRacerTypes.null:
|
||||
return None
|
||||
if typ == MiniRacerTypes.undefined:
|
||||
return JSUndefined
|
||||
if typ == MiniRacerTypes.bool:
|
||||
return bool(val.int_val == 1)
|
||||
if typ == MiniRacerTypes.integer:
|
||||
return int(val.int_val)
|
||||
if typ == MiniRacerTypes.double:
|
||||
return float(val.double_val)
|
||||
if typ == MiniRacerTypes.str_utf8:
|
||||
return str(val.bytes_val[0:length].decode("utf-8"))
|
||||
if typ == MiniRacerTypes.function:
|
||||
return JSFunction(self.ctx, self)
|
||||
if typ == MiniRacerTypes.date:
|
||||
timestamp = val.double_val
|
||||
# JS timestamps are milliseconds. In Python we are in seconds:
|
||||
return datetime.fromtimestamp(timestamp / 1000.0, timezone.utc)
|
||||
if typ == MiniRacerTypes.symbol:
|
||||
return JSSymbol(self.ctx, self)
|
||||
if typ in (MiniRacerTypes.shared_array_buffer, MiniRacerTypes.array_buffer):
|
||||
buf = _ArrayBufferByte * length
|
||||
cdata = buf.from_address(val.value_ptr)
|
||||
# Save a reference to ourselves to prevent garbage collection of the
|
||||
# backing store:
|
||||
cdata._origin = self # noqa: SLF001
|
||||
result = memoryview(cdata)
|
||||
# Avoids "NotImplementedError: memoryview: unsupported format T{<B:b:}"
|
||||
# in Python 3.12:
|
||||
return result.cast("B")
|
||||
|
||||
if typ == MiniRacerTypes.promise:
|
||||
return JSPromise(self.ctx, self)
|
||||
|
||||
if typ == MiniRacerTypes.array:
|
||||
return JSArray(self.ctx, self)
|
||||
|
||||
if typ == MiniRacerTypes.object:
|
||||
return JSMappedObject(self.ctx, self)
|
||||
|
||||
raise JSConversionException
|
||||
|
||||
|
||||
def python_to_value_handle(
|
||||
context: AbstractContext, obj: PythonJSConvertedTypes
|
||||
) -> AbstractValueHandle:
|
||||
if isinstance(obj, JSObjectImpl):
|
||||
# JSObjects originate from the V8 side. We can just send back the handle
|
||||
# we originally got. (This also covers derived types JSFunction, JSSymbol,
|
||||
# JSPromise, and JSArray.)
|
||||
return obj.raw_handle
|
||||
|
||||
if obj is None:
|
||||
return context.create_intish_val(0, MiniRacerTypes.null)
|
||||
if obj is JSUndefined:
|
||||
return context.create_intish_val(0, MiniRacerTypes.undefined)
|
||||
if isinstance(obj, bool):
|
||||
return context.create_intish_val(1 if obj else 0, MiniRacerTypes.bool)
|
||||
if isinstance(obj, int):
|
||||
if obj - 2**31 <= obj < 2**31:
|
||||
return context.create_intish_val(obj, MiniRacerTypes.integer)
|
||||
|
||||
# We transmit ints as int32, so "upgrade" to double upon overflow.
|
||||
# (ECMAScript numeric is double anyway, but V8 does internally distinguish
|
||||
# int types, so we try and preserve integer-ness for round-tripping
|
||||
# purposes.)
|
||||
# JS BigInt would be a closer representation of Python int, but upgrading
|
||||
# to BigInt would probably be surprising for most applications, so for now,
|
||||
# we approximate with double:
|
||||
return context.create_doublish_val(obj, MiniRacerTypes.double)
|
||||
if isinstance(obj, float):
|
||||
return context.create_doublish_val(obj, MiniRacerTypes.double)
|
||||
if isinstance(obj, str):
|
||||
return context.create_string_val(obj, MiniRacerTypes.str_utf8)
|
||||
if isinstance(obj, datetime):
|
||||
# JS timestamps are milliseconds. In Python we are in seconds:
|
||||
return context.create_doublish_val(
|
||||
obj.timestamp() * 1000.0, MiniRacerTypes.date
|
||||
)
|
||||
|
||||
# Note: we skip shared array buffers, so for now at least, handles to shared
|
||||
# array buffers can only be transmitted from JS to Python.
|
||||
|
||||
raise JSConversionException
|
||||
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Reference in new issue
Block a user