Skip to content

Commit 4cfd707

Browse files
Merge branch 'main' into caleb/fix/variadic
2 parents 7b6e71f + 05795f1 commit 4cfd707

File tree

8 files changed

+189
-24
lines changed

8 files changed

+189
-24
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ They are largely based on SpiderMonkey's `CompileOptions`. The supported option
164164
- `filename`: set the filename of this code for the purposes of generating stack traces etc.
165165
- `lineno`: set the line number offset of this code for the purposes of generating stack traces etc.
166166
- `column`: set the column number offset of this code for the purposes of generating stack traces etc.
167-
- `mutedErrors`: experimental
168-
- `noScriptRval`: experimental
169-
- `selfHosting`: experimental
170-
- `strict`: experimental
171-
- `module`: experimental
167+
- `mutedErrors`: if set to `True`, eval errors or unhandled rejections are ignored ("muted"). Default `False`.
168+
- `noScriptRval`: if `False`, return the last expression value of the script as the result value to the caller. Default `False`.
169+
- `selfHosting`: *experimental*
170+
- `strict`: forcibly evaluate in strict mode (`"use strict"`). Default `False`.
171+
- `module`: indicate the file is an ECMAScript module (always strict mode code and disallow HTML comments). Default `False`.
172172
- `fromPythonFrame`: generate the equivalent of filename, lineno, and column based on the location of
173173
the Python call to eval. This makes it possible to evaluate Python multiline string literals and
174174
generate stack traces in JS pointing to the error in the Python source file.

python/pminit/pminit/cli.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ def execute(cmd: str, cwd: str):
1111

1212
popen.stdout.close()
1313
return_code = popen.wait()
14-
if return_code:
15-
raise subprocess.CalledProcessError(return_code, cmd)
14+
if return_code != 0:
15+
sys.exit(return_code)
1616

1717
def commandType(value: str):
1818
if value != "npm":
@@ -34,7 +34,3 @@ def main():
3434
)
3535

3636
execute(' '.join( args.executable + args.args ), pythonmonkey_path)
37-
38-
39-
40-

python/pythonmonkey/helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,19 @@ def new(ctor):
4343
return (lambda *args: newCtor(list(args)))
4444

4545

46+
def simpleUncaughtExceptionHandler(loop, context):
47+
"""
48+
A simple exception handler for uncaught JS Promise rejections sent to the Python event-loop
49+
50+
See https://docs.python.org/3.11/library/asyncio-eventloop.html#error-handling-api
51+
"""
52+
error = context["exception"]
53+
pm.eval("(err) => console.error('Uncaught', err)")(error)
54+
pm.stop() # unblock `await pm.wait()` to gracefully exit the program
55+
56+
4657
# List which symbols are exposed to the pythonmonkey module.
47-
__all__ = ["new", "typeof"]
58+
__all__ = ["new", "typeof", "simpleUncaughtExceptionHandler"]
4859

4960
# Add the non-enumerable properties of globalThis which don't collide with pythonmonkey.so as exports:
5061
globalThis = pm.eval('globalThis')

python/pythonmonkey/pythonmonkey.pyi

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""
2+
stub file for type hints & documentations for the native module
23
@see https://typing.readthedocs.io/en/latest/source/stubs.html
34
"""
45

@@ -25,6 +26,25 @@ def eval(code: str, evalOpts: EvalOptions = {}, /) -> _typing.Any:
2526
"""
2627

2728

29+
def require(moduleIdentifier: str, /) -> JSObjectProxy:
30+
"""
31+
Return the exports of a CommonJS module identified by `moduleIdentifier`, using standard CommonJS semantics
32+
"""
33+
34+
35+
def new(ctor: JSFunctionProxy) -> _typing.Callable[..., _typing.Any]:
36+
"""
37+
Wrap the JS new operator, emitting a lambda which constructs a new
38+
JS object upon invocation
39+
"""
40+
41+
42+
def typeof(jsval: _typing.Any, /):
43+
"""
44+
This is the JS `typeof` operator, wrapped in a function so that it can be used easily from Python.
45+
"""
46+
47+
2848
def wait() -> _typing.Awaitable[None]:
2949
"""
3050
Block until all asynchronous jobs (Promise/setTimeout/etc.) finish.
@@ -43,6 +63,12 @@ def stop() -> None:
4363
"""
4464

4565

