Automatically generatate C method->ml_doc docstrings from Python (#1352)

Co-authored-by: Roman Yurchak <rth.yurchak@gmail.com>
This commit is contained in:
Hood Chatham 2021-03-24 02:24:06 -07:00 committed by GitHub
parent d175636851
commit e408dede25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 437 additions and 123 deletions

View File

@ -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."

View File

@ -9,4 +9,5 @@ Backward compatibility of the API is not guaranteed at this point.
.. automodule:: pyodide
:members:
:autosummary:
```
:autosummary-no-nesting:
```

52
src/core/docstring.c Normal file
View File

@ -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;
}

10
src/core/docstring.h Normal file
View File

@ -0,0 +1,10 @@
#ifndef DOCSTRING_H
#define DOCSTRING_H
int
set_method_docstring(PyMethodDef* method, PyObject* parent);
int
docstring_init();
#endif /* DOCSTRING_H */

View File

@ -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

View File

@ -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.
*/

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

View File

@ -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

View File

@ -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__)

View File

@ -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",

View File

@ -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"]

View File

@ -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(
"""

View File

@ -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):