diff --git a/cyjs/__init__.py b/cyjs/__init__.py index 9e857e7..7e4db62 100644 --- a/cyjs/__init__.py +++ b/cyjs/__init__.py @@ -1,6 +1,7 @@ from ._cyjs import ( CancelledError, Context, + JSClass, JSError, JSFunction, Object, @@ -15,6 +16,7 @@ __all__ = ( "CancelledError", "Context", + "JSClass", "JSError", "JSFunction", "Object", diff --git a/cyjs/_cyjs.pxd b/cyjs/_cyjs.pxd index 690612e..cc74d86 100644 --- a/cyjs/_cyjs.pxd +++ b/cyjs/_cyjs.pxd @@ -43,6 +43,22 @@ cdef extern from "bridge.h": bint promise ) + # Shortcuts for Making global variables, these are not + # in quickjs's header file but are useful to have here + int CYJS_NewGlobalCConstructor2(JSContext *ctx, + JSValue func_obj, + const char *name, + JSValue proto) + + JSValue CYJS_NewGlobalCConstructor(JSContext *ctx, const char *name, JSCFunctionMagic func, int length, JSValue proto, int magic) + ctypedef JSValue (*cyjs_get)(JSContext *ctx, JSValue this_val, int magic) noexcept with gil + ctypedef JSValue (*cyjs_set)(JSContext *ctx, JSValue this_val, JSValue value, int magic ) noexcept with gil + JSCFunctionListEntry* CYJS_MakeJSCFunctionListEntries( + list names, + cyjs_get get, + cyjs_set set + ) except NULL + cdef class JSError(Exception): @staticmethod cdef JSError new(JSContext* ctx, JSValue value) @@ -102,7 +118,7 @@ cdef class PromiseHook: - + cdef class Runtime: """ @@ -141,8 +157,13 @@ cdef class Runtime: cpdef void update_statck_top(self) cpdef object set_promise_hook(self, object func) - - + cpdef JSClass new_class( + self, + object py_type, + object name =*, + object attrs=* + ) + cdef class Object: cdef: @@ -192,11 +213,53 @@ cdef class JSFunction: cdef JSValue call_js(self, JSContext *ctx, JSValue this_val, int argc, JSValue *argv, int magic) noexcept +# Inspired by pyduktape +cdef class JSRef: + """Used for acting as a bridge between Javascript and Python attributes""" + cdef: + readonly Context context + JSContext* ctx + JSValue value + object ref + list slots # list of exposable attributes to chain to JS_CGETSET_MAGIC_DEF + + + @staticmethod + cdef JSRef new(Context context, JSValue value, object ref, list slots) + + # cdef int has(self, JSAtom at) except -1 + # cdef JSValue get(self, JSAtom at) + + +cdef class JSClass: + """Enables Class Creation (Mostly internal)""" + cdef: + readonly Runtime runtime + readonly JSClassID id # expose to python for debugging + object py_type + JSClassDef cls_def + list properties + JSCFunctionListEntry* entries + + @staticmethod + cdef JSClass new( + Runtime runtime, + object py_type, + object class_name=*, + list properties=* + ) + + + + + + # This helps with benchmarking and eliminating a few options when # crunching some more obvious cases... ctypedef fused quickjs_type_t: JSFunction + JSRef Object Exception dict @@ -215,7 +278,11 @@ cdef class Context: JSContext* ctx # incase a hook of some kind with a void happens to throw an exception we can capture it. object _cb_exception - + # This will allow us to attempt to safely hook JSClassIDs to Python classes. + dict _pyjs_classes + # helps with figuring out if a type can be safely JSRef'd + set _registered_py_types + # NOTE: I'm Putting type ignores here because # C function "get_exception" is implemented in pxd definition of C class "Context" without the "inline" qualifier @@ -296,3 +363,10 @@ cdef class Context: bint promise =* ) + # Wraps functions and registers JSClass + # to start performing conversions with it. + cpdef object add_class( + self, + JSClass js_cls + ) + diff --git a/cyjs/_cyjs.pyi b/cyjs/_cyjs.pyi index 964a2ca..cd9ef38 100644 --- a/cyjs/_cyjs.pyi +++ b/cyjs/_cyjs.pyi @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Iterable from enum import IntEnum from typing import Any, Generic, ParamSpec, TypeVar @@ -68,6 +68,24 @@ class Runtime: self, func: Callable[["Context", PromiseHookType, Promise, Promise | None], None], ) -> object: ... + def new_class( + self, + py_type: type[_T], + name: str | bytes | bytearray | memoryview = ..., + attrs: Iterable[str] = ..., + ) -> JSClass[_T]: + """Registers a python class to bind with quickjs + + :param py_type: the python type to bind with. + :param name: an alternative name to provide \ + to the given type object. The Default name \ + will get derrived from py_type if name is None. + :param attrs: an iterable of readable public properties \ + that this class should allow quickjs to be able to have \ + access to and read. \ + NOTE: functions and methods haven't been implemented yet \ + but might be planned in a future update. + """ class _OView: def __init__(self, obj: Object) -> None: ... @@ -137,6 +155,8 @@ class Object: """evaluates javascript module code""" ... +# TODO: Provide Generic typehinting capabilites in the actual cython code. +# Like how frozenlist does it or through a simillar mechanism... class JSFunction(Generic[_P, _T]): context: Context @@ -145,7 +165,19 @@ class JSFunction(Generic[_P, _T]): def object(self) -> Object: pass +class JSClass(Generic[_T]): + runtime: Runtime + id: int + + @property + def type(self) -> type[_T]: + """returns the original type provided to be binded to quickjs""" + @property + def name(self) -> str: + """provides the name of the given JSClass through it's JSClassDef structure""" + class Context: + runtime: Runtime def __init__( self, runtime: Runtime = ..., @@ -232,6 +264,14 @@ class Context: backtrace_barrier: bool = ..., promise: bool = ..., ) -> Any: ... + def add_class(self, js_cls: JSClass[Any]): + """ + binds a JSClass Globally to globalThis + :param js_cls: the Javascript Class to bind \ + these can be created via obtaining the runtime \ + attribute of this context \ + as a shortcut and calling `runtime.new_class(...)` beforehand + """ class CancelledError(Exception): """Promise was rejected""" diff --git a/cyjs/_cyjs.pyx b/cyjs/_cyjs.pyx index 28770c2..4f72d25 100644 --- a/cyjs/_cyjs.pyx +++ b/cyjs/_cyjs.pyx @@ -7,8 +7,10 @@ from cpython.exc cimport (PyErr_CheckSignals, PyErr_NoMemory, PyErr_Occurred, from cpython.list cimport PyList_AsTuple from cpython.long cimport PyLong_AsLongAndOverflow, PyLong_FromString from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc -from cpython.object cimport PyObject_CallObject, PyObject_Str +from cpython.object cimport PyObject_CallObject, PyObject_Str, Py_TYPE, PyObject_GetAttr, PyObject_SetAttr, PyObject_HasAttrString +from cpython.list cimport PyList_GET_SIZE from cpython.tuple cimport PyTuple_GET_SIZE +from cpython.type cimport PyType_Check from cpython.unicode cimport PyUnicode_FromString, PyUnicode_FromStringAndSize from .quickjs cimport * @@ -20,6 +22,8 @@ cdef extern from "Python.h": # hacky REFs to not need a linked list setup like with the old quickjs library void Py_XDECREF(object) void Py_XINCREF(object) + object PyList_GET_ITEM(object p, Py_ssize_t pos) + @@ -198,6 +202,26 @@ cdef class Runtime: self.has_promise_hook = True JS_SetPromiseHook(self.rt, on_promise_hook, self.promise_hook) + cpdef JSClass new_class( + self, + object py_type, + object name = None, + object attrs = [] + ): + """Registers a python class to bind with quickjs + :param py_type: the python type to bind with. + :param name: an alternative name to provide \ + to the given type object. The Default name \ + will get derrived from py_type if name is None. + :param attrs: an iterable of readable public properties + that this class should allow quickjs to be able to have access to and read. NOTE: class methods + haven't been implemented yet but might be planned + in a future update... + """ + if not PyType_Check(py_type): + raise TypeError("py_type must be a type object.") + return JSClass.new(self, py_type, name, list(attrs)) + def __dealloc__(self): if self.rt != NULL: @@ -297,6 +321,9 @@ cdef int to_quickjs(JSContext* ctx, JSValue* val, quickjs_type_t obj) noexcept: if quickjs_type_t is Object: val[0] = (obj).value return 0 + elif quickjs_type_t is JSRef: + val[0] = (obj).value + return 0 elif quickjs_type_t is JSFunction: val[0] = (obj).value return 0 @@ -339,7 +366,11 @@ cdef int to_quickjs(JSContext* ctx, JSValue* val, quickjs_type_t obj) noexcept: # very quick shortcut val[0] = (obj).value return 0 - + + elif isinstance(obj, JSRef): + val[0] = (obj).value + return 0 + elif isinstance(obj, Exception): # Convert exception val[0] = py_to_js_exception(ctx, obj) @@ -757,7 +788,239 @@ cdef class JSFunction: +cdef class JSRef: + """Used for acting as a bridge between Javascript and Python attributes""" + + @staticmethod + cdef JSRef new(Context context, JSValue value, object ref, list slots): + cdef JSRef self = JSRef.__new__(JSRef) + self.context = context + # faster shortcut + self.ctx = context.ctx + self.value = value + self.ref = ref + # Let Opaque value dictate how we perform closure... + Py_XINCREF(self) + JS_SetOpaque(self.value, self) + self.slots = slots + return self + + def __dealloc__(self): + JS_FreeValue(self.context.ctx, self.value) + + # cdef int has(self, JSAtom at) except -1: + # cdef const char* cstr = JS_AtomToCString(self.context.ctx, at) + # try: + # return PyObject_HasAttrString(self.ref, cstr) + # finally: + # JS_FreeCString(self.context.ctx, cstr) + + # cdef JSValue get(self, JSAtom at): + # cdef const char* cstr = JS_AtomToCString(self.context.ctx, at) + # cdef JSValue attr + # try: + # if to_quickjs(self.ctx, &attr, PyObject_GetAttrString( + # self.ref, cstr)) < 0: + # return JS_EXCEPTION + # return attr + # except BaseException as e: + # self.context._cb_exception = e + # return JS_EXCEPTION + # finally: + # JS_FreeCString(self.context.ctx, cstr) + + # cdef JSValue set(self, JSAtom at, ): + # cdef const char* cstr = JS_AtomToCString(self.context.ctx, at) + # cdef JSValue attr + # try: + # if to_quickjs(self.ctx, &attr, PyObject_GetAttrString( + # self.ref, cstr)) < 0: + # return JS_EXCEPTION + # return attr + # except BaseException as e: + # self.context._cb_exception = e + # return JS_EXCEPTION + # finally: + # JS_FreeCString(self.context.ctx, cstr) + + + + + + + +cdef inline JSRef get_jsref(JSValue value): + cdef JSClassID temp = 0 + return (JS_GetAnyOpaque(value, &temp)) + +# JSRef acts as a proxy of it's own creation this is to cheat +# a few things and allow for lazy development of python types... + + + + + +cdef void js_class_finalizer(JSRuntime* rt, JSValue value) noexcept with gil: + cdef JSRef ref = get_jsref(value) + Py_XDECREF(ref) + +cdef JSValue jsref_get(JSContext *ctx, JSValue this_val, int magic) noexcept with gil: + cdef JSRef ref = get_jsref(this_val) + cdef object ret + cdef JSValue val + try: + ret = PyObject_GetAttr(ref.ref, PyList_GET_ITEM(ref.slots, magic)) + if to_quickjs(ctx, &val, ret) < 0: + return JS_EXCEPTION + return val + except BaseException as e: + # Special Shortcut... + ref.context._cb_exception = e + return JS_EXCEPTION + +cdef JSValue jsref_set(JSContext *ctx, JSValue this_val, JSValue val, int magic) noexcept with gil: + cdef JSRef ref = get_jsref(this_val) + cdef object ret + try: + ret = to_python(ctx, val) + if PyObject_SetAttr(ref.ref, PyList_GET_ITEM(ref.slots, magic), ret) < 0: + return JS_EXCEPTION + return JS_UNDEFINED + except BaseException as e: + # Special Shortcut... + ref.context._cb_exception = e + return JS_EXCEPTION + + + # ctypedef JSValue (*cyjs_set)(JSContext *ctx, JSValue this_val, JSValue value, int magic ) + + +cdef class JSClass: + """Enables Class Creation (Mostly internal)""" + @staticmethod + cdef JSClass new( + Runtime runtime, + object py_type, + object class_name = None, + # NOTE: while enabling all items of a class to be + # exposed would be nice. There can also be security + # concerns with that so having it be user provided is a + # safer feature over all... + # list of attributes to expose to javascript + list properties = [] + ): + cdef object name + cdef Py_buffer view + cdef JSClass self = JSClass.__new__(JSClass) + + name = class_name or py_type.__name__ + self.runtime = runtime + self.id = 0 + self.py_type = py_type + + JS_NewClassID(runtime.rt, &self.id) + + if cyjs_get_buffer(name, &view) < 0: + raise + + self.cls_def.finalizer + self.cls_def.class_name = view.buf + if JS_NewClass(runtime.rt, self.id, &self.cls_def) < 0: + raise + + cyjs_release_buffer(&view) + + self.properties = properties + if properties: + self.entries = CYJS_MakeJSCFunctionListEntries( + properties, + jsref_get, + jsref_set + ) + + else: + self.entries = NULL + + return self + + @property + def type(self): + """returns the original type provided to be binded to quickjs""" + return self.py_type + + @property + def name(self): + """provides the name of the given JSClass through it's JSClassDef structure""" + return PyUnicode_FromString(self.cls_def.class_name) + + def __dealloc__(self): + if self.entries != NULL: + PyMem_Free(self.entries) + + + + + + + + + + + + + + + + + +cdef JSValue pyjs_constructor(JSContext* ctx, JSValue new_target, int argc, JSValue* argv, int magic) noexcept with gil: + # We need to retrace our object back for recovery of the ClassID + # Since our objects are dynamic and not what the Quickjs devs had intended + # it's purpose to be since were attempting to allow newly dynamic python classes + # and structures to be allowed through the system... + cdef Context context = JS_GetContextOpaque(ctx) + cdef JSClass js_cls + cdef JSValue obj, proto + cdef arg_parser_t parser + cdef object opaque + cdef JSRef js_ref + arg_parser_init(&parser, ctx, argv, argc, argc) + + try: + js_cls = context._pyjs_classes[magic] + opaque = PyObject_CallObject(js_cls.py_type, arg_parser_unpack(&parser)) + proto = JS_GetPropertyStr(ctx, new_target, "prototype") + if JS_IsException(proto): + return JS_EXCEPTION + # allow subclassing the python class + obj = JS_NewObjectProtoClass( + ctx, + proto, + js_cls.id + ) + if JS_IsException(obj): + return JS_EXCEPTION + js_ref = JSRef.new(context, obj, opaque, js_cls.properties) + return js_ref.value + except BaseException as e: + context._cb_exception = e + JS_FreeValue(ctx, obj) + return JS_EXCEPTION + finally: + arg_parser_finish(&parser) + + + + + + + + + + + + # cdef class InterruptHandler: @@ -818,7 +1081,9 @@ cdef class Context: # type: ignore self.runtime = runtime # Collection exception if seen in a void callback self._cb_exception = None - + self._pyjs_classes = {} + self._registered_py_types = set() + # Inlined # cdef bint has_exception(self): @@ -1022,7 +1287,11 @@ cdef class Context: # type: ignore finally: cyjs_release_buffer(&view) - + + + + + cdef object ceval_this( self, object code, @@ -1094,6 +1363,56 @@ cdef class Context: # type: ignore ): return self.ceval_this(code, this, filename, True, strict, backtrace_barrier, promise) + + cpdef object add_class( + self, + JSClass js_cls + ): + """ + binds a JSClass Globally + :param js_cls: the Javascript Class to bind, + these can be created via obtaining the runtime attribute of this context + as a shortcut and calling runtime.new_class(...) + """ + cdef JSValue js_cls_proto, obj + + self._pyjs_classes[js_cls.id] = js_cls + # for to_quickjs conversions (object route) + self._registered_py_types.add(js_cls.py_type) + + js_cls_proto = JS_NewObjectClass(self.ctx, js_cls.id) + if JS_IsException(js_cls_proto): + self.raise_exception() + raise + + obj = CYJS_NewGlobalCConstructor( + self.ctx, + js_cls.cls_def.class_name, + pyjs_constructor, + 1, + js_cls_proto, + # Needed otherwise chaining and stuff doesn't work... + js_cls.id + ) + + + if js_cls.entries != NULL: + if JS_SetPropertyFunctionList(self.ctx, js_cls_proto, js_cls.entries, PyList_GET_SIZE(js_cls.properties)) < 0: + self.raise_exception() + raise + + # TODO: JS_SetPropertyFunctionList... Might need a custom macro to make it... + + # Handle Object since we added safety mechanisms to our version of + # JS_NewGlobalCConstructor to ensure that we are 100% safe. + if JS_IsException(obj): + self.raise_exception() + raise + + return to_python(self.ctx, obj) + + + # TODO: Soon as I figure out how to make callbacks and promises work... # Another library called aiojs plans to be worked on work making python's asyncio # with quickjs work happily together. @@ -1258,10 +1577,17 @@ cdef Promise jsv_to_promise(JSContext* ctx, JSValue value): v.completed = False return v +cdef inline bint is_registerd_jsref(JSContext* ctx, JSValue value): + cdef Context context = JS_GetContextOpaque(ctx) + return JS_GetClassID(value) in context._pyjs_classes + +cdef inline object get_jsref_python_type(JSValue value): + return get_jsref(value).ref cdef object to_python(JSContext* ctx, JSValue value): cdef int32_t tag = JS_VALUE_GET_TAG(value) cdef object ret + if tag == JS_TAG_EXCEPTION: (JS_GetContextOpaque(ctx)).raise_exception() @@ -1269,6 +1595,8 @@ cdef object to_python(JSContext* ctx, JSValue value): elif tag == JS_TAG_MODULE or tag == JS_TAG_OBJECT or tag == JS_TAG_SYMBOL: + if is_registerd_jsref(ctx, value): + return get_jsref_python_type(value) if JS_IsPromise(value): return jsv_to_promise(ctx, value) return jsv_to_object(ctx, value) diff --git a/cyjs/bridge.h b/cyjs/bridge.h index 9d919ef..5ec79cc 100644 --- a/cyjs/bridge.h +++ b/cyjs/bridge.h @@ -298,28 +298,90 @@ cyjs_release_buffer(Py_buffer *view) { +// these functions are private in Quickjs but can prove useful when needing to expose a class object globally, +// Extremely good if your needing to mimic something like HTML5 Javascript for instance... +// These are slightly modified so we can proxy through python objects without problems. +// But also require strict exception handling incase anything bad happens that we didn't take into account. +static int CYJS_NewGlobalCConstructor2(JSContext *ctx, + JSValue func_obj, + const char *name, + JSValueConst proto) +{ + JSValue global_obj = JS_GetGlobalObject(ctx); + + if (JS_DefinePropertyValueStr(ctx, global_obj, name, + JS_DupValue(ctx, func_obj), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0){ + return -1; + }; -/// @brief Initalizes JSClassDef with help from a PyObject -/// @param obj The Python Object/Class or Cython C Extension object to intialize -/// @param cls The JSClass Definition -/// @param finalizer Optional Callback -/// @param gc_mark Optional Callback -/// @param call Optional Callback -// void CYJS_CreateJSClassDef( -// PyObject* obj, -// JSClassDef* cls, -// JSClassFinalizer *finalizer, -// JSClassGCMark* gc_mark, -// JSClassCall* call, -// ){ -// /* XXX: Cython is not very capable of this but is perfectly acceptable in C... */ -// cls->class_name = Py_TYPE(obj)->tp_name; -// cls->finalizer = finalizer; -// cls->gc_mark = gc_mark; -// cls->call = call; - -// } + JS_SetConstructor(ctx, func_obj, proto); + JS_FreeValue(ctx, func_obj); + return 0; +} +static JSValue CYJS_NewGlobalCConstructor(JSContext *ctx, const char *name, + JSCFunctionMagic *func, int length, + JSValueConst proto, int magic) +{ + JSValue func_obj; + func_obj = JS_NewCFunctionMagic(ctx, func, name, length, JS_CFUNC_constructor_or_func_magic, magic); + if (CYJS_NewGlobalCConstructor2(ctx, func_obj, name, proto) < 0){ + JS_FreeValue(ctx, func_obj); + return JS_EXCEPTION; + } + return func_obj; +} + +static JSValue CYJS_NewGlobalCConstructorOnly(JSContext *ctx, const char *name, + JSCFunctionMagic *func, int length, + JSValue proto, int magic) +{ + JSValue func_obj; + func_obj = JS_NewCFunctionMagic(ctx, func, name, length, JS_CFUNC_constructor_magic, magic); + if (CYJS_NewGlobalCConstructor2(ctx, func_obj, name, proto) < 0){ + JS_FreeValue(ctx, func_obj); + return JS_EXCEPTION; + } + return func_obj; +} + +typedef JSValue (*cyjs_get)(JSContext *ctx, JSValue this_val, int magic); +typedef JSValue (*cyjs_set)(JSContext *ctx, JSValue this_val, JSValue value, int magic); + +static JSCFunctionListEntry* CYJS_MakeJSCFunctionListEntries( + PyObject* names, + cyjs_get get, + cyjs_set set +){ + Py_buffer view; + Py_ssize_t size = PyList_GET_SIZE(names); + JSCFunctionListEntry* entries = PyMem_Malloc(sizeof(JSCFunctionListEntry) * size); + JSCFunctionListEntry e; + if (entries == NULL){ + PyErr_NoMemory(); + return NULL; + } + + for (Py_ssize_t i = 0; i < size; i++){ + PyObject* obj = PyList_GET_ITEM(names, i); + if (cyjs_get_buffer(obj, &view) < 0){ + PyMem_Free(entries); + return NULL; + } + /* let magic dictate what slot is given + we also need to expand macro due to C++ + being mean with us... */ + entries[i].name = view.buf; + entries[i].prop_flags = JS_PROP_CONFIGURABLE; + entries[i].def_type = JS_DEF_CGETSET_MAGIC; + entries[i].magic = (int16_t)i; + entries[i].u.getset.get.getter_magic = get; + entries[i].u.getset.set.setter_magic = set; + cyjs_release_buffer(&view); + } + return entries; +}; #ifdef __cplusplus } diff --git a/cyjs/quickjs.pxd b/cyjs/quickjs.pxd index 691c929..2d79ff0 100644 --- a/cyjs/quickjs.pxd +++ b/cyjs/quickjs.pxd @@ -28,8 +28,8 @@ cdef extern from "quickjs.h" nogil: JS_TAG_SHORT_BIG_INT = 7 JS_TAG_FLOAT64 = 8 - ctypedef struct JSClass: - pass + # ctypedef struct JSClass: + # pass ctypedef struct JSModuleDef: pass ctypedef struct JSGCObjectHeader: @@ -195,7 +195,7 @@ cdef extern from "quickjs.h" nogil: int (*has_property)(JSContext*, JSValue, JSAtom) JSValue (*get_property)(JSContext*, JSValue, JSAtom, JSValue) int (*set_property)(JSContext*, JSValue, JSAtom, JSValue, JSValue, int) - + ctypedef void (*JSClassFinalizer)(JSRuntime*, JSValue) noexcept with gil ctypedef void (*JSClassGCMark)(JSRuntime*, JSValue, JS_MarkFunc*) noexcept with gil ctypedef JSValue (*JSClassCall)(JSContext*, JSValue, JSValue, int, JSValue*, int) noexcept with gil @@ -358,6 +358,8 @@ cdef extern from "quickjs.h" nogil: void* JS_GetAnyOpaque(JSValue, JSClassID*) JSValue JS_ParseJSON(JSContext*, const char*, size_t, const char*) JSValue JS_UNDEFINED + JSValue JS_EXCEPTION + JSValue JS_JSONStringify(JSContext*, JSValue, JSValue, JSValue) ctypedef void (*JSFreeArrayBufferDataFunc)(JSRuntime*, void*, void*) JSValue JS_NewArrayBuffer(JSContext*, uint8_t*, size_t, JSFreeArrayBufferDataFunc*, void*, bint) @@ -475,8 +477,8 @@ cdef extern from "quickjs.h" nogil: JSValue JS_NewCFunctionData2(JSContext*, JSCFunctionData*, const char*, int, int, int, JSValue*) ctypedef void (*JSCClosureFinalizerFunc)(void*) noexcept with gil JSValue JS_NewCClosure(JSContext*, JSCClosure, const char*, JSCClosureFinalizerFunc, int, int, void*) - JSValue JS_NewCFunction(JSContext*, JSCFunction*, const char*, int) - JSValue JS_NewCFunctionMagic(JSContext*, JSCFunctionMagic*, const char*, int, JSCFunctionEnum, int) + JSValue JS_NewCFunction(JSContext*, JSCFunction, const char*, int) + JSValue JS_NewCFunctionMagic(JSContext*, JSCFunctionMagic, const char*, int, JSCFunctionEnum, int) void JS_SetConstructor(JSContext*, JSValue, JSValue) struct pxdgen_anon_pxdgen_anon_JSCFunctionListEntry_0_0: JSCFunctionListEntry* tab @@ -556,3 +558,6 @@ cdef extern from "quickjs.h" nogil: int JS_PROP_NORMAL int JS_PROP_GETSET + + + \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 02eea25..a5e70ac 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,2 +1,4 @@ # Will need test.txt if we want to run some tests --r test.txt \ No newline at end of file +-r test.txt +cython==3.2.4 +setuptools==80.9.0 diff --git a/tests/conftest.py b/tests/conftest.py index 47d1b42..9a50e8e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ -from cyjs._cyjs import Context, Runtime - import pytest +from cyjs._cyjs import Context, Runtime + @pytest.fixture() def rt(): diff --git a/tests/test_cclosure.py b/tests/test_cclosure.py index 11681c6..c4b888b 100644 --- a/tests/test_cclosure.py +++ b/tests/test_cclosure.py @@ -1,7 +1,7 @@ -from cyjs._cyjs import Context - import pytest +from cyjs._cyjs import Context + # oldest trick in the book... def add(a: int, b: int) -> int: diff --git a/tests/test_class.py b/tests/test_class.py new file mode 100644 index 0000000..cfc52a2 --- /dev/null +++ b/tests/test_class.py @@ -0,0 +1,73 @@ +import pytest + +from cyjs import Context, JSClass, Runtime + + +def test_new_class(rt: Runtime) -> None: + class X: + pass + + js_cls = rt.new_class(X) + assert isinstance(js_cls, JSClass) + assert js_cls.name == "X" + assert js_cls.id != 0 + assert js_cls.type == X + assert js_cls.runtime == rt + +class Point: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + +def test_new_class_with_init(ctx: Context) -> None: + ctx.add_class(ctx.runtime.new_class(Point)) + point: Point = ctx.eval("new Point(1, 2)") + assert point.x == 1 + assert point.y == 2 + + +def test_new_class_with_init_exception(ctx: Context) -> None: + class Point: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + ctx.add_class(ctx.runtime.new_class(Point)) + with pytest.raises(TypeError): + # Would be the same as Point(1) in python but new syntax because it allocates memory + ctx.eval("new Point(1)") + + +def test_new_class_with_public_attributes(ctx: Context) -> None: + class Point: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + ctx.add_class(ctx.runtime.new_class(Point, attrs=("x", "y"))) + x = ctx.eval("let x = new Point(1, 2); x.x") + assert x == 1 + point: Point = ctx.eval( + "function doit(){" + "let point = new Point(10, 2);" + # Try something hacky to trigger a set attribute + "point.x = 5;" + "return point;};" + "doit()" + ) + assert point.x == 5 + assert point.y == 2 + + +# TODO: Test subclassing a Python class from Javascript +# Example: +# class ColorPoint extends Point { +# constructor(x, y, color) { +# super(x, y); +# this.color = color; +# } +# get_color() { +# return this.color; +# } +# } +# def test_with_extension_subclass(ctx: Context) -> None: diff --git a/tests/test_promise_hook.py b/tests/test_promise_hook.py index dc8661a..c1330f4 100644 --- a/tests/test_promise_hook.py +++ b/tests/test_promise_hook.py @@ -1,7 +1,8 @@ -from cyjs import PromiseHookType, Context, Runtime, Promise from dataclasses import dataclass + import pytest +from cyjs import Context, Promise, PromiseHookType, Runtime # Reflects test api-test.c # void promise_hook(void) Line: 523 - 661