Skip to content

Commit 5ffc454

Browse files
committed
Merge branch 'main' into feature/js-test-coverage
2 parents 1316c90 + ba9fc9d commit 5ffc454

20 files changed

+361
-110
lines changed

.github/dependabot.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# To get started with Dependabot version updates, you'll need to specify which
2+
# package ecosystems to update and where the package manifests are located.
3+
# Please see the documentation for all configuration options:
4+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5+
6+
version: 2
7+
updates:
8+
- package-ecosystem: "npm" # See documentation for possible values
9+
directory: "/python/pminit/pythonmonkey" # Location of package manifests
10+
schedule:
11+
interval: "weekly"

include/JSObjectProxy.hh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,22 @@ public:
112112
* @return bool - Whether the compared objects are equal or not
113113
*/
114114
static bool JSObjectProxy_richcompare_helper(JSObjectProxy *self, PyObject *other, std::unordered_map<PyObject *, PyObject *> &visited);
115+
116+
/**
117+
* @brief Return an iterator object to make JSObjectProxy iterable, emitting (key, value) tuples
118+
*
119+
* @param self - The JSObjectProxy
120+
* @return PyObject* - iterator object
121+
*/
122+
static PyObject *JSObjectProxy_iter(JSObjectProxy *self);
123+
124+
/**
125+
* @brief Compute a string representation of the JSObjectProxy
126+
*
127+
* @param self - The JSObjectProxy
128+
* @return the string representation (a PyUnicodeObject) on success, NULL on failure
129+
*/
130+
static PyObject *JSObjectProxy_repr(JSObjectProxy *self);
115131
};
116132

117133

include/PyProxyHandler.hh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,9 @@ public:
181181
bool delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult &result) const override;
182182
};
183183

184+
/**
185+
* @brief Convert jsid to a PyObject to be used as dict keys
186+
*/
187+
PyObject *idToKey(JSContext *cx, JS::HandleId id);
188+
184189
#endif

include/StrType.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public:
5050
* @return PyObject* - the UCS4-encoding of the pyObject string
5151
*
5252
*/
53-
PyObject *asUCS4();
53+
static PyObject *asUCS4(PyObject *pyObject);
5454
};
5555

5656
#endif

include/modules/pythonmonkey/pythonmonkey.hh

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,6 @@ void handleSharedPythonMonkeyMemory(JSContext *cx, JSGCStatus status, JS::GCReas
6464
*/
6565
static PyObject *collect(PyObject *self, PyObject *args);
6666

67-
/**
68-
* @brief Function exposed by the python module to convert UTF16 strings to UCS4 strings
69-
*
70-
* @param self - Pointer to the module object
71-
* @param args - Pointer to the python tuple of arguments (expected to contain a UTF16-encoded string as the first element)
72-
* @return PyObject* - A new python string in UCS4 encoding
73-
*/
74-
static PyObject *asUCS4(PyObject *self, PyObject *args);
75-
7667
/**
7768
* @brief Function exposed by the python module for evaluating arbitrary JS code
7869
*

include/pyTypeFactory.hh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ PyType *pyTypeFactory(PyObject *object);
3636
* @return PyType* - Pointer to a PyType object corresponding to the JS::Value
3737
*/
3838
PyType *pyTypeFactory(JSContext *cx, JS::Rooted<JSObject *> *thisObj, JS::Rooted<JS::Value> *rval);
39+
/**
40+
* @brief same to pyTypeFactory, but it's guaranteed that no error would be set on the Python error stack, instead
41+
* return `pythonmonkey.null` on error
42+
*/
43+
PyType *pyTypeFactorySafe(JSContext *cx, JS::Rooted<JSObject *> *thisObj, JS::Rooted<JS::Value> *rval);
3944

4045
/**
4146
* @brief Helper function for pyTypeFactory to create FuncTypes through PyCFunction_New
@@ -44,6 +49,6 @@ PyType *pyTypeFactory(JSContext *cx, JS::Rooted<JSObject *> *thisObj, JS::Rooted
4449
* @param args - Pointer to a PyTupleObject containing the arguments to the python function
4550
* @return PyObject* - The result of the JSFunction called with args coerced to JS types, coerced back to a PyObject type, or NULL if coercion wasn't possible
4651
*/
47-
static PyObject *callJSFunc(PyObject *JSFuncAddress, PyObject *args);
52+
PyObject *callJSFunc(PyObject *JSFuncAddress, PyObject *args);
4853

4954
#endif

python/pythonmonkey/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,18 @@
99
# Load the module by default to make `console`/`atob`/`btoa` globally available
1010
require("console")
1111
require("base64")
12+
13+
# Add the `.keys()` method on `Object.prototype` to get JSObjectProxy dict() conversion working
14+
# Conversion from a dict-subclass to a strict dict by `dict(subclass)` internally calls the .keys() method to read the dictionary keys,
15+
# but .keys on a JSObjectProxy can only come from the JS side
16+
pm.eval("""
17+
(makeList) => {
18+
const keysMethod = {
19+
get() {
20+
return () => makeList(...Object.keys(this))
21+
}
22+
}
23+
Object.defineProperty(Object.prototype, "keys", keysMethod)
24+
Object.defineProperty(Array.prototype, "keys", keysMethod)
25+
}
26+
""")(lambda *args: list(args))

python/pythonmonkey/cli/pmjs.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys, os, readline, signal, getopt
77
import pythonmonkey as pm
88
globalThis = pm.eval("globalThis")
9-
evalOptions = { 'strict': False }
9+
evalOpts = { 'filename': __file__, 'fromPythonFrame': True, 'strict': False }
1010

1111
if (os.getenv('PMJS_PATH')):
1212
requirePath = list(map(os.path.abspath, os.getenv('PMJS_PATH').split(':')))
@@ -42,12 +42,23 @@
4242
}
4343
4444
if (cmd === '')
45+
{
4546
return;
47+
}
48+
49+
try {
50+
if (arguments[0] === 'from' || arguments[0] === 'import')
51+
{
52+
return python.exec(cmd);
53+
}
4654
47-
if (arguments[0] === 'from' || arguments[0] === 'import')
48-
return python.exec(cmd);
49-
50-
const retval = python.eval(cmd);
55+
const retval = python.eval(cmd);
56+
}
57+
catch(error) {
58+
globalThis._error = error;
59+
return util.inspect(error);
60+
}
61+
5162
pythonCmd.serial = (pythonCmd.serial || 0) + 1;
5263
globalThis['$' + pythonCmd.serial] = retval;
5364
python.stdout.write('$' + pythonCmd.serial + ' = ');
@@ -130,7 +141,7 @@
130141
return util.inspect(error);
131142
}
132143
}
133-
""");
144+
""", evalOpts);
134145

