Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pytest:

clean:
python setup.py clean
rm -rf pyduktape2.c build/ dist/ pyduktape2.egg-info/ .eggs/ *.so pyduktape2.html cython_debug .pytest_cache
rm -rf pyduktape2/_pyduktape2.c build/ dist/ pyduktape2.egg-info/ .eggs/ *.so pyduktape2.html cython_debug .pytest_cache

build:
python setup.py build_ext --inplace
Expand Down
3 changes: 3 additions & 0 deletions pyduktape2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._pyduktape2 import *

__version__ = "0.5.0"
46 changes: 46 additions & 0 deletions pyduktape2/_pyduktape2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations
from typing import Any, AnyStr
from pathlib import Path

DUK_TYPE_NONE: int = ...
DUK_TYPE_UNDEFINED: int = ...
DUK_TYPE_NULL: int = ...
DUK_TYPE_BOOLEAN: int = ...
DUK_TYPE_NUMBER: int = ...
DUK_TYPE_STRING: int = ...
DUK_TYPE_OBJECT: int = ...
DUK_TYPE_BUFFER: int = ...
DUK_TYPE_POINTER: int = ...
DUK_TYPE_LIGHTFUNC: int = ...
DUK_ENUM_OWN_PROPERTIES_ONLY: int = ...
DUK_VARARGS: int = ...
DUK_ERR_ERROR: int = ...

class DuktapeError(Exception): ...
class DuktapeThreadError(DuktapeError): ...
class JSError(Exception): ...

class DuktapeContext:
def _check_thread(self) -> None: ...
def set_globals(self, **kwargs) -> None: ...
def get_global(self, name: str | bytes): ...
def set_base_path(self, path: str | bytes | Path) -> None: ...
def eval_js(self, src: str | bytes) -> object: ...
def eval_js_file(self, src_path: str) -> None: ...
def get_file_path(self, src_path: str) -> str: ...
def make_jsref(self, index: int) -> JSRef: ...

class JSRef(object):
def __init__(self, py_ctx: DuktapeContext, ref_index: int) -> None: ...
def to_js(self) -> None: ...
def __del__(self) -> None: ...

class JSProxy(object):
def __init__(self, ref: JSRef, bind_proxy: J) -> None: ...
def __setattr__(self, name:str, value:object) -> None: ...
def __getattr__(self, name:str): ...
def __getitem__(self, name:str) -> Any: ...
def __repr__(self) -> str: ...
def __call__(self, *args): ...
def new(self, *args): ...

124 changes: 75 additions & 49 deletions pyduktape2.pyx → pyduktape2/_pyduktape2.pyx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#cython: language_level=3
# cython: language_level = 3, freethreading_compatible = True
import contextlib
import os
import threading
import traceback
from cpython.unicode cimport PyUnicode_Check
from cpython.exc cimport PyErr_SetNone

cdef extern from "Python.h":
unsigned long PyThread_get_thread_ident()


DUK_TYPE_NONE = 0
Expand Down Expand Up @@ -110,28 +114,27 @@ cdef extern from 'vendor/duk_module_duktape.c':


cdef extern from 'vendor/duk_print_alert.c':
ctypedef unsigned int duk_uint_t
ctypedef struct duk_context:
pass

cdef void duk_print_alert_init(duk_context *ctx, duk_uint_t flags)


class DuktapeError(Exception):
# NOTE: It's better that these be C-Extensions for increased performance reasons...
cdef class DuktapeError(Exception):
pass


class DuktapeThreadError(DuktapeError):
cdef class DuktapeThreadError(DuktapeError):
pass


class JSError(Exception):
cdef class JSError(Exception):
pass


cdef class DuktapeContext(object):
cdef duk_context *ctx
cdef object thread_id

cdef object js_base_path
# index into the global js stash
# when a js value is returned to python,
Expand All @@ -140,12 +143,15 @@ cdef class DuktapeContext(object):
cdef int next_ref_index

# these keep python objects referenced only by js code alive
cdef object registered_objects
cdef object registered_proxies
cdef object registered_proxies_reverse
cdef dict registered_objects
cdef dict registered_proxies
cdef dict registered_proxies_reverse

# Thread id
cdef unsigned long thread_id

def __cinit__(self):
self.thread_id = threading.current_thread().ident
self.thread_id = PyThread_get_thread_ident()
self.js_base_path = ''
self.next_ref_index = -1

Expand All @@ -169,12 +175,16 @@ cdef class DuktapeContext(object):
duk_put_prop_string(self.ctx, -2, b"modSearch")
duk_pop(self.ctx)

def _check_thread(self):
if threading.current_thread().ident != self.thread_id:
raise DuktapeThreadError()
# Nitpick, this should be internal and not public...
cdef inline int _check_thread(self) except -1:
if PyThread_get_thread_ident() != self.thread_id:
PyErr_SetNone(DuktapeThreadError)
return -1
return 0

def set_globals(self, **kwargs):
self._check_thread()
if self._check_thread() < 0:
raise

for name, value in kwargs.items():
self._set_global(name.encode(), value)
Expand All @@ -184,7 +194,7 @@ cdef class DuktapeContext(object):
duk_put_global_string(self.ctx, name)

def get_global(self, name):
if not isinstance(name, str):
if not PyUnicode_Check(name):
raise TypeError('Global variable name must be a string, {} found'.format(type(name)))

duk_get_global_string(self.ctx, name.encode())
Expand All @@ -196,7 +206,7 @@ cdef class DuktapeContext(object):
return value