66+
def runProgramModule(filename: str, argv: _typing.List[str], extraPaths: _typing.List[str] = []) -> None:
67+
"""
68+
Load and evaluate a program (main) module. Program modules must be written in JavaScript.
69+
"""
70+
71+
4672
def isCompilableUnit(code: str) -> bool:
4773
"""
4874
Hint if a string might be compilable Javascript without actual evaluation
@@ -55,6 +81,14 @@ def collect() -> None:
5581
"""
5682

5783

84+
def internalBinding(namespace: str) -> JSObjectProxy:
85+
"""
86+
INTERNAL USE ONLY
87+
88+
See function declarations in ./builtin_modules/internal-binding.d.ts
89+
"""
90+
91+
5892
class JSFunctionProxy():
5993
"""
6094
JavaScript Function proxy
@@ -81,13 +115,60 @@ class JSMethodProxy(JSFunctionProxy, object):
81115
print(myObject.value) # 42.0
82116
"""
83117

84-
def __init__(self) -> None:
85-
"""
86-
PythonMonkey init function
87-
"""
118+
def __init__(self) -> None: "deleted"
119+
120+
121+
class JSObjectProxy(dict):
122+
"""
123+
JavaScript Object proxy dict
124+
"""
125+
126+
def __init__(self) -> None: "deleted"
127+
128+
129+
class JSArrayProxy(list):
130+
"""
131+
JavaScript Array proxy
132+
"""
133+
134+
def __init__(self) -> None: "deleted"
135+
136+
137+
class JSArrayIterProxy(_typing.Iterator):
138+
"""
139+
JavaScript Array Iterator proxy
140+
"""
141+
142+
def __init__(self) -> None: "deleted"
143+
144+
145+
class JSStringProxy(str):
146+
"""
147+
JavaScript String proxy
148+
"""
149+
150+
def __init__(self) -> None: "deleted"
151+
152+
153+
class bigint(int):
154+
"""
155+
Representing JavaScript BigInt in Python
156+
"""
157+
158+
159+
class SpiderMonkeyError(Exception):
160+
"""
161+
Representing a corresponding JS Error in Python
162+
"""
88163

89164

90165
null = _typing.Annotated[
91-
_typing.NewType("pythonmonkey.null", object),
92-
"Representing the JS null type in Python using a singleton object",
166+
_typing.NewType("pythonmonkey.null", object),
167+
"Representing the JS null type in Python using a singleton object",
168+
]
169+
170+
171+
globalThis = _typing.Annotated[
172+
JSObjectProxy,
173+
"A Python Dict which is equivalent to the globalThis object in JavaScript",
93174
]

python/pythonmonkey/require.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ def runProgramModule(filename, argv, extraPaths=[]):
409409
globalThis.__filename = fullFilename
410410
globalThis.__dirname = os.path.dirname(fullFilename)
411411
with open(fullFilename, encoding="utf-8", mode="r") as mainModuleSource:
412-
pm.eval(mainModuleSource.read(), {'filename': fullFilename})
412+
pm.eval(mainModuleSource.read(), {'filename': fullFilename, 'noScriptRval': True})
413+
# forcibly run in file mode. We shouldn't be getting the last expression of the script as the result value.
413414

414415
# The pythonmonkey require export. Every time it is used, the stack is inspected so that the filename
415416
# passed to createRequire is correct. This is necessary so that relative requires work. If the filename

src/JobQueue.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
#include "include/JobQueue.hh"
12+
#include "include/modules/pythonmonkey/pythonmonkey.hh"
1213

1314
#include "include/PyEventLoop.hh"
1415
#include "include/pyTypeFactory.hh"
@@ -134,6 +135,21 @@ void JobQueue::promiseRejectionTracker(JSContext *cx,
134135
return;
135136
}
136137

138+
// Test if there's no user-defined (or pmjs defined) exception handler on the Python event-loop
139+
PyEventLoop loop = PyEventLoop::getRunningLoop();
140+
if (!loop.initialized()) return;
141+
PyObject *customHandler = PyObject_GetAttrString(loop._loop, "_exception_handler"); // see https://github.com/python/cpython/blob/v3.9.16/Lib/asyncio/base_events.py#L1782
142+
if (customHandler == Py_None) { // we only have the default exception handler
143+
// Set an exception handler to the event-loop
144+
PyObject *pmModule = PyImport_ImportModule("pythonmonkey");
145+
PyObject *exceptionHandler = PyObject_GetAttrString(pmModule, "simpleUncaughtExceptionHandler");
146+
PyObject_CallMethod(loop._loop, "set_exception_handler", "O", exceptionHandler);
147+
Py_DECREF(pmModule);
148+
Py_DECREF(exceptionHandler);
149+
}
150+
Py_DECREF(customHandler);
151+
152+
// Go ahead and send this unhandled Promise rejection to the exception handler on the Python event-loop
137153
PyObject *pyFuture = PromiseType::getPyObject(cx, promise); // ref count == 2
138154
// Unhandled Future object calls the event-loop exception handler in its destructor (the `__del__` magic method)
139155
// See https://github.com/python/cpython/blob/v3.9.16/Lib/asyncio/futures.py#L108

