From 387c4abdd62c0adfe143f29f0b41eadc9bc1ff18 Mon Sep 17 00:00:00 2001 From: DerThorsten Date: Mon, 18 Mar 2024 09:39:56 +0100 Subject: [PATCH] updated docs --- build_mkdocs.sh | 34 +++++++- docs/JavaScript_API.md | 21 +++++ docs/Python_API.md | 13 +-- environment-dev.yml | 2 +- include/pyjs/pre_js/apply.js | 18 ++-- include/pyjs/pre_js/init.js | 4 + mkdocs.yml | 14 +++- module/pyjs/core.py | 155 +++++++++++++++++++++++++++++++++-- module/pyjs/webloop.py | 22 ++--- src/convert.cpp | 1 + src/export_py_object.cpp | 41 ++++++++- src/js_timestamp.cpp | 2 +- stubs/pyjs_core/__init__.pyi | 85 ++++++++++++++++++- 13 files changed, 367 insertions(+), 45 deletions(-) diff --git a/build_mkdocs.sh b/build_mkdocs.sh index 16a1d82..0984065 100755 --- a/build_mkdocs.sh +++ b/build_mkdocs.sh @@ -29,8 +29,7 @@ if [ ! -d "$WASM_ENV_PREFIX" ]; then --yes \ python pybind11 nlohmann_json pybind11_json numpy \ bzip2 sqlite zlib libffi exceptiongroup \ - xeus xeus-lite xeus-python xeus-javascript xtl "ipython<8.20" - + xeus xeus-lite xeus-python "xeus-javascript>=0.3.1" xtl "ipython=8.22.2=py311had7285e_1" "traitlets>=5.14.2" else echo "Wasm env $WASM_ENV_NAME already exists" fi @@ -38,7 +37,7 @@ fi -if [ ! -f "$PYJS_PROBE_FILE" ]; then +if true; then echo "Building pyjs" cd $THIS_DIR @@ -75,7 +74,6 @@ else fi -# PYJS_PROBE_FILE=$WASM_ENV_PREFIX/share/jupyter/kernels/xpython/kernel.json if false; then echo "Building xeus-python" @@ -113,6 +111,34 @@ else echo "Skipping build xeus-python" fi +# no need to build xeus-javascript, the distributed version is fine +# if false; then +# echo "Building xeus-javascript" + +# cd $THIS_DIR +# source $EMSDK_DIR/emsdk_env.sh + + +# cd ~/src/xeus-javascript +# mkdir -p build_wasm +# cd build_wasm + +# export PREFIX=$WASM_ENV_PREFIX +# export CMAKE_PREFIX_PATH=$PREFIX +# export CMAKE_SYSTEM_PREFIX_PATH=$PREFIX + + +# emcmake cmake .. \ +# -DCMAKE_BUILD_TYPE=Release \ +# -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ON \ +# -DCMAKE_INSTALL_PREFIX=$PREFIX \ +# -DXPYT_EMSCRIPTEN_WASM_BUILD=ON\ + +# emmake make -j8 install +# else +# echo "Skipping build xeus-javascript" +# fi + if true ; then diff --git a/docs/JavaScript_API.md b/docs/JavaScript_API.md index 23d5322..6d02a28 100644 --- a/docs/JavaScript_API.md +++ b/docs/JavaScript_API.md @@ -1,12 +1,33 @@ # JavaScript API + ## `pyjs` +The main module for the JavaScript API. ### `exec` +Execute a string of Python code. ### `exec_eval` +Execute a string of Python code and return the last expression. + +Example: +```javascript +pyjs.exec(` +import numpy +print(numpy.random.rand(3)) +`); +``` + ### `eval` +Evaluate a string with a Python expression. ### `async_exec_eval` +Schedule the execution of a string of Python code and return a promise. +The last expression is returned as the result of the promise. ### `eval_file` +Evaluate a file with Python code. ### `pyobject` +A Python object exported as a JavaScript class #### `py_call` +Call the `__call__` method of a Python object. #### `py_apply` +Call the `__call__` method of a Python object with an array of arguments. #### `get` +Get an attribute of a Python object. diff --git a/docs/Python_API.md b/docs/Python_API.md index bcc99b4..76bea0c 100644 --- a/docs/Python_API.md +++ b/docs/Python_API.md @@ -12,21 +12,18 @@ options: show_submodules: true members: + - pyjs_core - to_js + - to_py - register_converter - - to_py_json - JsToPyConverterOptions - - to_py - - error_to_py - - error_to_py_and_raise - new - create_callable - callable_context - - promise - create_once_callable + - promise - apply - - japply - - gapply + - WebLoop - JsException - JsGenericError - JsError @@ -36,8 +33,6 @@ - JsSyntaxError - JsTypeError - JsURIError - - WebLoop - - pyjs_core diff --git a/environment-dev.yml b/environment-dev.yml index aaba3d9..849959c 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -18,6 +18,7 @@ dependencies: - mkdocs - mkdocstrings - mkdocstrings-python + - mkdocs-material - empack >=3.2.0 - jupyter_server # to enable contents - jupyterlite @@ -27,5 +28,4 @@ dependencies: # pyjs_code_runner dev deps - hatchling - pip: - - mkdocs-dracula-theme - JLDracula diff --git a/include/pyjs/pre_js/apply.js b/include/pyjs/pre_js/apply.js index 7f4f3be..421727a 100644 --- a/include/pyjs/pre_js/apply.js +++ b/include/pyjs/pre_js/apply.js @@ -1,14 +1,16 @@ function isPromise(p) { - if ( - p !== null && - typeof p === 'object' && - typeof p.then === 'function' && - typeof p.catch === 'function' - ) { - return true; + try{ + if ( + p !== null && + typeof p === 'object' && + typeof p.then === 'function' && + typeof p.catch === 'function' + ) { + return true; + } + } catch (e) { } - return false; } diff --git a/include/pyjs/pre_js/init.js b/include/pyjs/pre_js/init.js index 5274c89..c8e6388 100644 --- a/include/pyjs/pre_js/init.js +++ b/include/pyjs/pre_js/init.js @@ -80,6 +80,10 @@ Module['init_phase_1'] = async function(prefix, python_version) { return this.py_apply(args) }; + // Module['pyobject'].prototype.toString = function(...args) { + // return this.py_apply(args) + // }; + Module['pyobject'].prototype.py_apply = function(args, kwargs) { if (args === undefined) { diff --git a/mkdocs.yml b/mkdocs.yml index e0a3618..77d5e69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Pyjs docs theme: - # name: material - name: dracula + name: material + # name: dracula palette: @@ -34,3 +34,13 @@ plugins: allow_inspection: true show_root_heading: true show_source: false + + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences diff --git a/module/pyjs/core.py b/module/pyjs/core.py index e284568..545d3b3 100644 --- a/module/pyjs/core.py +++ b/module/pyjs/core.py @@ -5,8 +5,8 @@ from typing import Any import ast import pyjs_core - from pyjs_core import JsValue, js_array, js_py_object + def install_submodules(): def _js_mod__getattr__(name: str) -> Any: ret = pyjs_core.internal.global_property(name) @@ -36,29 +36,89 @@ def _module_mod__getattr__(name: str) -> Any: def new(cls_, *args): + """ Create a new instance of a JavaScript class. + + This function is a wrapper around the `new` operator in JavaScript. + + Args: + cls_ (JsValue): The JavaScript class to create an instance of + *args (Any): The arguments to pass to the constructor of the JavaScript class + """ return pyjs_core._module._new(cls_, *args) - +# todo deprecate def async_import_javascript(path): return pyjs_core._module._async_import_javascript(path) +# TODO make private def type_str(x): return pyjs_core.internal.type_str(x) def create_callable(py_function): + '''Create a JavaScript callable from a Python function. + + Args: + py_function (Callable): The Python function to create a JavaScript callable from. + + Example: + ```python + def py_function(x, y): + return x + y + + js_callable, js_py_object = create_callable(py_function) + + # this function can be passed to JavaScript. + # lets create some JavaScript code to test it + higher_order_function = pyjs.js.Function("f", "x", "y", "z", """ + return z * f(x, y); + """) + + # call the higher order JavaScript function with py_function wrapped as a JavaScript callable + result = higher_order_function(js_callable, 1, 2, 3) + assert result == 9 + + js_py_object.delete() + ``` + + Returns: + callable: The JavaScript callable + js_py_object: this object needs to be deleted after the callable is no longer needed + ''' _js_py_object = js_py_object(py_function) return _js_py_object["py_call"].bind(_js_py_object), _js_py_object @contextlib.contextmanager def callable_context(py_function): + ''' Create a JavaScript callable from a Python function and delete it when the context is exited. + + See `create_callable` for more information. + + Args: + py_function (Callable): The Python function to create a JavaScript callable from. + + Example: + + ```python + def py_function(x, y): + return x + y + + with pyjs.callable_context(py_function) as js_function: + # js_function is a JavaScript callable and could be passed and called from JavaScript + # here we just call it from Python + print(js_function(1,2)) + ``` + ''' + + cb, handle = create_callable(py_function) yield cb handle.delete() +# todo, deprecate class AsOnceCallableMixin(object): def __init__(self): self._once_callable = create_once_callable(self) @@ -68,10 +128,70 @@ def as_once_callable(self): def promise(py_resolve_reject): - return js.Promise.new(create_once_callable(py_resolve_reject)) + """ Create a new JavaScript promise with a python callback to resolve or reject the promise. + + Args: + py_resolve_reject (Callable): A Python function that takes two arguments, resolve and reject, which are both functions. + The resolve function should be called with the result of the promise and the reject function should be called with an error. + + Example: + ```python + import asyncio + import pyjs + def f(resolve, reject): + async def task(): + try: + print("start task") + await asyncio.sleep(1) + print("end task") + # resolve when everything is done + resolve() + except: + # reject the promise in case of an error + reject() + asyncio.create_task(task()) + + js_promise = pyjs.promise(f) + print("await the js promise from python") + await js_promise + print("the wait has an end") + print(js_promise) + ``` + """ + + return pyjs_core.js.Promise.new(create_once_callable(py_resolve_reject)) def create_once_callable(py_function): + """Create a JavaScript callable from a Python function that can only be called once. + + Since this function can only be called once, it will be deleted after the first call. + Therefore no manual deletion is necessary. + See `create_callable` for more information. + + Args: + py_function (Callable): The Python function to create a JavaScript callable from. + + Returns: + callable: The JavaScript callable + + Example: + ```python + + def py_function(x, y): + return x + y + + js_function = pyjs.create_once_callable(py_function) + print(js_function(1,2)) # this will print 3 + + # the following will raise an error + try: + print(js_function(1,2)) + except Exception as e: + print(e) + ``` + """ + js_py_function = JsValue(py_function) once_callable = pyjs_core._module._create_once_callable(js_py_function) return once_callable @@ -88,17 +208,38 @@ def _make_js_args(args): def apply(js_function, args): + '''Call a JavaScript function with the given arguments. + + Args: + js_function (JsValue): The JavaScript function to call + args (List): The arguments to pass to the JavaScript function + + Returns: + Any: The result of the JavaScript function + + Example: + ```python + + # create a JavaScript function on the fly + js_function = pyjs.js.Function("x", "y", """ + return x + y; + """) + result = pyjs.apply(js_function, [1, 2]) + assert result == 3 + ``` + ''' js_array_args, is_generated_proxy = _make_js_args(args) ret, meta = pyjs_core.internal.apply_try_catch(js_function, js_array_args, is_generated_proxy) return ret +# deprecated def japply(js_function, args): sargs = json.dumps(args) ret, meta = pyjs_core.internal.japply_try_catch(js_function, sargs) return ret - +# deprecated def gapply(js_function, args, jin=True, jout=True): if jin: args = json.dumps(args) @@ -115,6 +256,7 @@ def gapply(js_function, args, jin=True, jout=True): return ret +# move to internal def exec_eval(script, globals=None, locals=None): """Execute a script and return the value of the last expression""" stmts = list(ast.iter_child_nodes(ast.parse(script))) @@ -145,10 +287,7 @@ def exec_eval(script, globals=None, locals=None): # otherwise we just execute the entire code return exec(script, globals, locals) - -import ast - - +# move to internal async def async_exec_eval(stmts, globals=None, locals=None): parsed_stmts = ast.parse(stmts) if parsed_stmts.body: diff --git a/module/pyjs/webloop.py b/module/pyjs/webloop.py index 844dade..2e83688 100644 --- a/module/pyjs/webloop.py +++ b/module/pyjs/webloop.py @@ -346,16 +346,18 @@ def default_exception_handler(self, context): def call_exception_handler(self, context): """Call the current event loop's exception handler. The context argument is a dict containing the following keys: - - 'message': Error message; - - 'exception' (optional): Exception object; - - 'future' (optional): Future instance; - - 'task' (optional): Task instance; - - 'handle' (optional): Handle instance; - - 'protocol' (optional): Protocol instance; - - 'transport' (optional): Transport instance; - - 'socket' (optional): Socket instance; - - 'asyncgen' (optional): Asynchronous generator that caused - the exception. + + * `message`: Error message; + * `exception` (optional): Exception object; + * `future` (optional): Future instance; + * `task` (optional): Task instance; + * `handle` (optional): Handle instance; + * `protocol` (optional): Protocol instance; + * `transport` (optional): Transport instance; + * `socket` (optional): Socket instance; + * `asyncgen` (optional): Asynchronous generator that caused + the exception. + New keys maybe introduced in the future. Note: do not overload this method in an event loop subclass. For custom exception handling, use the diff --git a/src/convert.cpp b/src/convert.cpp index dc1c8eb..5ec97b7 100644 --- a/src/convert.cpp +++ b/src/convert.cpp @@ -52,6 +52,7 @@ namespace pyjs } } + py::object implicit_js_to_py(em::val val, const std::string& type_string) { if (type_string.size() == 1) diff --git a/src/export_py_object.cpp b/src/export_py_object.cpp index 9a7fd43..ec39087 100644 --- a/src/export_py_object.cpp +++ b/src/export_py_object.cpp @@ -235,7 +235,46 @@ namespace pyjs } })) - + .function("toString", + em::select_overload( + [](py::object& pyobject) -> em::val + { + const std::string str = py::str(pyobject); + return em::val(str); + }) + ) + .function("toJSON", + em::select_overload( + [](py::object& pyobject, em::val val) -> em::val + { + auto json_module = py::module::import("json"); + auto json_dumps = json_module.attr("dumps"); + try{ + auto json_str = em::val(json_dumps(pyobject).cast()); + return json_str; + } + catch (py::error_already_set& e) + { + return em::val(py::str(pyobject).cast()); + } + }) + ) + .function("toJSON", + em::select_overload( + [](py::object& pyobject) -> em::val + { + auto json_module = py::module::import("json"); + auto json_dumps = json_module.attr("dumps"); + try{ + auto json_str = em::val(json_dumps(pyobject).cast()); + return json_str; + } + catch (py::error_already_set& e) + { + return em::val(py::str(pyobject).cast()); + } + }) + ) ; } diff --git a/src/js_timestamp.cpp b/src/js_timestamp.cpp index 3c0437b..f56c7ab 100644 --- a/src/js_timestamp.cpp +++ b/src/js_timestamp.cpp @@ -1 +1 @@ -#define PYJS_JS_UTC_TIMESTAMP "2024-03-13 10:28:59.928677" \ No newline at end of file +#define PYJS_JS_UTC_TIMESTAMP "2024-03-15 12:48:37.105024" \ No newline at end of file diff --git a/stubs/pyjs_core/__init__.pyi b/stubs/pyjs_core/__init__.pyi index c802773..3f03423 100644 --- a/stubs/pyjs_core/__init__.pyi +++ b/stubs/pyjs_core/__init__.pyi @@ -1,10 +1,13 @@ """This module does blah blah.""" +from typing import Any + + class JsValue: """ A class holding a javascript object/value. """ - def ok_1(self, foo: list[str] = ...) -> None: ... + def __init__(self, value: Any) -> None: """ @@ -14,4 +17,84 @@ class JsValue: If the value is a primitive type (int, float, str, bool) it will be converted to the corresponding javascript type. For any other python object, it will be converted to the javascript class `pyjs.pyobject` which is a wrapper around the python object on the javascript side. """ + ... + + # def __getitem__(self, key: Union[str, int]) -> Any: + + def __call__(self, *args: Any) -> Any: + """ + Call the javascript object as a function. + + Args: + *args: The arguments to pass to the function. + + Returns: + The result of the function call. + """ + ... + + def __str__(self) -> str: + """ + Convert the javascript object to a string. + """ + ... + + def __repr__(self) -> str: + """ + Convert the javascript object to a string. + """ + ... + + def __len__(self) -> int: + """ + Get the length of the javascript object. + """ + ... + + def __contains__(self, q: Any) -> bool: + """ + Check if the javascript object contains a value. + """ + ... + + def __eq__(self, q: Any) -> bool: + """ + Check if the javascript object is equal to a value. + """ + ... + + def new(self, *args: Any) -> Any: + """ + Create a new instance of a JavaScript class. + """ + ... + + def __iter__(self) -> Any: + """ + Get an iterator for the javascript object. + """ + ... + + def __next__(self) -> Any: + """ + Get the next value from the iterator. + """ + ... + + def __delattr__(self, __name: str) -> None: + """ + Delete an attribute from the javascript object. + """ + ... + + def __delitem__(self, __name: str) -> None: + """ + Delete an item from the javascript object. + """ + ... + + def __await__(self) -> Any: + """ + Wait for the javascript object to resolve. + """ ... \ No newline at end of file