135146
def repl():
136147
"""
@@ -322,21 +333,20 @@ def main():
322333
print(pm.__version__)
323334
sys.exit()
324335
elif o in ("--use-strict"):
325-
evalOptions['strict'] = True
336+
evalOpts['strict'] = True
326337
elif o in ("-h", "--help"):
327338
usage()
328339
sys.exit()
329340
elif o in ("-i", "--interactive"):
330341
forceRepl = True
331342
elif o in ("-e", "--eval"):
332-
pm.eval(a, evalOptions)
343+
pm.eval(a, evalOpts)
333344
enterRepl = False
334345
elif o in ("-p", "--print"):
335-
print(pm.eval(a, evalOptions))
346+
print(pm.eval(a, evalOpts))
336347
enterRepl = False
337348
elif o in ("-r", "--require"):
338349
globalThis.require(a)
339-
# pm.eval('require')(a)
340350
else:
341351
assert False, "unhandled option"
342352

python/pythonmonkey/pythonmonkey.pyi

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@ stub file for type hints & documentations for the native module
55

66
import typing as _typing
77

8+
class EvalOptions(_typing.TypedDict, total=False):
9+
filename: str
10+
lineno: int
11+
column: int
12+
mutedErrors: bool
13+
noScriptRval: bool
14+
selfHosting: bool
15+
strict: bool
16+
module: bool
17+
fromPythonFrame: bool
18+
819
# pylint: disable=redefined-builtin
9-
def eval(code: str, /) -> _typing.Any:
20+
def eval(code: str, evalOpts: EvalOptions = {}, /) -> _typing.Any:
1021
"""
1122
JavaScript evaluator in Python
1223
"""
@@ -16,15 +27,6 @@ def collect() -> None:
1627
Calls the spidermonkey garbage collector
1728
"""
1829

