diff --git a/Makefile b/Makefile index 03d8efffd..8bc0ae3fa 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,7 @@ all: check \ build/pyodide.asm.js: \ + src/core/docstring.o \ src/core/error_handling.o \ src/core/hiwire.o \ src/core/js2python.o \ @@ -85,6 +86,7 @@ build/pyodide.asm.js: \ --preload-file src/_testcapi.py@/lib/python$(PYMINOR)/_testcapi.py \ --preload-file src/pystone.py@/lib/python$(PYMINOR)/pystone.py \ --preload-file src/pyodide-py/pyodide@/lib/python$(PYMINOR)/site-packages/pyodide \ + --preload-file src/pyodide-py/_pyodide@/lib/python$(PYMINOR)/site-packages/_pyodide \ --exclude-file "*__pycache__*" \ --exclude-file "*/test/*" date +"[%F %T] done building pyodide.asm.js." diff --git a/docs/usage/api/python-api.md b/docs/usage/api/python-api.md index de3ce8e77..4ada08e79 100644 --- a/docs/usage/api/python-api.md +++ b/docs/usage/api/python-api.md @@ -9,4 +9,5 @@ Backward compatibility of the API is not guaranteed at this point. .. automodule:: pyodide :members: :autosummary: -``` \ No newline at end of file + :autosummary-no-nesting: +``` diff --git a/src/core/docstring.c b/src/core/docstring.c new file mode 100644 index 000000000..6d5248421 --- /dev/null +++ b/src/core/docstring.c @@ -0,0 +1,52 @@ +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include "error_handling.h" + +_Py_IDENTIFIER(get_cmeth_docstring); +PyObject* py_docstring_mod; + +int +set_method_docstring(PyMethodDef* method, PyObject* parent) +{ + bool success = false; + PyObject* py_method = NULL; + PyObject* py_result = NULL; + + py_method = PyObject_GetAttrString(parent, method->ml_name); + FAIL_IF_NULL(py_method); + + py_result = _PyObject_CallMethodIdObjArgs( + py_docstring_mod, &PyId_get_cmeth_docstring, py_method, NULL); + FAIL_IF_NULL(py_result); + + Py_ssize_t size; + const char* py_result_utf8 = PyUnicode_AsUTF8AndSize(py_result, &size); + // size is the number of characters in the string, not including the null + // byte at the end. + // We are never going to free this memory. + char* result = (char*)malloc(size + 1); + FAIL_IF_NULL(result); + + memcpy(result, py_result_utf8, size + 1); + method->ml_doc = result; + + success = true; +finally: + Py_CLEAR(py_method); + Py_CLEAR(py_result); + return success ? 0 : -1; +} + +int +docstring_init() +{ + bool success = false; + + py_docstring_mod = PyImport_ImportModule("_pyodide.docstring"); + FAIL_IF_NULL(py_docstring_mod); + + success = true; +finally: + return success ? 0 : -1; +} diff --git a/src/core/docstring.h b/src/core/docstring.h new file mode 100644 index 000000000..750233c36 --- /dev/null +++ b/src/core/docstring.h @@ -0,0 +1,10 @@ +#ifndef DOCSTRING_H +#define DOCSTRING_H + +int +set_method_docstring(PyMethodDef* method, PyObject* parent); + +int +docstring_init(); + +#endif /* DOCSTRING_H */ diff --git a/src/core/hiwire.c b/src/core/hiwire.c index 0a5ce7eec..f546595a9 100644 --- a/src/core/hiwire.c +++ b/src/core/hiwire.c @@ -680,6 +680,16 @@ EM_JS_REF(JsRef, hiwire_object_entries, (JsRef idobj), { return Module.hiwire.new_value(Object.entries(jsobj)); }); +EM_JS_REF(JsRef, hiwire_object_keys, (JsRef idobj), { + let jsobj = Module.hiwire.get_value(idobj); + return Module.hiwire.new_value(Object.keys(jsobj)); +}); + +EM_JS_REF(JsRef, hiwire_object_values, (JsRef idobj), { + let jsobj = Module.hiwire.get_value(idobj); + return Module.hiwire.new_value(Object.values(jsobj)); +}); + EM_JS_NUM(bool, hiwire_is_typedarray, (JsRef idobj), { let jsobj = Module.hiwire.get_value(idobj); // clang-format off diff --git a/src/core/hiwire.h b/src/core/hiwire.h index 3dea9b25c..afd7cab66 100644 --- a/src/core/hiwire.h +++ b/src/core/hiwire.h @@ -632,6 +632,18 @@ hiwire_get_iterator(JsRef idobj); JsRef hiwire_object_entries(JsRef idobj); +/** + * Returns `Object.keys(obj)` + */ +JsRef +hiwire_object_keys(JsRef idobj); + +/** + * Returns `Object.values(obj)` + */ +JsRef +hiwire_object_values(JsRef idobj); + /** * Returns 1 if the value is a typedarray. */ diff --git a/src/core/jsproxy.c b/src/core/jsproxy.c index ab2630682..a4c4166b6 100644 --- a/src/core/jsproxy.c +++ b/src/core/jsproxy.c @@ -31,6 +31,7 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" +#include "docstring.h" #include "hiwire.h" #include "js2python.h" #include "jsproxy.h" @@ -328,6 +329,58 @@ JsProxy_object_entries(PyObject* o, PyObject* _args) return result; } +PyMethodDef JsProxy_object_entries_MethodDef = { + "object_entries", + (PyCFunction)JsProxy_object_entries, + METH_NOARGS, +}; + +/** + * This is exposed as a METH_NOARGS method on the JsProxy. It returns + * Object.keys(obj) as a new JsProxy. + */ +static PyObject* +JsProxy_object_keys(PyObject* o, PyObject* _args) +{ + JsProxy* self = (JsProxy*)o; + JsRef result_id = hiwire_object_keys(self->js); + if (result_id == NULL) { + return NULL; + } + PyObject* result = JsProxy_create(result_id); + hiwire_decref(result_id); + return result; +} + +PyMethodDef JsProxy_object_keys_MethodDef = { + "object_keys", + (PyCFunction)JsProxy_object_keys, + METH_NOARGS, +}; + +/** + * This is exposed as a METH_NOARGS method on the JsProxy. It returns + * Object.entries(obj) as a new JsProxy. + */ +static PyObject* +JsProxy_object_values(PyObject* o, PyObject* _args) +{ + JsProxy* self = (JsProxy*)o; + JsRef result_id = hiwire_object_values(self->js); + if (result_id == NULL) { + return NULL; + } + PyObject* result = JsProxy_create(result_id); + hiwire_decref(result_id); + return result; +} + +PyMethodDef JsProxy_object_values_MethodDef = { + "object_values", + (PyCFunction)JsProxy_object_values, + METH_NOARGS, +}; + /** * len(proxy) overload for proxies of Js objects with `length` or `size` fields. * Prefers `object.size` over `object.length`. Controlled by HAS_LENGTH. @@ -587,6 +640,13 @@ finally: return result; } +PyMethodDef JsProxy_Dir_MethodDef = { + "__dir__", + (PyCFunction)JsProxy_Dir, + METH_NOARGS, + PyDoc_STR("Returns a list of the members and methods on the object."), +}; + /** * The to_py method, uses METH_FASTCALL calling convention. */ @@ -609,6 +669,12 @@ JsProxy_toPy(PyObject* self, PyObject* const* args, Py_ssize_t nargs) return js2python_convert(GET_JSREF(self), depth); } +PyMethodDef JsProxy_toPy_MethodDef = { + "to_py", + (PyCFunction)JsProxy_toPy, + METH_FASTCALL, +}; + /** * Overload for bool(proxy), implemented for every JsProxy. Return `False` if * the object is falsey in Javascript, or if it has a `size` field equal to 0, @@ -730,6 +796,12 @@ finally: return result; } +PyMethodDef JsProxy_then_MethodDef = { + "then", + (PyCFunction)JsProxy_then, + METH_VARARGS | METH_KEYWORDS, +}; + /** * Overload for `catch` for JsProxies with a `then` method. */ @@ -761,6 +833,12 @@ finally: return result; } +PyMethodDef JsProxy_catch_MethodDef = { + "catch", + (PyCFunction)JsProxy_catch, + METH_O, +}; + /** * Overload for `finally` for JsProxies with a `then` method. This isn't * strictly necessary since one could get the same effect by just calling @@ -796,6 +874,12 @@ finally: return result; } +PyMethodDef JsProxy_finally_MethodDef = { + "finally_", + (PyCFunction)JsProxy_finally, + METH_O, +}; + // clang-format off static PyNumberMethods JsProxy_NumberMethods = { .nb_bool = JsProxy_Bool @@ -806,7 +890,7 @@ static PyGetSetDef JsProxy_GetSet[] = { { "typeof", .get = JsProxy_typeof }, { NULL } }; static PyTypeObject JsProxyType = { - .tp_name = "JsProxy", + .tp_name = "pyodide.JsProxy", .tp_basicsize = sizeof(JsProxy), .tp_dealloc = (destructor)JsProxy_dealloc, .tp_getattro = JsProxy_GetAttr, @@ -1060,6 +1144,14 @@ JsMethod_jsnew(PyObject* o, PyObject* args, PyObject* kwargs) return pyresult; } +// clang-format off +PyMethodDef JsMethod_jsnew_MethodDef = { + "new", + (PyCFunction)JsMethod_jsnew, + METH_VARARGS | METH_KEYWORDS +}; +// clang-format on + static int JsMethod_cinit(PyObject* obj, JsRef this_) { @@ -1208,24 +1300,9 @@ JsProxy_create_subtype(int flags) int cur_member = 0; // clang-format off - methods[cur_method++] = (PyMethodDef){ - "__dir__", - (PyCFunction)JsProxy_Dir, - METH_NOARGS, - PyDoc_STR("Returns a list of the members and methods on the object."), - }; - methods[cur_method++] = (PyMethodDef){ - "to_py", - (PyCFunction)JsProxy_toPy, - METH_FASTCALL, - PyDoc_STR("Convert the JsProxy to a native Python object (as best as possible)"), - }; - methods[cur_method++] = (PyMethodDef){ - "object_entries", - (PyCFunction)JsProxy_object_entries, - METH_NOARGS, - PyDoc_STR("This does javascript Object.entries(object)."), - }; + methods[cur_method++] = JsProxy_Dir_MethodDef; + methods[cur_method++] = JsProxy_toPy_MethodDef; + methods[cur_method++] = JsProxy_object_entries_MethodDef; // clang-format on PyTypeObject* base = &JsProxyType; @@ -1275,35 +1352,9 @@ JsProxy_create_subtype(int flags) if (flags & IS_AWAITABLE) { slots[cur_slot++] = (PyType_Slot){ .slot = Py_am_await, .pfunc = (void*)JsProxy_Await }; - methods[cur_method++] = (PyMethodDef){ - "then", - (PyCFunction)JsProxy_then, - METH_VARARGS | METH_KEYWORDS, - PyDoc_STR( - "then(onfulfilled : Callable = None, onrejected : Callable = None)" - " -> JsProxy" - "\n\n" - "The promise.then api, wrapped to manage the lifetimes of the " - "arguments"), - }; - methods[cur_method++] = (PyMethodDef){ - "catch", - (PyCFunction)JsProxy_catch, - METH_O, - PyDoc_STR("catch(onrejected : Callable) -> JsProxy" - "\n\n" - "The promise.catch api, wrapped to manage the lifetime of the " - "argument."), - }; - methods[cur_method++] = (PyMethodDef){ - "finally_", - (PyCFunction)JsProxy_finally, - METH_O, - PyDoc_STR("finally_(onrejected : Callable) -> JsProxy" - "\n\n" - "The promise.finally api, wrapped to manage the lifetime of " - "the argument."), - }; + methods[cur_method++] = JsProxy_then_MethodDef; + methods[cur_method++] = JsProxy_catch_MethodDef; + methods[cur_method++] = JsProxy_finally_MethodDef; } if (flags & IS_CALLABLE) { tp_flags |= _Py_TPFLAGS_HAVE_VECTORCALL; @@ -1312,12 +1363,7 @@ JsProxy_create_subtype(int flags) // We could test separately for whether a function is constructable, // but it generates a lot of false positives. // clang-format off - methods[cur_method++] = (PyMethodDef){ - "new", - (PyCFunction)JsMethod_jsnew, - METH_VARARGS | METH_KEYWORDS, - "Construct a new instance" - }; + methods[cur_method++] = JsMethod_jsnew_MethodDef; // clang-format on } if (flags & IS_BUFFER) { @@ -1375,8 +1421,7 @@ JsProxy_create_subtype(int flags) slots[cur_slot++] = (PyType_Slot){ 0 }; PyType_Spec spec = { - // TODO: for Python3.9 the name will need to change to "pyodide.JsProxy" - .name = "JsProxy", + .name = "pyodide.JsProxy", .itemsize = 0, .flags = tp_flags, .slots = slots, @@ -1568,8 +1613,34 @@ JsProxy_init(PyObject* core_module) { bool success = false; + PyObject* _pyodide_core = NULL; + PyObject* jsproxy_mock = NULL; PyObject* asyncio_module = NULL; + _pyodide_core = PyImport_ImportModule("_pyodide._core"); + FAIL_IF_NULL(_pyodide_core); + _Py_IDENTIFIER(JsProxy); + jsproxy_mock = + _PyObject_CallMethodIdObjArgs(_pyodide_core, &PyId_JsProxy, NULL); + FAIL_IF_NULL(jsproxy_mock); + + // Load the docstrings for JsProxy methods from the corresponding stubs in + // _pyodide._core. set_method_docstring uses + // _pyodide.docstring.get_cmeth_docstring to generate the appropriate C-style + // docstring from the Python-style docstring. +#define SET_DOCSTRING(x) \ + FAIL_IF_MINUS_ONE(set_method_docstring(&x, jsproxy_mock)) + SET_DOCSTRING(JsProxy_object_entries_MethodDef); + SET_DOCSTRING(JsProxy_object_keys_MethodDef); + SET_DOCSTRING(JsProxy_object_values_MethodDef); + // SET_DOCSTRING(JsProxy_Dir_MethodDef); + SET_DOCSTRING(JsProxy_toPy_MethodDef); + SET_DOCSTRING(JsProxy_then_MethodDef); + SET_DOCSTRING(JsProxy_catch_MethodDef); + SET_DOCSTRING(JsProxy_finally_MethodDef); + SET_DOCSTRING(JsMethod_jsnew_MethodDef); +#undef SET_DOCSTRING + asyncio_module = PyImport_ImportModule("asyncio"); FAIL_IF_NULL(asyncio_module); @@ -1590,6 +1661,8 @@ JsProxy_init(PyObject* core_module) success = true; finally: + Py_CLEAR(_pyodide_core); + Py_CLEAR(jsproxy_mock); Py_CLEAR(asyncio_module); return success ? 0 : -1; } diff --git a/src/core/main.c b/src/core/main.c index 672e45f9c..d3e235bbc 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -4,6 +4,7 @@ #include #include +#include "docstring.h" #include "error_handling.h" #include "hiwire.h" #include "js2python.h" @@ -16,9 +17,11 @@ do { \ printf("FATAL ERROR: "); \ printf(args); \ + printf("\n"); \ if (PyErr_Occurred()) { \ printf("Error was triggered by Python exception:\n"); \ PyErr_Print(); \ + EM_ASM(throw new Error("Fatal pyodide error")); \ } \ return -1; \ } while (0) @@ -31,7 +34,7 @@ #define TRY_INIT(mod) \ do { \ if (mod##_init()) { \ - FATAL_ERROR("Failed to initialize module %s.\n", #mod); \ + FATAL_ERROR("Failed to initialize module %s.", #mod); \ } \ } while (0) @@ -61,7 +64,7 @@ finally: #define TRY_INIT_WITH_CORE_MODULE(mod) \ do { \ if (mod##_init(core_module)) { \ - FATAL_ERROR("Failed to initialize module %s.\n", #mod); \ + FATAL_ERROR("Failed to initialize module %s.", #mod); \ } \ } while (0) @@ -117,8 +120,9 @@ main(int argc, char** argv) FATAL_ERROR("Failed to create core module."); } - TRY_INIT(hiwire); TRY_INIT_WITH_CORE_MODULE(error_handling); + TRY_INIT(hiwire); + TRY_INIT(docstring); TRY_INIT(js2python); TRY_INIT_WITH_CORE_MODULE(JsProxy); TRY_INIT_WITH_CORE_MODULE(pyproxy); @@ -136,7 +140,12 @@ main(int argc, char** argv) } EM_ASM({ Module.init_dict = Module.hiwire.pop_value($0); }, init_dict_proxy); + PyObject* pyodide = PyImport_ImportModule("pyodide"); + if (pyodide == NULL) { + FATAL_ERROR("Failed to import pyodide module"); + } Py_CLEAR(core_module); + Py_CLEAR(pyodide); printf("Python initialization complete\n"); emscripten_exit_with_live_runtime(); return 0; diff --git a/src/core/pyproxy.c b/src/core/pyproxy.c index 3e5251161..7d18fa274 100644 --- a/src/core/pyproxy.c +++ b/src/core/pyproxy.c @@ -3,6 +3,7 @@ #include "error_handling.h" #include +#include "docstring.h" #include "hiwire.h" #include "js2python.h" #include "jsproxy.h" @@ -1338,23 +1339,11 @@ static PyMethodDef pyproxy_methods[] = { "create_once_callable", create_once_callable_py, METH_O, - PyDoc_STR( - "create_once_callable(obj : Callable) -> JsProxy" - "\n\n" - "Wrap a Python callable in a Javascript function that can be called " - "once. After being called the proxy will decrement the reference count " - "of the Callable. The javascript function also has a `destroy` API that " - "can be used to release the proxy without calling it."), }, { "create_proxy", create_proxy, METH_O, - PyDoc_STR("create_proxy(obj : Any) -> JsProxy" - "\n\n" - "Create a `JsProxy` of a `PyProxy`. This allows explicit control " - "over the lifetime of the `PyProxy` from Python: call the " - "`destroy` API when done."), }, { NULL } /* Sentinel */ }; @@ -1362,16 +1351,25 @@ static PyMethodDef pyproxy_methods[] = { int pyproxy_init(PyObject* core) { - PyModule_AddFunctions(core, pyproxy_methods); + bool success = false; + int i = 0; + + PyObject* _pyodide_core = NULL; + _pyodide_core = PyImport_ImportModule("_pyodide._core"); + FAIL_IF_NULL(_pyodide_core); + + while (pyproxy_methods[i].ml_name != NULL) { + FAIL_IF_MINUS_ONE(set_method_docstring(&pyproxy_methods[i], _pyodide_core)); + i++; + } + FAIL_IF_MINUS_ONE(PyModule_AddFunctions(core, pyproxy_methods)); asyncio = PyImport_ImportModule("asyncio"); - if (asyncio == NULL) { - return -1; - } - if (PyType_Ready(&FutureDoneCallbackType)) { - return -1; - } - if (pyproxy_init_js()) { - return -1; - } + FAIL_IF_NULL(asyncio); + FAIL_IF_MINUS_ONE(PyType_Ready(&FutureDoneCallbackType)); + FAIL_IF_MINUS_ONE(pyproxy_init_js()); + + success = true; +finally: + Py_CLEAR(_pyodide_core); return 0; } diff --git a/src/pyodide-py/_pyodide/__init__.py b/src/pyodide-py/_pyodide/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pyodide-py/_pyodide/_core.py b/src/pyodide-py/_pyodide/_core.py new file mode 100644 index 000000000..86f5af272 --- /dev/null +++ b/src/pyodide-py/_pyodide/_core.py @@ -0,0 +1,95 @@ +# type: ignore + +from typing import Any, Callable + +# All docstrings for public `core` APIs should be extracted from here. We use +# the utilities in `docstring.py` and `docstring.c` to format them +# appropriately. + +# Sphinx uses __name__ to determine the paths and such. It looks better for it +# to refer to e.g., `pyodide.JsProxy` than `_pyodide._core.JsProxy`. +_save_name = __name__ +__name__ = "pyodide" +try: + # From jsproxy.c + class JsException(Exception): + """ + A wrapper around a Javascript ``Error`` to allow the ``Error`` to be thrown in Python. + """ + + class JsProxy: + """A proxy to make a Javascript object behave like a Python object + + For more information see :ref:`type-translations` documentation. + """ + + def __init__(self): + """""" + + def object_entries(self) -> "JsProxy": + "The Javascript API ``Object.entries(object)``" + + def object_keys(self) -> "JsProxy": + "The Javascript API ``Object.keys(object)``" + + def object_values(self) -> "JsProxy": + "The Javascript API ``Object.values(object)``" + + def new(self, *args, **kwargs) -> "JsProxy": + """Construct a new instance of the Javascript object""" + + def to_py(self) -> Any: + """Convert the :class:`JsProxy` to a native Python object as best as possible""" + pass + + def then(self, onfulfilled: Callable, onrejected: Callable) -> "Promise": + """The ``Promise.then`` api, wrapped to manage the lifetimes of the + handlers. + + Only available if the wrapped Javascript object has a "then" method. + Pyodide will automatically release the references to the handlers + when the promise resolves. + """ + + def catch(self, onrejected: Callable) -> "Promise": + """The ``Promise.catch`` api, wrapped to manage the lifetimes of the + handler. + + Only available if the wrapped Javascript object has a "then" method. + Pyodide will automatically release the references to the handler + when the promise resolves. + """ + + def finally_(self, onfinally: Callable) -> "Promise": + """The ``Promise.finally`` api, wrapped to manage the lifetimes of + the handler. + + Only available if the wrapped Javascript object has a "then" method. + Pyodide will automatically release the references to the handler + when the promise resolves. Note the trailing underscore in the name; + this is needed because ``finally`` is a reserved keyword in Python. + """ + + # from pyproxy.c + + def create_once_callable(obj: Callable) -> JsProxy: + """Wrap a Python callable in a Javascript function that can be called once. + + After being called the proxy will decrement the reference count + of the Callable. The Javascript function also has a ``destroy`` API that + can be used to release the proxy without calling it. + """ + return obj + + def create_proxy(obj: Any) -> JsProxy: + """Create a ``JsProxy`` of a ``PyProxy``. + + This allows explicit control over the lifetime of the ``PyProxy`` from + Python: call the ``destroy`` API when done. + """ + return obj + + +finally: + __name__ = _save_name + del _save_name diff --git a/src/pyodide-py/_pyodide/docstring.py b/src/pyodide-py/_pyodide/docstring.py new file mode 100644 index 000000000..36df5101e --- /dev/null +++ b/src/pyodide-py/_pyodide/docstring.py @@ -0,0 +1,48 @@ +from textwrap import dedent + + +def dedent_docstring(docstring): + """This removes initial spaces from the lines of the docstring. + + After the first line of the docstring, all other lines will include some + spaces. This removes them. + + Examples + -------- + >>> from _pyodide.docstring import dedent_docstring + >>> dedent_docstring(dedent_docstring.__doc__).split("\\n")[2] + 'After the first line of the docstring, all other lines will include some' + """ + first_newline = docstring.find("\n") + if first_newline == -1: + return docstring + docstring = docstring[:first_newline] + dedent(docstring[first_newline:]) + return docstring + + +def get_cmeth_docstring(func): + """Get the value to use for the PyMethodDef.ml_doc attribute for a builtin + function. This is used in docstring.c. + + The ml_doc should start with a signature which cannot have any type + annotations. The signature must end with the exact characters ")\n--\n\n". + For example: "funcname(arg1, arg2)\n--\n\n" + + See: + https://github.com/python/cpython/blob/v3.8.2/Objects/typeobject.c#L84 + + Examples + -------- + >>> from _pyodide.docstring import get_cmeth_docstring + >>> get_cmeth_docstring(sum)[:80] + "sum(iterable, /, start=0)\\n--\\n\\nReturn the sum of a 'start' value (default: 0) plu" + """ + from inspect import signature, _empty + + sig = signature(func) + # remove param and return annotations and + for param in sig.parameters.values(): + param._annotation = _empty + sig._return_annotation = _empty + + return func.__name__ + str(sig) + "\n--\n\n" + dedent_docstring(func.__doc__) diff --git a/src/pyodide-py/pyodide/__init__.py b/src/pyodide-py/pyodide/__init__.py index 9778750e9..e95c175b7 100644 --- a/src/pyodide-py/pyodide/__init__.py +++ b/src/pyodide-py/pyodide/__init__.py @@ -1,5 +1,5 @@ from ._base import open_url, eval_code, eval_code_async, find_imports -from ._core import JsException, create_once_callable, create_proxy # type: ignore +from ._core import JsProxy, JsException, create_once_callable, create_proxy # type: ignore from ._importhooks import jsfinder from .webloop import WebLoopPolicy from . import _state # type: ignore # noqa @@ -22,6 +22,7 @@ __all__ = [ "eval_code", "eval_code_async", "find_imports", + "JsProxy", "JsException", "register_js_module", "unregister_js_module", diff --git a/src/pyodide-py/pyodide/_core.py b/src/pyodide-py/pyodide/_core.py index a29fa87ab..baba6079f 100644 --- a/src/pyodide-py/pyodide/_core.py +++ b/src/pyodide-py/pyodide/_core.py @@ -1,44 +1,10 @@ -import platform -from typing import Any, Callable +import sys -if platform.system() == "Emscripten": - from _pyodide_core import ( - JsProxy, - JsException, - create_proxy, - create_once_callable, - ) -else: - # Can add shims here if we are so inclined. - class JsException(Exception): # type: ignore - """ - A wrapper around a Javascript Error to allow the Error to be thrown in Python. - """ +if "_pyodide_core" not in sys.modules: + from _pyodide import _core as _pyodide_core - # Defined in jsproxy.c - - class JsProxy: # type: ignore - """A proxy to make a Javascript object behave like a Python object""" - - # Defined in jsproxy.c - - # Defined in jsproxy.c - - def create_once_callable(obj: Callable) -> JsProxy: - """Wrap a Python callable in a Javascript function that can be called - once. After being called the proxy will decrement the reference count - of the Callable. The javascript function also has a `destroy` API that - can be used to release the proxy without calling it. - """ - return obj - - def create_proxy(obj: Any) -> JsProxy: - """Create a `JsProxy` of a `PyProxy`. - - This allows explicit control over the lifetime of the `PyProxy` from - Python: call the `destroy` API when done. - """ - return obj + sys.modules["_pyodide_core"] = _pyodide_core +from _pyodide_core import JsProxy, JsException, create_proxy, create_once_callable __all__ = ["JsProxy", "JsException", "create_proxy", "create_once_callable"] diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py index 8521fd1ec..60bfbd804 100644 --- a/src/tests/test_pyodide.py +++ b/src/tests/test_pyodide.py @@ -341,6 +341,43 @@ def test_create_proxy(selenium): ) +def test_docstrings_a(): + from _pyodide.docstring import get_cmeth_docstring, dedent_docstring + from pyodide import JsProxy + + jsproxy = JsProxy() + c_docstring = get_cmeth_docstring(jsproxy.then) + assert c_docstring == "then(onfulfilled, onrejected)\n--\n\n" + dedent_docstring( + jsproxy.then.__doc__ + ) + + +def test_docstrings_b(selenium): + from pyodide import create_once_callable, JsProxy + from _pyodide.docstring import dedent_docstring + + jsproxy = JsProxy() + ds_then_should_equal = dedent_docstring(jsproxy.then.__doc__) + sig_then_should_equal = "(onfulfilled, onrejected)" + ds_once_should_equal = dedent_docstring(create_once_callable.__doc__) + sig_once_should_equal = "(obj)" + selenium.run_js("window.a = Promise.resolve();") + [ds_then, sig_then, ds_once, sig_once] = selenium.run( + """ + from js import a + from pyodide import create_once_callable as b + [ + a.then.__doc__, a.then.__text_signature__, + b.__doc__, b.__text_signature__ + ] + """ + ) + assert ds_then == ds_then_should_equal + assert sig_then == sig_then_should_equal + assert ds_once == ds_once_should_equal + assert sig_once == sig_once_should_equal + + def test_restore_state(selenium): selenium.run_js( """ diff --git a/src/tests/test_typeconversions.py b/src/tests/test_typeconversions.py index 03c3c5ecc..abddab493 100644 --- a/src/tests/test_typeconversions.py +++ b/src/tests/test_typeconversions.py @@ -617,7 +617,7 @@ def test_to_py(selenium): return result; """ ) - assert result == "" + assert result == "" msg = "Cannot use key of type Array as a key to a Python dict" with pytest.raises(selenium.JavascriptException, match=msg):