src/pyTypeFactory.cc

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
#include <js/ValueArray.h>
3838

3939
PyObject *pyTypeFactory(JSContext *cx, JS::HandleValue rval) {
40+
std::string errorString;
41+
4042
if (rval.isUndefined()) {
4143
return NoneType::getPyObject();
4244
}
@@ -53,7 +55,7 @@ PyObject *pyTypeFactory(JSContext *cx, JS::HandleValue rval) {
5355
return StrType::getPyObject(cx, rval);
5456
}
5557
else if (rval.isSymbol()) {
56-
printf("symbol type is not handled by PythonMonkey yet");
58+
errorString = "symbol type is not handled by PythonMonkey yet.\n";
5759
}
5860
else if (rval.isBigInt()) {
5961
return IntType::getPyObject(cx, rval.toBigInt());
@@ -117,12 +119,17 @@ PyObject *pyTypeFactory(JSContext *cx, JS::HandleValue rval) {
117119
return DictType::getPyObject(cx, rval);
118120
}
119121
else if (rval.isMagic()) {
120-
printf("magic type is not handled by PythonMonkey yet\n");
122+
errorString = "magic type is not handled by PythonMonkey yet.\n";
121123
}
122124

123-
std::string errorString("pythonmonkey cannot yet convert Javascript value of: ");
124-
JS::RootedString str(cx, JS::ToString(cx, rval));
125-
errorString += JS_EncodeStringToUTF8(cx, str).get();
125+
errorString += "pythonmonkey cannot yet convert Javascript value of: ";
126+
JSString *valToStr = JS::ToString(cx, rval);
127+
if (!valToStr) { // `JS::ToString` returns `nullptr` for JS symbols, see https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/js/src/vm/StringType.cpp#l2208
128+
// TODO (Tom Tang): Revisit this once we have Symbol coercion support
129+
valToStr = JS_ValueToSource(cx, rval);
130+
}
131+
JS::RootedString valToStrRooted(cx, valToStr);
132+
errorString += JS_EncodeStringToUTF8(cx, valToStrRooted).get();
126133
PyErr_SetString(PyExc_TypeError, errorString.c_str());
127134
return NULL;
128135
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#! /bin/bash
2+
#
3+
# @file uncaught-rejection-handler.bash
4+
# For testing if the actual JS error gets printed out for uncaught Promise rejections,
5+
# instead of printing out a Python `Future exception was never retrieved` error message when not in pmjs
6+
#
7+
# @author Tom Tang ([email protected])
8+
# @date June 2024
9+
10+
set -u
11+
set -o pipefail
12+
13+
panic()
14+
{
15+
echo "FAIL: $*" >&2
16+
exit 2
17+
}
18+
19+
cd `dirname "$0"` || panic "could not change to test directory"
20+
21+
code='
22+
import asyncio
23+
import pythonmonkey as pm
24+
25+
async def pythonmonkey_main():
26+
pm.eval("""void Promise.reject(new TypeError("abc"));""")
27+
await pm.wait()
28+
29+
asyncio.run(pythonmonkey_main())
30+
'
31+
32+
OUTPUT=$(python -c "$code" \
33+
< /dev/null 2>&1
34+
)
35+
36+
echo "$OUTPUT" \
37+
| tr -d '\r' \
38+
| (grep -c 'Future exception was never retrieved' || true) \
39+
| while read qty
40+
do
41+
echo "$OUTPUT"
42+
[ "$qty" != "0" ] && panic "There shouldn't be a 'Future exception was never retrieved' error massage"
43+
break
44+
done || exit $?
45+
46+
echo "$OUTPUT" \
47+
| tr -d '\r' \
48+
| grep -c 'Uncaught TypeError: abc' \
49+
| while read qty
50+
do
51+
[ "$qty" != "1" ] && panic "It should print out 'Uncaught TypeError' directly"
52+
break
53+
done || exit $?

0 commit comments

Comments
 (0)