19-
@_typing.overload
20-
def asUCS4(utf16_str: str, /) -> str:
21-
"""
22-
Expects a python string in UTF16 encoding, and returns a new equivalent string in UCS4.
23-
Undefined behaviour if the string is not in UTF16.
24-
"""
25-
@_typing.overload
26-
def asUCS4(anything_else: _typing.Any, /) -> _typing.NoReturn: ...
27-
2830
class bigint(int):
2931
"""
3032
Representing JavaScript BigInt in Python

python/pythonmonkey/require.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@
4040
"node_modules"
4141
)
4242
)
43+
evalOpts = { 'filename': __file__, 'fromPythonFrame': True }
4344

4445
# Add some python functions to the global python object for code in this file to use.
45-
globalThis = pm.eval("globalThis;")
46-
pm.eval("globalThis.python = { pythonMonkey: {}, stdout: {}, stderr: {} }")
46+
globalThis = pm.eval("globalThis;", evalOpts)
47+
pm.eval("globalThis.python = { pythonMonkey: {}, stdout: {}, stderr: {} }", evalOpts);
4748
globalThis.pmEval = pm.eval
4849
globalThis.python.pythonMonkey.dir = os.path.dirname(__file__)
4950
#globalThis.python.pythonMonkey.version = pm.__version__
@@ -59,15 +60,15 @@
5960
globalThis.python.exec = exec
6061
globalThis.python.getenv = os.getenv
6162
globalThis.python.paths = ':'.join(sys.path)
62-
pm.eval("python.paths = python.paths.split(':');"); # fix when pm supports arrays
63+
pm.eval("python.paths = python.paths.split(':');", evalOpts); # fix when pm supports arrays
6364

6465
globalThis.python.exit = pm.eval("""'use strict';
6566
(exit) => function pythonExitWrapper(exitCode) {
6667
if (typeof exitCode === 'number')
6768
exitCode = BigInt(Math.floor(exitCode));
6869
exit(exitCode);
6970
}
70-
""")(sys.exit);
71+
""", evalOpts)(sys.exit);
7172

7273
# bootstrap is effectively a scoping object which keeps us from polluting the global JS scope.
7374
# The idea is that we hold a reference to the bootstrap object in Python-load, for use by the
@@ -78,7 +79,7 @@
7879
7980
const bootstrap = {
8081
modules: {
81-
vm: { runInContext: eval },
82+
vm: {},
8283
'ctx-module': {},
8384
},
8485
}
@@ -92,7 +93,7 @@
9293
throw new Error('module not found: ' + mid);
9394
}
9495
95-
bootstrap.modules.vm.runInContext_broken = function runInContext(code, _unused_contextifiedObject, options)
96+
bootstrap.modules.vm.runInContext = function runInContext(code, _unused_contextifiedObject, options)
9697
{
9798
var evalOptions = {};
9899
@@ -169,7 +170,7 @@
169170
globalThis.bootstrap = bootstrap;
170171
171172
return bootstrap;
172-
})(globalThis.python)""")
173+
})(globalThis.python)""", evalOpts)
173174

174175
def statSync_inner(filename: str) -> Union[Dict[str, int], bool]:
175176
"""
@@ -208,15 +209,18 @@ def existsSync(filename: str) -> bool:
208209
# require and exports symbols injected from the bootstrap object above. Current PythonMonkey bugs
209210
# prevent us from injecting names properly so they are stolen from trail left behind in the global
210211
# scope until that can be fixed.
212+
#
213+
# lineno should be -5 but jsapi 102 uses unsigned line numbers, so we take the newlines out of the
214+
# wrapper prologue to make stack traces line up.
211215
with open(node_modules + "/ctx-module/ctx-module.js", "r") as ctxModuleSource:
212216
initCtxModule = pm.eval("""'use strict';
213217
(function moduleWrapper_forCtxModule(broken_require, broken_exports)
214218
{
215219
const require = bootstrap.require;
216220
const exports = bootstrap.modules['ctx-module'];
217-
""" + ctxModuleSource.read() + """
221+
""".replace("\n", " ") + "\n" + ctxModuleSource.read() + """
218222
})
219-
""");
223+
""", { 'filename': node_modules + "/ctx-module/ctx-module.js", 'lineno': 0 });
220224
#broken initCtxModule(bootstrap.require, bootstrap.modules['ctx-module'].exports)
221225
initCtxModule();
222226

@@ -298,7 +302,7 @@ def _createRequireInner(*args):
298302
module.require.path.splice(module.require.path.length, 0, ...(extraPaths.split(':')));
299303
300304
return module.require;
301-
})""")(*args)
305+
})""", evalOpts)(*args)
302306

303307
def createRequire(filename, extraPaths: Union[List[str], Literal[False]] = False, isMain = False):
304308
"""
@@ -331,7 +335,7 @@ def runProgramModule(filename, argv, extraPaths=[]):
331335
globalThis.__filename = fullFilename;
332336
globalThis.__dirname = os.path.dirname(fullFilename);
333337
with open(fullFilename, encoding="utf-8", mode="r") as mainModuleSource:
334-
pm.eval(mainModuleSource.read())
338+
pm.eval(mainModuleSource.read(), {'filename': fullFilename})
335339

336340
def require(moduleIdentifier: str):
337341
# Retrieve the caller’s filename from the call stack

0 commit comments

Comments
 (0)