def set_base_path(self, path):
if not isinstance(path, str):
if not PyUnicode_Check(path):
raise TypeError('Path must be a string, {} found'.format(type(path)))

self.js_base_path = path
Expand All @@ -206,19 +216,16 @@ cdef class DuktapeContext(object):
src = src.encode()

if not isinstance(src, bytes):
raise TypeError('Javascript source must be a string')

def eval_string():
return duk_peval_string(self.ctx, src)
raise TypeError('Javascript source must be a string or bytes object')

return self._eval_js(eval_string)
return self._eval_js(src)

def eval_js_file(self, src_path):
src_path = str(src_path)
with open(self.get_file_path(src_path), 'rb') as f:
code = f.read()

return self.eval_js(code)
return self._eval_js(code)

def get_file_path(self, src_path):
if not src_path.endswith('.js'):
Expand All @@ -229,10 +236,11 @@ cdef class DuktapeContext(object):

return src_path

def _eval_js(self, eval_function):
self._check_thread()
cdef object _eval_js(self, bytes src):
if self._check_thread() < 0:
raise

if eval_function() != 0:
if duk_peval_string(self.ctx, src) != 0:
error = self.get_error()
duk_pop(self.ctx)
result = None
Expand All @@ -254,8 +262,9 @@ cdef class DuktapeContext(object):

return error

def make_jsref(self, duk_idx_t index):
self._check_thread()
cpdef JSRef make_jsref(self, duk_idx_t index):
if self._check_thread() < 0:
raise

assert duk_is_object(self.ctx, index)

Expand Down Expand Up @@ -331,8 +340,9 @@ cdef class JSRef(object):
self.py_ctx = py_ctx
self.ref_index = ref_index

def to_js(self):
self.py_ctx._check_thread()
cpdef to_js(self):
if self.py_ctx._check_thread() < 0:
raise

duk_push_global_stash(self.py_ctx.ctx)
if duk_get_prop_index(self.py_ctx.ctx, -1, self.ref_index) == 0:
Expand All @@ -351,11 +361,18 @@ cdef class JSRef(object):
duk_pop(self.py_ctx.ctx)


ctypedef duk_ret_t (*callfunc)(duk_context *, duk_idx_t)

ctypedef fused PROXY_OR_NONE:
object
JSProxy


cdef class JSProxy(object):
cdef JSRef __ref
cdef JSProxy __bind_proxy
cdef object __bind_proxy

def __init__(self, JSRef ref, bind_proxy):
def __init__(self, JSRef ref, PROXY_OR_NONE bind_proxy):
ref.py_ctx._check_thread()

self.__ref = ref
Expand Down Expand Up @@ -388,7 +405,7 @@ cdef class JSProxy(object):

return res

def __getitem__(self, name):
def __getitem__(self, object name):
self.__ref.py_ctx._check_thread()

if not isinstance(name, (int, str)):
Expand All @@ -397,32 +414,39 @@ cdef class JSProxy(object):
return getattr(self, str(name))

def __repr__(self):
self.__ref.py_ctx._check_thread()
cdef duk_context* ctx
if self.__ref.py_ctx._check_thread() < 0:
raise

ctx = self.__ref.py_ctx.ctx

self.__ref.to_js()
res = duk_safe_to_string(ctx, -1)
res = <bytes>duk_safe_to_string(ctx, -1)
duk_pop(ctx)

return '<JSProxy: {}, bind_proxy={}>'.format(res.decode(), self.__bind_proxy.__repr__())

def __call__(self, *args):
self.__ref.py_ctx._check_thread()
if self.__ref.py_ctx._check_thread() < 0:
raise

if self.__bind_proxy is None:
return self.__call(duk_pcall, args, None)
else:
return self.__call(duk_pcall_method, args, self.__bind_proxy)

def new(self, *args):
self.__ref.py_ctx._check_thread()
if self.__ref.py_ctx._check_thread() < 0:
raise

return self.__call(safe_new, args, None)

cdef __call(self, duk_ret_t (*call_type)(duk_context *, duk_idx_t), args, this):
self.__ref.py_ctx._check_thread()
cdef object __call(self, callfunc call_type, tuple args, this):
cdef object arg

if self.__ref.py_ctx._check_thread() < 0:
raise

ctx = self.__ref.py_ctx.ctx

self.__ref.to_js()
Expand All @@ -449,19 +473,21 @@ cdef class JSProxy(object):

return res

def __nonzero__(self):
self.__ref.py_ctx._check_thread()
# XXX: __nonzero__ removed in Python 3...
def __bool__(self):
if self.__ref.py_ctx._check_thread() < 0:
raise

return getattr(self, 'length', 1) > 0

def __len__(self):
self.__ref.py_ctx._check_thread()

if self.__ref.py_ctx._check_thread() < 0:
raise
return self.length

def __iter__(self):
self.__ref.py_ctx._check_thread()

if self.__ref.py_ctx._check_thread() < 0:
raise
ctx = self.__ref.py_ctx.ctx

self.__ref.to_js()
Expand All @@ -484,9 +510,9 @@ cdef class JSProxy(object):
for key in keys:
yield key

def to_js(self):
self.__ref.py_ctx._check_thread()

cpdef to_js(self):
if self.__ref.py_ctx._check_thread() < 0:
raise
self.__ref.to_js()


Expand Down
Empty file added pyduktape2/py.typed
Empty file.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[
Extension(
'pyduktape2',
['pyduktape2.pyx'],
['pyduktape2/_pyduktape2.pyx'],
include_dirs=['vendor'],
)
],
Expand Down