stock-tracker

This commit is contained in:
C菌 committed 2026-07-04 00:17:11 +08:00
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
Whitespace-only changes.