mirror of https://github.com/pyodide/pyodide.git
Automatically generatate C method->ml_doc docstrings from Python (#1352)
Co-authored-by: Roman Yurchak <rth.yurchak@gmail.com>
This commit is contained in:
parent
d175636851
commit
e408dede25
2
Makefile
2
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."
|
||||
|
|
|
@ -9,4 +9,5 @@ Backward compatibility of the API is not guaranteed at this point.
|
|||
.. automodule:: pyodide
|
||||
:members:
|
||||
:autosummary:
|
||||
```
|
||||
:autosummary-no-nesting:
|
||||
```
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#ifndef DOCSTRING_H
|
||||
#define DOCSTRING_H
|
||||
|
||||
int
|
||||
set_method_docstring(PyMethodDef* method, PyObject* parent);
|
||||
|
||||
int
|
||||
docstring_init();
|
||||
|
||||
#endif /* DOCSTRING_H */
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include <emscripten.h>
|
||||
#include <stdalign.h>
|
||||
|
||||
#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;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "error_handling.h"
|
||||
#include <emscripten.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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__)
|
|
@ -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",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
@ -617,7 +617,7 @@ def test_to_py(selenium):
|
|||
return result;
|
||||
"""
|
||||
)
|
||||
assert result == "<class 'JsProxy'>"
|
||||
assert result == "<class 'pyodide.JsProxy'>"
|
||||
|
||||
msg = "Cannot use key of type Array as a key to a Python dict"
|
||||
with pytest.raises(selenium.JavascriptException, match=msg):
|
||||
|
|
Loading…
Reference in New Issue