├── python ├── pythonmonkey │ ├── py.typed │ ├── cli │ │ └── __init__.py │ ├── builtin_modules │ │ ├── internal-binding.py │ │ ├── dom-exception.js │ │ ├── url.js │ │ ├── base64.d.ts │ │ ├── base64.py │ │ ├── XMLHttpRequest-internal.d.ts │ │ ├── internal-binding.d.ts │ │ └── dom-exception.d.ts │ ├── __init__.py │ ├── tsconfig.json │ ├── lib │ │ ├── wtfpm.py │ │ └── pmjs │ │ │ └── global-init.js │ ├── helpers.py │ ├── global.d.ts │ └── pythonmonkey.pyi └── pminit │ ├── .gitignore │ ├── pminit │ ├── __init__.py │ └── cli.py │ ├── pythonmonkey │ └── package.json │ ├── pyproject.toml │ └── post-install-hook.py ├── mozcentral.version ├── tests ├── js │ ├── modules │ │ ├── collide.js │ │ ├── collide.py │ │ ├── print-load.js │ │ ├── python-cjs-module.py │ │ ├── cjs-module.js │ │ └── vm-tools.py │ ├── resources │ │ └── eval-test.js │ ├── program-exit.js │ ├── program-throw.js │ ├── timer-throw.js │ ├── timers-natural-exit.js │ ├── load-cjs-module.simple │ ├── timer-reject.js │ ├── throw-filename.js │ ├── load-cjs-python-module.simple │ ├── timers-segfault.simple │ ├── timers-force-exit.simple │ ├── use-strict.simple │ ├── not-strict-mode.simple │ ├── py2js │ │ ├── trivial-function.simple │ │ ├── object.simple │ │ ├── string.simple │ │ ├── integer.simple │ │ ├── array-change-index.simple │ │ ├── higher-order-function.simple │ │ ├── arraybuffer.simple │ │ ├── promise.simple.failing │ │ ├── error.simple │ │ ├── function-curry.simple.failing │ │ ├── boolean.simple │ │ ├── object-methods.simple │ │ └── datetime.simple │ ├── is-compilable-unit.simple │ ├── js2py │ │ ├── trivial-function.simple │ │ ├── object.simple │ │ ├── string.simple │ │ ├── promise.simple │ │ ├── higher-order-function.simple │ │ ├── bigint.simple │ │ ├── boolean.simple │ │ ├── arraybuffer.simple │ │ ├── promise-await-in-python.simple │ │ ├── object-mutation.simple │ │ ├── function-curry.simple │ │ ├── array-change-index.simple │ │ ├── datetime.simple │ │ ├── error.simple │ │ └── datetime2.simple │ ├── eval-test.simple │ ├── console-smoke.simple │ ├── program.js │ ├── timers-natural-exit.bash │ ├── collide.simple │ ├── program-throw.bash │ ├── util-module.simple │ ├── program-exit.bash │ ├── pmjs-popt.bash │ ├── pmjs-eopt.bash │ ├── pmjs-ropt.bash │ ├── require-module-stack.bash.failing │ ├── set-timeout.simple │ ├── timer-throw.bash │ ├── set-interval.bash │ ├── console-stdio.bash │ ├── console-this.simple │ ├── timer-reject.bash │ ├── pmjs-require-cache.bash │ ├── program-module.simple │ ├── typeofs-segfaults.simple.failing │ ├── pmjs-global-arguments.bash │ ├── uncaught-rejection-handler.bash │ ├── pmjs-interactive-smoke.bash │ ├── commonjs-modules.bash │ ├── typeofs.simple │ ├── test-atob-btoa.simple │ ├── quint.js │ └── xhr-http-keep-alive.bash └── python │ ├── conftest.py │ ├── test_finalizationregistry.py │ ├── test_reentrance_smoke.py │ ├── test_list_array.py │ ├── test_xhr.py │ └── test_functions.py ├── cmake └── docs │ ├── favicon.ico │ ├── PythonMonkey-icon.png │ ├── CMakeLists.txt │ └── Doxyfile.in ├── examples ├── use-python-module │ ├── index.js │ └── my-python-module.py ├── use-require │ ├── test1.js │ └── test2.js ├── use-python-module.py └── use-require.py ├── .gitmodules ├── src ├── NoneType.cc ├── BoolType.cc ├── FloatType.cc ├── NullType.cc ├── FuncType.cc ├── ListType.cc ├── DictType.cc ├── JSStringProxy.cc ├── CMakeLists.txt ├── PyBaseProxyHandler.cc ├── DateType.cc ├── JSFunctionProxy.cc ├── internalBinding.cc ├── JSArrayIterProxy.cc ├── JSMethodProxy.cc ├── JSObjectIterProxy.cc ├── JSObjectItemsProxy.cc └── PyDictProxyHandler.cc ├── .git-blame-ignore-revs ├── .gitignore ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_pythonmonkey.yaml │ └── bug_pythonmonkey.yaml └── workflows │ └── update-mozcentral-version.yaml ├── include ├── NoneType.hh ├── BoolType.hh ├── FloatType.hh ├── NullType.hh ├── internalBinding.hh ├── FuncType.hh ├── ListType.hh ├── PyIterableProxyHandler.hh ├── DictType.hh ├── pyTypeFactory.hh ├── DateType.hh ├── setSpiderMonkeyException.hh ├── IntType.hh ├── ExceptionType.hh ├── StrType.hh ├── PyBaseProxyHandler.hh ├── PyListProxyHandler.hh ├── PyBytesProxyHandler.hh ├── PromiseType.hh ├── JSFunctionProxy.hh ├── JSMethodProxy.hh ├── JSStringProxy.hh ├── BufferType.hh ├── modules │ └── pythonmonkey │ │ └── pythonmonkey.hh ├── jsTypeFactory.hh ├── JSArrayIterProxy.hh └── JSObjectIterProxy.hh ├── .vscode ├── c_cpp_properties.json ├── launch.json └── tasks.json ├── Makefile └── pyproject.toml /python/pythonmonkey/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /python/pythonmonkey/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/pminit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /mozcentral.version: -------------------------------------------------------------------------------- 1 | 6bca861985ba51920c1cacc21986af01c51bd690 2 | -------------------------------------------------------------------------------- /tests/js/modules/collide.js: -------------------------------------------------------------------------------- 1 | exports.which = 'javascript'; 2 | -------------------------------------------------------------------------------- /tests/js/modules/collide.py: -------------------------------------------------------------------------------- 1 | exports['which'] = "python" 2 | -------------------------------------------------------------------------------- /tests/js/modules/print-load.js: -------------------------------------------------------------------------------- 1 | console.log('LOADED', __filename); 2 | -------------------------------------------------------------------------------- /python/pminit/pminit/__init__.py: -------------------------------------------------------------------------------- 1 | # Placeholder 2 | # A __init__.py file is required by pip 3 | -------------------------------------------------------------------------------- /tests/js/resources/eval-test.js: -------------------------------------------------------------------------------- 1 | console.log('eval-test.js loaded from', __filename); 2 | 18436572; 3 | -------------------------------------------------------------------------------- /cmake/docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Distributive-Network/PythonMonkey/HEAD/cmake/docs/favicon.ico -------------------------------------------------------------------------------- /examples/use-python-module/index.js: -------------------------------------------------------------------------------- 1 | const { helloWorld } = require('./my-python-module'); 2 | helloWorld() 3 | 4 | -------------------------------------------------------------------------------- /cmake/docs/PythonMonkey-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Distributive-Network/PythonMonkey/HEAD/cmake/docs/PythonMonkey-icon.png -------------------------------------------------------------------------------- /examples/use-require/test1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const makeOutput = require('./test2').makeOutput; 4 | 5 | makeOutput('hello world'); 6 | -------------------------------------------------------------------------------- /examples/use-python-module/my-python-module.py: -------------------------------------------------------------------------------- 1 | def helloWorld(): 2 | print('hello, world!') 3 | 4 | 5 | exports['helloWorld'] = helloWorld 6 | -------------------------------------------------------------------------------- /examples/use-require/test2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.makeOutput = function makeOutput() 4 | { 5 | const argv = Array.from(arguments); 6 | argv.unshift('TEST OUTPUT: '); 7 | python.print.apply(null, argv); 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/internal-binding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Re-export `internalBinding` to JS 3 | """ 4 | 5 | import pythonmonkey as pm 6 | 7 | """ 8 | See function declarations in ./internal-binding.d.ts 9 | """ 10 | exports = pm.internalBinding # type: ignore 11 | -------------------------------------------------------------------------------- /tests/js/program-exit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file program-exit.js 3 | * Support code program-exit.bash 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | setTimeout(()=>console.log('hey'), 10400) 8 | python.exit(99) 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/commonjs-official"] 2 | path = tests/commonjs-official 3 | url = https://github.com/commonjs/commonjs.git 4 | [submodule "cmake/docs/doxygen-awesome-css"] 5 | path = cmake/docs/doxygen-awesome-css 6 | url = https://github.com/jothepro/doxygen-awesome-css.git 7 | -------------------------------------------------------------------------------- /examples/use-python-module.py: -------------------------------------------------------------------------------- 1 | # @file use-require.py 2 | # Sample code which demonstrates how to use require 3 | # @author Wes Garland, wes@distributive.network 4 | # @date Jun 2023 5 | 6 | import pythonmonkey as pm 7 | 8 | pm.require('./use-python-module') 9 | -------------------------------------------------------------------------------- /tests/js/program-throw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file program-throw.js 3 | * Support code program-throw.bash 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | 8 | setTimeout(() => console.error('goodbye'), 6000) 9 | throw new Error('hello') 10 | -------------------------------------------------------------------------------- /tests/js/timer-throw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file timer-throw.js 3 | * Support code timer-throw.bash 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | 8 | setTimeout(() => { throw new Error('goodbye') }, 600); 9 | console.error('hello'); 10 | -------------------------------------------------------------------------------- /examples/use-require.py: -------------------------------------------------------------------------------- 1 | # @file use-require.py 2 | # Sample code which demonstrates how to use require 3 | # @author Wes Garland, wes@distributive.network 4 | # @date Jun 2023 5 | 6 | import pythonmonkey as pm 7 | 8 | pm.require('./use-require/test1') 9 | print("Done") 10 | -------------------------------------------------------------------------------- /tests/js/modules/python-cjs-module.py: -------------------------------------------------------------------------------- 1 | # @file python-cjs-module.py 2 | # A CommonJS Module written Python. 3 | # @author Wes Garland, wes@distributive.network 4 | # @date Jun 2023 5 | 6 | def helloWorld(): 7 | print('hello, world!') 8 | 9 | 10 | exports['helloWorld'] = helloWorld 11 | -------------------------------------------------------------------------------- /tests/js/timers-natural-exit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file timers-natural-exit.js 3 | * Support code timers-natural-exit.bash 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | 8 | setTimeout(()=>console.log('fired timer'), 500); 9 | console.log('end of program') 10 | -------------------------------------------------------------------------------- /tests/js/load-cjs-module.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file load-cjs-module.js 3 | * Simple smoke test which ensures that we can load a CommonJS Module 4 | * @author Wes Garland, wes@distributive.network 5 | * @date June 2023 6 | */ 7 | 8 | const { helloWorld } = require('./modules/cjs-module'); 9 | 10 | helloWorld(); 11 | -------------------------------------------------------------------------------- /tests/js/modules/cjs-module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cjs-module.js 3 | * A CommonJS Module written in JavaScript 4 | * @author Wes Garland, wes@distributive.network 5 | * @date Jun 2023 6 | */ 7 | 8 | function helloWorld() 9 | { 10 | console.log('hello, world!') 11 | } 12 | 13 | exports.helloWorld = helloWorld; 14 | -------------------------------------------------------------------------------- /tests/js/timer-reject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file timer-reject.js 3 | * Support code timer-reject.bash 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | 8 | setTimeout(async () => { throw new Error('goodbye') }, 600); 9 | setTimeout(async () => { console.warn('this should not fire') }, 2000); 10 | console.error('hello'); 11 | -------------------------------------------------------------------------------- /tests/js/throw-filename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file throw-filename.js 3 | * A helper that throws when loaded, so that we can test that loading things throws with the right filename. 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | try 8 | { 9 | throw new Error('lp0 on fire'); 10 | } 11 | catch(error) 12 | { 13 | console.log(error.stack); 14 | } 15 | -------------------------------------------------------------------------------- /tests/python/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pythonmonkey as pm 3 | import gc 4 | 5 | # This is run at the end of each test function 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def teardown_function(): 10 | """ 11 | Forcing garbage collection (twice) whenever a test function finishes, 12 | to locate GC-related errors 13 | """ 14 | gc.collect(), pm.collect() 15 | gc.collect(), pm.collect() 16 | -------------------------------------------------------------------------------- /tests/js/modules/vm-tools.py: -------------------------------------------------------------------------------- 1 | # @file vm-tools.py 2 | # A CommonJS Module written Python which feeds some Python- and JS-VM-related methods 3 | # back up to JS so we can use them during tests. 4 | # @author Wes Garland, wes@distributive.network 5 | # @date Jun 2023 6 | 7 | import pythonmonkey as pm 8 | 9 | exports['isCompilableUnit'] = pm.isCompilableUnit 10 | print('MODULE LOADED') 11 | -------------------------------------------------------------------------------- /src/NoneType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NoneType.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing None 5 | * @date 2023-02-22 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/NoneType.hh" 12 | 13 | PyObject *NoneType::getPyObject() { 14 | Py_INCREF(Py_None); 15 | return Py_None; 16 | } -------------------------------------------------------------------------------- /src/BoolType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file BoolType.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python bools 5 | * @date 2022-12-02 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/BoolType.hh" 12 | 13 | 14 | PyObject *BoolType::getPyObject(long n) { 15 | return PyBool_FromLong(n); 16 | } -------------------------------------------------------------------------------- /tests/js/load-cjs-python-module.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file load-cjs-python-module.js 3 | * Simple smoke test which ensures that we can load a CommonJS Module which happens to be 4 | * written in Python instead of JavaScript 5 | * @author Wes Garland, wes@distributive.network 6 | * @date June 2023 7 | */ 8 | 9 | const { helloWorld } = require('./modules/python-cjs-module'); 10 | 11 | helloWorld(); 12 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/dom-exception.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file dom-exception.js 3 | * Polyfill the DOMException interface 4 | * 5 | * @author Tom Tang 6 | * @date August 2023 7 | * 8 | * @copyright Copyright (c) 2023 Distributive Corp. 9 | */ 10 | 11 | // Apply polyfill from core-js 12 | require('core-js/actual/dom-exception'); 13 | 14 | exports.DOMException = globalThis.DOMException; 15 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # chore(linting): lint all python files 2 | aae30e864449442cf0b04e94f8a242b1b667de9a 3 | 4 | # chore(linting): lint all JavaScript files 5 | 16dc3153b3cb684ca72445ed058babc8f5d97f42 6 | 7 | # chore(linting): lint all C++ files 8 | 58cd4b45777b046f03a63255c1d93e289e1cab5e 9 | 10 | # chore(linting): lint PyBytesProxyHandler.cc 11 | d540ed6e0edfe9538dc726cf587dfb2cc76dde34 12 | 13 | # chore(linting): lint PyObjectProxyHandler.cc 14 | 1d45ea98e42294cce16deec5454725d4de36f59f -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | pyvenv.cfg 4 | python/pythonmonkey/node_modules 5 | bin/ 6 | lib/* 7 | .pytest_cache 8 | .DS_Store 9 | firefox-*.tar.xz 10 | firefox-*.zip 11 | firefox-*/ 12 | mozilla-central-* 13 | __pycache__ 14 | Testing/Temporary 15 | _spidermonkey_install* 16 | uncrustify-*.tar.gz 17 | uncrustify-*/ 18 | uncrustify 19 | uncrustify.exe 20 | *.uncrustify 21 | __pycache__/* 22 | dist 23 | *.so 24 | *.dylib 25 | *.dll 26 | *.pyd 27 | *~ 28 | # Virtual Environment 29 | .venv/ 30 | -------------------------------------------------------------------------------- /python/pythonmonkey/__init__.py: -------------------------------------------------------------------------------- 1 | # Export public PythonMonkey APIs 2 | from .pythonmonkey import * 3 | from .helpers import * 4 | from .require import * 5 | 6 | # Expose the package version 7 | import importlib.metadata 8 | __version__ = importlib.metadata.version(__name__) 9 | del importlib 10 | 11 | # Load the module by default to expose global APIs 12 | # builtin_modules 13 | require("console") 14 | require("base64") 15 | require("timers") 16 | require("url") 17 | require("XMLHttpRequest") 18 | -------------------------------------------------------------------------------- /tests/js/timers-segfault.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file timers-segfault.simple 3 | * Test using the builtin_modules/timers.js module. 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | * 7 | * timeout: 10 8 | */ 9 | /* eslint-disable brace-style */ 10 | 11 | setTimeout(() => { console.log(0); }); 12 | const interval = setInterval(() => { console.log(1); }, 500); 13 | setTimeout(() => { clearInterval(interval); console.log(2); }, 1000); 14 | -------------------------------------------------------------------------------- /python/pythonmonkey/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./builtin_modules", // The base directory to resolve non-relative module names 4 | // This resolution has higher priority than lookups from node_modules. 5 | "target": "ES2022", 6 | "lib": [ 7 | "ES2022" 8 | ], 9 | "noEmit": true, 10 | "checkJs": true, 11 | "moduleDetection": "force", // Every non-declaration file will be treated as a module 12 | } 13 | } -------------------------------------------------------------------------------- /tests/js/timers-force-exit.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file timers-force-exit.simple 3 | * ensure we can use python.exit() even though there are timers pending 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | * 7 | * timeout: 4 8 | */ 9 | 10 | setTimeout(()=>console.log('fired timer'), 500000); 11 | setTimeout(()=>console.error('should not have fired timer!'), 0); 12 | console.log('about to exit even though timers are pending'); 13 | python.exit(0); 14 | -------------------------------------------------------------------------------- /src/FloatType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file FloatType.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python floats 5 | * @date 2022-12-02 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/FloatType.hh" 12 | 13 | PyObject *FloatType::getPyObject(double n) { 14 | PyObject *doubleVal = Py_BuildValue("d", n); 15 | Py_INCREF(doubleVal); 16 | return doubleVal; 17 | } -------------------------------------------------------------------------------- /tests/js/use-strict.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file use-strict.simple 3 | * Simple test which ensures that tests are evaluated correctly, such that the "use strict" 4 | * directive has meaning. 5 | * @author Wes Garland, wes@distributive.network 6 | * @date June 2023 7 | */ 8 | 'use strict'; 9 | 10 | function fun(abc) 11 | { 12 | arguments[0] = 123; 13 | 14 | if (abc === 123) 15 | throw new Error('"use strict" did not put interpreter in strict mode'); 16 | } 17 | 18 | fun(456); 19 | -------------------------------------------------------------------------------- /tests/js/not-strict-mode.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file not-strict-mode.simple 3 | * Simple test which ensures that tests are evaluated correctly, such that the interpreter 4 | * does not run tests in strict mode unless explicitly directed to do so. 5 | * @author Wes Garland, wes@distributive.network 6 | * @date June 2023 7 | */ 8 | function fun(abc) 9 | { 10 | arguments[0] = 123; 11 | 12 | if (abc !== 123) 13 | throw new Error('interpreter is in strict mode'); 14 | } 15 | 16 | fun(456); 17 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file url.js 3 | * Polyfill the URL and URLSearchParams interfaces 4 | * 5 | * @author Tom Tang 6 | * @date August 2023 7 | * 8 | * @copyright Copyright (c) 2023 Distributive Corp. 9 | */ 10 | 11 | // Apply polyfills from core-js 12 | require('./dom-exception'); 13 | require('core-js/actual/url'); 14 | require('core-js/actual/url-search-params'); 15 | 16 | exports.URL = globalThis.URL; 17 | exports.URLSearchParams = globalThis.URLSearchParams; 18 | -------------------------------------------------------------------------------- /src/NullType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NullType.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing JS null in a python object 5 | * @date 2023-02-22 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/NullType.hh" 12 | 13 | #include "include/modules/pythonmonkey/pythonmonkey.hh" 14 | 15 | PyObject *NullType::getPyObject() { 16 | PyObject *pmNull = getPythonMonkeyNull(); 17 | Py_INCREF(pmNull); 18 | return pmNull; 19 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 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" 12 | -------------------------------------------------------------------------------- /tests/js/py2js/trivial-function.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/trivial-function.simple 3 | * A trivial example of evaluating a python function in JS. 4 | * @author Liang Wang, liang@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | const double = python.eval('lambda x: x * 2'); 10 | const throughJS = x => x; 11 | const eight = throughJS(double)(2); 12 | 13 | if (eight !== 4) 14 | { 15 | console.error('expected', 4, 'but got', eight); 16 | throw new Error('test failed'); 17 | } 18 | 19 | console.log('pass 2 * 2 is 4'); 20 | -------------------------------------------------------------------------------- /tests/python/test_finalizationregistry.py: -------------------------------------------------------------------------------- 1 | import pythonmonkey as pm 2 | 3 | 4 | def test_finalizationregistry(): 5 | result = pm.eval(""" 6 | (collect) => { 7 | let arr = [42, 43]; 8 | const registry = new FinalizationRegistry(heldValue => { arr[heldValue] = heldValue; }); 9 | let obj1 = {}; 10 | let obj2 = {}; 11 | registry.register(obj1, 0); 12 | registry.register(obj2, 1); 13 | obj1 = null; 14 | obj2 = null; 15 | 16 | collect(); 17 | 18 | return arr; 19 | } 20 | """)(pm.collect) 21 | 22 | assert result[0] == 0 23 | assert result[1] == 1 24 | -------------------------------------------------------------------------------- /include/NoneType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NoneType.hh 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing None 5 | * @date 2023-02-22 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_NoneType_ 12 | #define PythonMonkey_NoneType_ 13 | 14 | #include 15 | 16 | /** 17 | * @brief This struct represents the 'None' type in Python 18 | */ 19 | struct NoneType { 20 | public: 21 | static PyObject *getPyObject(); 22 | 23 | }; 24 | 25 | #endif -------------------------------------------------------------------------------- /cmake/docs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # set input and output files 2 | set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) 3 | set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) 4 | 5 | # request to configure the file 6 | configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) 7 | message("Building docs with Doxygen") 8 | 9 | # note the option ALL which allows to build the docs together with the application 10 | add_custom_target( doc_doxygen ALL 11 | COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} 12 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} 13 | COMMENT "Generating API documentation with Doxygen" 14 | VERBATIM ) -------------------------------------------------------------------------------- /tests/js/is-compilable-unit.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file is-compilable-unit.simple 3 | * Simple test which ensures that pm.isCompilableUnit works from JS as expected 4 | * written in Python instead of JavaScript 5 | * @author Wes Garland, wes@distributive.network 6 | * @date June 2023 7 | */ 8 | 9 | const { isCompilableUnit } = require('./modules/vm-tools'); 10 | 11 | if (isCompilableUnit('()=>')) 12 | throw new Error('isCompilableUnit lied about ()=>'); 13 | 14 | if (!isCompilableUnit('123')) 15 | throw new Error('isCompilableUnit lied about 123'); 16 | 17 | console.log('done') 18 | -------------------------------------------------------------------------------- /tests/js/js2py/trivial-function.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/trivial-function.simple 3 | A trivial function that squares a number. The function is passed to python but evaluated in JS. 4 | * @author Liang Wang, liang@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | const square = x => x * x; 10 | const throughPython = python.eval('(lambda x: x)'); 11 | const four = throughPython(square)(2); 12 | 13 | if (four !== 4) 14 | { 15 | console.error('expected', 4 , 'but got', four); 16 | throw new Error('test failed'); 17 | } 18 | 19 | console.log('pass, 2 * 2 is 4'); 20 | -------------------------------------------------------------------------------- /tests/js/eval-test.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file eval-test.simple 3 | * A test that makes sure we can correctly evaluate a JS file 4 | * @copyright Copyright (c) 2024, Distributve Corp 5 | * @author Wes Garland, wes@distributive.network 6 | * @date March 2024 7 | */ 8 | 'use strict'; 9 | 10 | python.exec(` 11 | import os 12 | 13 | globalThis = pm.eval('globalThis') 14 | globalThis.result = pm.eval(open(os.path.join(os.getcwd(), "tests", "js", "resources", "eval-test.js"), "rb")) 15 | `); 16 | 17 | if (globalThis.result !== 18436572) 18 | throw new Error('incorrect result from eval-test.js'); 19 | -------------------------------------------------------------------------------- /tests/js/py2js/object.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/object.simple 3 | * Simple test which shows that sending objects to JS and getting them back into Python 4 | * works as expected. 5 | * @author Joash Mathew, 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const obj = python.eval('{"a": 1, "b": 2, "c": 3}'); 11 | const throughJS = (x) => x; 12 | const jsObj = throughJS(obj); 13 | 14 | if (jsObj !== obj) 15 | { 16 | console.error('expected ', obj, ' but got ', jsObj); 17 | throw new Error('Test failed'); 18 | } 19 | 20 | console.log('Test passed'); -------------------------------------------------------------------------------- /tests/js/py2js/string.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/string.simple 3 | * Simple test which shows that sending Python strings to JS and getting them back into 4 | * Python works as expected 5 | * @author Wes Garland, wes@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const hello = python.eval('"hello, world!"'); 11 | const throughJS = x => x; 12 | const bonjour = throughJS(hello); 13 | 14 | if (bonjour !== hello) 15 | { 16 | console.error('expected', hello, 'but got', bonjour); 17 | throw new Error('test failed'); 18 | } 19 | 20 | console.log('pass -', bonjour); 21 | -------------------------------------------------------------------------------- /include/BoolType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file BoolType.hh 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python bools 5 | * @date 2022-12-02 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_BoolType_ 12 | #define PythonMonkey_BoolType_ 13 | 14 | #include 15 | 16 | /** 17 | * @brief This struct represents the 'bool' type in Python, which is represented as a 'long' in C++ 18 | */ 19 | struct BoolType { 20 | public: 21 | static PyObject *getPyObject(long n); 22 | }; 23 | 24 | #endif -------------------------------------------------------------------------------- /tests/js/js2py/object.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/object.simple 3 | * Simple test which shows that sending objects to Python and getting them back into JS 4 | * works as expected. 5 | * @author Joash Mathew, 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const objJs = { a: 1, b: 2, c: 3 }; 11 | const throughPython = python.eval('(lambda x: x)'); 12 | const objPy = throughPython(objJs); 13 | 14 | if (objJs !== objPy) 15 | { 16 | console.error(`Expected ${objJs} but got ${objPy}`); 17 | throw new Error('Test failed'); 18 | } 19 | 20 | console.log('Test passed'); -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/include/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/clang", 10 | "cStandard": "c17", 11 | "cppStandard": "c++20", 12 | "intelliSenseMode": "linux-clang-x64", 13 | "browse": { 14 | "limitSymbolsToIncludedHeaders": true 15 | }, 16 | "compileCommands": "${workspaceFolder}/build/compile_commands.json" 17 | } 18 | ], 19 | "version": 4 20 | } -------------------------------------------------------------------------------- /include/FloatType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file FloatType.hh 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python floats 5 | * @date 2022-12-02 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_FloatType_ 12 | #define PythonMonkey_FloatType_ 13 | 14 | #include 15 | 16 | /** 17 | * @brief This struct represents the 'float' type in Python, which is represented as a 'double' in C++ 18 | */ 19 | struct FloatType { 20 | public: 21 | static PyObject *getPyObject(double n); 22 | }; 23 | 24 | #endif -------------------------------------------------------------------------------- /include/NullType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NullType.hh 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing JS null in a python object 5 | * @date 2023-02-22 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_NullType_ 12 | #define PythonMonkey_NullType_ 13 | 14 | #include 15 | 16 | /** 17 | * @brief This struct represents the JS null type in Python using a singleton object on the pythonmonkey module 18 | */ 19 | struct NullType { 20 | public: 21 | static PyObject *getPyObject(); 22 | }; 23 | 24 | #endif -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_pythonmonkey.yaml: -------------------------------------------------------------------------------- 1 | name: Make a Feature Request for PythonMonkey 2 | description: Use this template to make feature requests for PythonMonkey. Thank you so much for making an issue! 3 | body: 4 | 5 | - type: textarea 6 | id: feature-body 7 | attributes: 8 | label: Describe your feature request here. 9 | description: Feel free to include diagrams drawings or anything else to help explain it. 10 | 11 | - type: textarea 12 | id: feature-code 13 | attributes: 14 | label: Code example 15 | description: Provide a code example of this feature if applicable. 16 | value: 17 | render: shell 18 | 19 | -------------------------------------------------------------------------------- /tests/python/test_reentrance_smoke.py: -------------------------------------------------------------------------------- 1 | # @file reentrance-smoke.py 2 | # Basic smoke test which shows that JS->Python->JS->Python calls work. Failures are 3 | # indicated by segfaults. 4 | # @author Wes Garland, wes@distributive.network 5 | # @date June 2023 6 | 7 | import sys 8 | import os 9 | import pythonmonkey as pm 10 | 11 | 12 | def test_reentrance(): 13 | globalThis = pm.eval("globalThis;") 14 | globalThis.pmEval = pm.eval 15 | globalThis.pyEval = eval 16 | 17 | abc = (pm.eval("() => { return {def: pyEval('123')} };"))() 18 | assert (abc['def'] == 123) 19 | print(pm.eval("pmEval(`pyEval(\"'test passed'\")`)")) 20 | -------------------------------------------------------------------------------- /include/internalBinding.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file internalBinding.hh 3 | * @author Tom Tang (xmader@distributive.network) 4 | * @brief 5 | * @date 2023-05-16 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | namespace InternalBinding { 15 | extern JSFunctionSpec utils[]; 16 | extern JSFunctionSpec timers[]; 17 | } 18 | 19 | JSObject *createInternalBindingsForNamespace(JSContext *cx, JSFunctionSpec *methodSpecs); 20 | JSObject *getInternalBindingsByNamespace(JSContext *cx, JSLinearString *namespaceStr); 21 | 22 | JSFunction *createInternalBinding(JSContext *cx); 23 | PyObject *getInternalBindingPyFn(JSContext *cx); 24 | -------------------------------------------------------------------------------- /tests/js/console-smoke.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file console-smoke.simple 3 | * 4 | * Simple smoke test which ensures that the global console is initialized and that the 5 | * console constructor works. We send stderr to stdout to avoid cluttering up the test 6 | * runner display. 7 | * 8 | * @author Wes Garland, wes@distributive.network 9 | * @date June 2023 10 | */ 11 | const console = new (require('console').Console)({ 12 | stdout: python.stdout, 13 | stderr: python.stdout 14 | }); 15 | globalThis.console.log('one'); 16 | globalThis.console.info('two'); 17 | console.debug('three'); 18 | console.error('four'); 19 | console.warn('five'); 20 | 21 | -------------------------------------------------------------------------------- /tests/js/py2js/integer.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/integer.simple 3 | * Simple test which shows that sending Python integers to JS and getting it back in Python works as expected. 4 | * @author Kirill Kirnichansky, kirill@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | const number = python.eval('int(777)'); 10 | 11 | if (typeof number !== 'number') 12 | { 13 | console.error(`expected number but got ${typeof number}`); 14 | throw new Error('test failed'); 15 | } 16 | if (number !== 777) 17 | { 18 | console.error(`expected ${777} but got ${number}`); 19 | throw new Error('test failed'); 20 | } 21 | 22 | console.log('pass -', number); 23 | -------------------------------------------------------------------------------- /include/FuncType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file FuncType.hh 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct representing python functions 5 | * @date 2022-08-08 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_FuncType_ 12 | #define PythonMonkey_FuncType_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * @brief This struct represents the 'function' type in Python 20 | */ 21 | struct FuncType { 22 | public: 23 | static PyObject *getPyObject(JSContext *cx, JS::HandleValue fval); 24 | }; 25 | 26 | #endif -------------------------------------------------------------------------------- /tests/js/program.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env pmjs 2 | /** 3 | * @file program.js 4 | * A program for use by pmjs *.bash tests which need a basic program module 5 | * @author Wes Garland, wes@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | if (arguments !== globalThis.arguments) 11 | throw new Error('arguments free variable is not the global arguments array!'); 12 | 13 | /* If the module cache is wrong, this will load again after -r of the same module */ 14 | require('./modules/print-load'); 15 | 16 | for (let argument of arguments) 17 | console.log('ARG', argument); 18 | console.log('ARGC', arguments.length); 19 | console.log('FINISHED - ran program', __filename); 20 | -------------------------------------------------------------------------------- /include/ListType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ListType.hh 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python lists 5 | * @date 2022-08-18 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_ListType_ 12 | #define PythonMonkey_ListType_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | 19 | /** 20 | * @brief This struct represents a list in python 21 | * 22 | * @author Giovanni 23 | */ 24 | struct ListType { 25 | public: 26 | static PyObject *getPyObject(JSContext *cx, JS::HandleObject arrayObj); 27 | }; 28 | #endif -------------------------------------------------------------------------------- /src/FuncType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file FuncType.cc 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct representing python functions 5 | * @date 2022-08-08 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/FuncType.hh" 12 | #include "include/JSFunctionProxy.hh" 13 | 14 | #include 15 | 16 | 17 | PyObject *FuncType::getPyObject(JSContext *cx, JS::HandleValue fval) { 18 | JSFunctionProxy *proxy = (JSFunctionProxy *)PyObject_CallObject((PyObject *)&JSFunctionProxyType, NULL); 19 | proxy->jsFunc->set(&fval.toObject()); 20 | return (PyObject *)proxy; 21 | } -------------------------------------------------------------------------------- /tests/js/py2js/array-change-index.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/array-chnage-index.simple 3 | * Simple test which demonstrates modifying a list passed 4 | * to JavaScript from Python and returning it to JS. 5 | * @author Will Pringle, will@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | python.exec(` 11 | def modifyAndReturn(modifierFun): 12 | numbers = [1,2,3,4,5,6,7,8,9] 13 | modifierFun(numbers) 14 | return numbers 15 | `); 16 | 17 | const modifyAndReturn = python.eval('modifyAndReturn'); 18 | const numbers = modifyAndReturn((array) => { 19 | array[1] = 999; 20 | }); 21 | 22 | if (numbers[1] !== 999) 23 | throw new Error('Python list not modified by JS'); 24 | 25 | console.log('pass'); 26 | 27 | -------------------------------------------------------------------------------- /tests/js/py2js/higher-order-function.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/higher-order-function.simple 3 | * A simple function composition in python, but evaluated in JS. 4 | * @author Liang Wang, liang@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | const pDouble = python.eval('lambda x: x * 2'); 10 | const pSquare = python.eval('lambda x: x**2'); 11 | const pCompose = python.eval('lambda f, g: lambda *args: f(g(*args))') 12 | const doubleAfterSqaure = pCompose(pDouble, pSquare); 13 | const throughJS = x => x; 14 | const eight = throughJS(doubleAfterSqaure)(2); 15 | 16 | if (eight !== 8) 17 | { 18 | console.error('expected', 8, 'but got', eight); 19 | throw new Error('test failed'); 20 | } 21 | 22 | console.log('pass 2(2^2) is 8'); 23 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/base64.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base64.d.ts 3 | * @brief TypeScript type declarations for base64.py 4 | * @author Tom Tang 5 | * @date July 2023 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | */ 9 | 10 | /** 11 | * Decode base64 string 12 | * @param b64 A string containing base64-encoded data. 13 | * @see https://html.spec.whatwg.org/multipage/webappapis.html#dom-atob-dev 14 | */ 15 | export declare function atob(b64: string): string; 16 | 17 | /** 18 | * Create a base64-encoded ASCII string from a binary string 19 | * @param data The binary string to encode. 20 | * @see https://html.spec.whatwg.org/multipage/webappapis.html#dom-btoa-dev 21 | */ 22 | export declare function btoa(data: string): string; 23 | -------------------------------------------------------------------------------- /tests/js/js2py/string.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/string.simple 3 | * Simple test which shows that sending strings to Python and getting them back into JS 4 | * works as expected - including non-BMP Unicode. 5 | * @author Wes Garland, wes@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const hello = 'hello, world!🐍🐒' 11 | const throughPython = python.eval('(lambda x: x)'); 12 | const bonjour = throughPython(hello); 13 | 14 | if (bonjour !== hello) 15 | { 16 | console.error('expected', hello, 'but got', bonjour); 17 | throw new Error('test failed'); 18 | } 19 | 20 | /* XXXwg jul 2023 note - we get unknown characters in the output here because of issue 89, unrelated to this test */ 21 | console.log('pass -', bonjour); 22 | -------------------------------------------------------------------------------- /tests/js/py2js/arraybuffer.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/arraybuffer.simple 3 | * Simple test which shows that sending Buffers from python to JavaScript and mutating 4 | * them works 5 | * @author Severn Lortie, severn@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | python.exec(` 11 | x = bytearray([ 104, 101, 108, 108, 111 ]) 12 | `); 13 | 14 | const pythonBuff = python.eval('x'); 15 | 16 | // Mutate the buffer 17 | pythonBuff[3] = 55; 18 | 19 | // Now pass back to python 20 | const newPythonBuff = python.eval('x'); 21 | 22 | // Check that both were updated 23 | for (let i = 0; i < pythonBuff.length; i++) 24 | if (pythonBuff[i] !== newPythonBuff[i]) throw new Error('The buffer was not changed, or not returned correctly.'); 25 | 26 | -------------------------------------------------------------------------------- /tests/js/py2js/promise.simple.failing: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/promise.simple 3 | * Simple test which shows that sending Python awaitable to JS appears as a JS promise 4 | * @author Ryan Saweczko ryansaweczko@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | const pythonCode = python.exec(` 10 | import asyncio 11 | 12 | async def nested(): 13 | return 42 14 | 15 | async def main(): 16 | global asyncio 17 | global nested 18 | task = asyncio.create_task(nested()) 19 | return task 20 | `); 21 | const task = python.eval('asyncio.run(main())'); 22 | 23 | if (task instanceof Promise) 24 | { 25 | console.log("Return was a promise"); 26 | } 27 | else 28 | { 29 | throw new Error(`Received a non-promise in JS from a python awaitable. Type ${typeof(task)}`); 30 | } -------------------------------------------------------------------------------- /tests/js/timers-natural-exit.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file timers-natural-exit.bash 4 | # A peter-jr test which show that programs exit when the event loop becomes empty. 5 | # 6 | # @author Wes Garland, wes@distributive.network 7 | # @date July 2023 8 | 9 | set -u 10 | set -o pipefail 11 | 12 | panic() 13 | { 14 | echo "FAIL: $*" >&2 15 | exit 2 16 | } 17 | 18 | cd `dirname "$0"` || panic "could not change to test directory" 19 | 20 | "${PMJS:-../../pmjs}" ./timers-natural-exit.js \ 21 | | egrep 'end of program|fired timer' \ 22 | | ( 23 | read line 24 | [ "$line" = "end of program" ] || panic "first line read was '$line', not 'end of program'" 25 | read line 26 | [ "$line" = "fired timer" ] || panic "second line read was '$line', not 'fired timer'" 27 | ) 28 | -------------------------------------------------------------------------------- /src/ListType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ListType.cc 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python lists 5 | * @date 2022-08-18 6 | * 7 | * @copyright Copyright (c) 2022, 2023, 2024 Distributive Corp 8 | * 9 | */ 10 | 11 | 12 | #include "include/ListType.hh" 13 | 14 | #include "include/JSArrayProxy.hh" 15 | 16 | 17 | PyObject *ListType::getPyObject(JSContext *cx, JS::HandleObject jsArrayObj) { 18 | JSArrayProxy *proxy = (JSArrayProxy *)PyObject_CallObject((PyObject *)&JSArrayProxyType, NULL); 19 | if (proxy != NULL) { 20 | proxy->jsArray = new JS::PersistentRootedObject(cx); 21 | proxy->jsArray->set(jsArrayObj); 22 | return (PyObject *)proxy; 23 | } 24 | return NULL; 25 | } -------------------------------------------------------------------------------- /tests/js/js2py/promise.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/promise.simple 3 | * Simple test which shows that creating a promise in javascript and sending it to Python and getting it back into JS 4 | * works as expected - able to resolve the promise sent back. 5 | * @author Ryan Saweczko ryansaweczko@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | 11 | var resolve; 12 | const examplePromise = new Promise((res, rej) => 13 | { 14 | resolve = res; 15 | }); 16 | 17 | const pythonCode = 'lambda x: x'; 18 | 19 | const pythonLambda = python.eval(pythonCode); 20 | const outPromise = pythonLambda(examplePromise); 21 | 22 | outPromise.then(() => 23 | { 24 | console.log('able to resolve promise after going through python'); 25 | python.exit(); 26 | }); 27 | 28 | resolve(); 29 | 30 | -------------------------------------------------------------------------------- /python/pminit/pythonmonkey/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pythonmonkey", 3 | "version": "0.0.1", 4 | "description": "PythonMonkey - JS Components", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Distributive-Network/PythonMonkey.git" 16 | }, 17 | "author": "Distributive Corp.", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Distributive-Network/PythonMonkey/issues" 21 | }, 22 | "homepage": "https://github.com/Distributive-Network/PythonMonkey#readme", 23 | "dependencies": { 24 | "core-js": "^3.35.1", 25 | "ctx-module": "^1.0.15", 26 | "events": "^3.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/js/collide.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file collide.simple 3 | * Test to ensure require module identitifier collision that resolves both py and js 4 | * modules correctly prefers py modules. 5 | * @author Wes Garland 6 | * @date Mar 2024 7 | */ 8 | 9 | const { which } = require('./modules/collide'); 10 | const whichjs = require('./modules/collide.js').which; 11 | const whichpy = require('./modules/collide.py').which; 12 | 13 | if (which !== 'python') 14 | throw new Error(`python module was not preferred, got ${which} instead`); 15 | 16 | if (whichpy !== 'python') 17 | throw new Error(`python module was not explicitly loaded, got ${whichpy} instead`); 18 | 19 | if (whichjs !== 'javascript') 20 | throw new Error(`javascript module was not explicitly loaded, got ${whichjs} instead`); 21 | -------------------------------------------------------------------------------- /tests/js/js2py/higher-order-function.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/higher-order-function.simple 3 | * A basic higher order function that composes two functions together, point free style. The function is 4 | * passed to python but evaluated in JS. 5 | * @author Liang Wang, liang@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const square = x => x * x; 11 | const double = x => 2 * x; 12 | 13 | const compose = (f, g) => (...args) => f(g(...args)); 14 | const throughPython = python.eval('(lambda x: x)'); 15 | const doubleAfterSqaure = throughPython(compose)(double, square); 16 | 17 | const eight = doubleAfterSqaure(2); 18 | 19 | if (eight !== 8) 20 | { 21 | console.error('expected', 8 , 'but got', eight); 22 | throw new Error('test failed'); 23 | } 24 | 25 | console.log('pass, 2(2^2) is 8'); 26 | -------------------------------------------------------------------------------- /tests/js/program-throw.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file program-throw.bash 4 | # A peter-jr test which shows that uncaught exceptions in the program throw, get shown 5 | # on stderr, cause a non-zero exit code, and aren't delayed because of pending events. 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 9 | # 10 | # timeout: 5 11 | 12 | set -u 13 | 14 | panic() 15 | { 16 | echo "FAIL: $*" >&2 17 | exit 2 18 | } 19 | 20 | cd `dirname "$0"` || panic "could not change to test directory" 21 | 22 | "${PMJS:-../../pmjs}" ./program-throw.js 2>&1 1>/dev/null \ 23 | | egrep 'hello|goodbye' \ 24 | | while read line 25 | do 26 | [[ "$line" =~ goodbye ]] && panic "found goodbye - timer fired when it shouldn't have!" 27 | [[ "$line" =~ hello ]] && echo "found expected '$line'" && exit 0 28 | done 29 | -------------------------------------------------------------------------------- /tests/js/util-module.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file util-module.simple 3 | * Simple test for the builtin util module 4 | * @author Tom Tang 5 | * @date March 2024 6 | */ 7 | 8 | const util = require('util'); 9 | 10 | // https://github.com/Distributive-Network/PythonMonkey/pull/300 11 | const err = new TypeError(); 12 | if (err.propertyIsEnumerable('stack')) 13 | throw new Error('The stack property should not be enumerable.'); 14 | err.anything = 123; 15 | err.stack = 'abc'; 16 | if (!err.propertyIsEnumerable('stack')) 17 | throw new Error('In SpiderMonkey, the stack property should be enumerable after changing it.'); 18 | const output = util.format(err); 19 | if (output.match(/abc/g).length !== 1) // should only be printed once 20 | throw new Error('The stack property should not be printed along with other enumerable properties'); 21 | -------------------------------------------------------------------------------- /tests/js/program-exit.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file program-exit.bash 4 | # A peter-jr test which shows that python.exit can cause a program to exit without 5 | # reporting errors, even if there are pending timers. 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 9 | # 10 | # timeout: 5 11 | 12 | set -u 13 | 14 | panic() 15 | { 16 | echo "FAIL: $*" >&2 17 | exit 2 18 | } 19 | 20 | cd `dirname "$0"` || panic "could not change to test directory" 21 | 22 | "${PMJS:-../../pmjs}" ./program-exit.js 2>&1 \ 23 | | while read line 24 | do 25 | panic "Unexpected output '$line'" 26 | done 27 | exitCode="$?" 28 | [ "$exitCode" = 0 ] || exit "$exitCode" 29 | 30 | "${PMJS:-../../pmjs}" ./program-exit.js 31 | exitCode="$?" 32 | 33 | [ "$exitCode" = "99" ] || panic "exit code should have been 99 but was $exitCode" 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/base64.py: -------------------------------------------------------------------------------- 1 | # @file base64.py 2 | # @author Tom Tang , Hamada Gasmallah 3 | # @date July 2023 4 | # @copyright Copyright (c) 2023 Distributive Corp. 5 | 6 | import pythonmonkey as pm 7 | import base64 8 | 9 | 10 | def atob(b64): 11 | padding = '=' * (4 - (len(b64) & 3)) 12 | return str(base64.standard_b64decode(b64 + padding), 'latin1') 13 | 14 | 15 | def btoa(data): 16 | return str(base64.standard_b64encode(bytes(data, 'latin1')), 'latin1') 17 | 18 | 19 | # Make `atob`/`btoa` globally available 20 | pm.eval(r"""(atob, btoa) => { 21 | if (!globalThis.atob) { 22 | globalThis.atob = atob; 23 | } 24 | if (!globalThis.btoa) { 25 | globalThis.btoa = btoa; 26 | } 27 | }""")(atob, btoa) 28 | 29 | # Module exports 30 | exports['atob'] = atob # type: ignore 31 | exports['btoa'] = btoa # type: ignore 32 | -------------------------------------------------------------------------------- /tests/js/pmjs-popt.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file pmjs-popt.bash 4 | # A peter-jr test which ensures that the pmjs -p option evaluates and print an expression 5 | # 6 | # @author Wes Garland, wes@distributive.network 7 | # @date July 2023 8 | 9 | set -u 10 | set -o pipefail 11 | 12 | panic() 13 | { 14 | echo "FAIL: $*" >&2 15 | exit 2 16 | } 17 | 18 | cd `dirname "$0"` || panic "could not change to test directory" 19 | 20 | "${PMJS:-pmjs}" -p '"OKAY"' < /dev/null |\ 21 | tr -d '\r' |\ 22 | while read keyword rest 23 | do 24 | case "$keyword" in 25 | "OKAY") 26 | echo "${keyword} ${rest}" 27 | echo "Done" 28 | exit 111 29 | ;; 30 | *) 31 | echo "Ignored: ${keyword} ${rest}" 32 | ;; 33 | esac 34 | done 35 | 36 | exitCode="$?" 37 | [ "$exitCode" = "111" ] && exit 0 38 | [ "$exitCode" = "2" ] && exit 2 39 | 40 | panic "Test did not run to completion" 41 | -------------------------------------------------------------------------------- /tests/js/pmjs-eopt.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file pmjs-eopt.bash 4 | # A peter-jr test which ensures that the pmjs -e option evaluates an expression 5 | # 6 | # @author Wes Garland, wes@distributive.network 7 | # @date July 2023 8 | 9 | set -u 10 | set -o pipefail 11 | 12 | panic() 13 | { 14 | echo "FAIL: $*" >&2 15 | exit 2 16 | } 17 | 18 | cd `dirname "$0"` || panic "could not change to test directory" 19 | 20 | "${PMJS:-pmjs}" -e 'console.log("OKAY")' < /dev/null |\ 21 | tr -d '\r' |\ 22 | while read keyword rest 23 | do 24 | case "$keyword" in 25 | "OKAY") 26 | echo "${keyword} ${rest}" 27 | echo "Done" 28 | exit 111 29 | ;; 30 | *) 31 | echo "Ignored: ${keyword} ${rest}" 32 | ;; 33 | esac 34 | done 35 | 36 | exitCode="$?" 37 | [ "$exitCode" = "111" ] && exit 0 38 | [ "$exitCode" = "2" ] && exit 2 39 | 40 | panic "Test did not run to completion" 41 | -------------------------------------------------------------------------------- /tests/js/pmjs-ropt.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file pmjs-ropt.bash 4 | # A peter-jr test which ensures that the pmjs -r loads a module by the identifier 5 | # 6 | # @author Wes Garland, wes@distributive.network 7 | # @date July 2023 8 | 9 | set -u 10 | set -o pipefail 11 | 12 | panic() 13 | { 14 | echo "FAIL: $*" >&2 15 | exit 2 16 | } 17 | 18 | cd `dirname "$0"` || panic "could not change to test directory" 19 | 20 | "${PMJS:-pmjs}" -r ./modules/print-load < /dev/null |\ 21 | tr -d '\r' |\ 22 | while read keyword rest 23 | do 24 | case "$keyword" in 25 | "LOADED") 26 | echo "${keyword} ${rest}" 27 | echo "Done" 28 | exit 111 29 | ;; 30 | *) 31 | echo "Ignored: ${keyword} ${rest}" 32 | ;; 33 | esac 34 | done 35 | 36 | exitCode="$?" 37 | [ "$exitCode" = "111" ] && exit 0 38 | [ "$exitCode" = "2" ] && exit 2 39 | 40 | panic "Test did not run to completion" 41 | -------------------------------------------------------------------------------- /src/DictType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DictType.cc 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct representing python dictionaries 5 | * @date 2022-08-10 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | 12 | #include "include/DictType.hh" 13 | 14 | #include "include/JSObjectProxy.hh" 15 | 16 | #include 17 | 18 | 19 | PyObject *DictType::getPyObject(JSContext *cx, JS::Handle jsObject) { 20 | JSObjectProxy *proxy = (JSObjectProxy *)PyObject_CallObject((PyObject *)&JSObjectProxyType, NULL); 21 | if (proxy != NULL) { 22 | JS::RootedObject obj(cx); 23 | JS_ValueToObject(cx, jsObject, &obj); 24 | proxy->jsObject = new JS::PersistentRootedObject(cx); 25 | proxy->jsObject->set(obj); 26 | return (PyObject *)proxy; 27 | } 28 | return NULL; 29 | } -------------------------------------------------------------------------------- /tests/js/py2js/error.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/string.simple 3 | * Ensures that an error constructed in JS and passed through python retains error message. 4 | * There will be a lossful conversion of error properties from JS to Py. 5 | * 6 | * @author David Courtis, david@distributive.network 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | class RandomError extends Error 12 | { 13 | constructor(message) 14 | { 15 | super(message); 16 | } 17 | } 18 | 19 | const exceptionjs = new RandomError('I was created!'); 20 | const throughPython = python.eval('(lambda x: x)'); 21 | const exceptionpy = throughPython(exceptionjs); 22 | 23 | if (!exceptionpy.toString().includes(exceptionjs.toString())) 24 | { 25 | console.error('Expected\n', exceptionjs.toString(), '\nbut got\n', exceptionpy.toString()); 26 | throw new Error('test failed'); 27 | } 28 | 29 | console.log('pass -', exceptionjs); -------------------------------------------------------------------------------- /tests/js/require-module-stack.bash.failing: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file require-module-stack.bash 4 | # A peter-jr test which tests that requiring a module yields JS stacks with the correct 5 | # filename and line number information. 6 | # 7 | # NOTE: This test currently fails because the stack coming back through python has 8 | # underscores instead of slashes in the filename 9 | # 10 | # @author Wes Garland, wes@distributive.network 11 | # @date July 2023 12 | 13 | set -u 14 | set -o pipefail 15 | 16 | panic() 17 | { 18 | echo "FAIL: $*" >&2 19 | exit 2 20 | } 21 | 22 | cd `dirname "$0"` || panic "could not change to test directory" 23 | 24 | if "${PMJS:-../../pmjs}" -r ./throw-filename ./program.js \ 25 | | grep '^@/home/wes/git/pythonmonkey2/tests/js/throw-filename.js:9:9$'; then 26 | echo 'pass' 27 | fi 28 | 29 | panic "did not find correct stack info" 30 | -------------------------------------------------------------------------------- /tests/js/js2py/bigint.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/bigint.simple 3 | * Simple test which shows that sending bigint to Python and getting them back into JS 4 | * works as expected. 5 | * @author Kirill Kirnichansky, kirill@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const bigint = BigInt(777); 11 | 12 | python.exec(` 13 | def isPythonInt(val): 14 | return isinstance(val, int) 15 | 16 | def compare(val): 17 | if val == int(777): 18 | return True 19 | return False 20 | `); 21 | const isPythonInt = python.eval('isPythonInt'); 22 | const compare = python.eval('compare'); 23 | 24 | if (!isPythonInt) 25 | { 26 | console.error(`${bigint} is not instance of int in Python.`); 27 | throw new Error('test failed'); 28 | } 29 | if (!compare) 30 | { 31 | console.error(`${bigint} !== 777 in Python`); 32 | throw new Error('test failed'); 33 | } 34 | 35 | console.log('pass'); 36 | -------------------------------------------------------------------------------- /tests/js/js2py/boolean.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/boolean.simple - Asserts that sending primitive and boxed 3 | * booleans to Python and getting them back into JS as primitive 4 | * booleans retains the correponsing logical values. 5 | * 6 | * @author Bryan Hoang 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const throughPython = python.eval('(lambda x: x)'); 12 | 13 | const primitiveJsTrue = true; 14 | const maybePrimitiveJsTrue = throughPython(primitiveJsTrue); 15 | if (maybePrimitiveJsTrue !== primitiveJsTrue) 16 | { 17 | console.error('Expected', primitiveJsTrue, 'but got', maybePrimitiveJsTrue); 18 | throw new Error('Test failed'); 19 | } 20 | 21 | const boxedJsFalse = new Boolean(false); 22 | const maybePrimitiveJsFalse = throughPython(boxedJsFalse); 23 | if (false !== maybePrimitiveJsFalse) 24 | { 25 | console.error('Expected', false, 'but got', maybePrimitiveJsFalse); 26 | throw new Error('Test failed'); 27 | } 28 | -------------------------------------------------------------------------------- /tests/js/js2py/arraybuffer.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/arraybuffer.simple 3 | * Simple test which shows that sending ArrayBuffers to Python and getting them back into JS 4 | * works as expected. Also tests mutation. 5 | * @author Severn Lortie, severn@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const buffer = new ArrayBuffer(5); // 5 bytes 11 | const bufferView = new Uint8Array(buffer); 12 | bufferView[0] = 104; 13 | bufferView[1] = 101; 14 | bufferView[2] = 108; 15 | bufferView[3] = 108; 16 | bufferView[4] = 111; 17 | 18 | python.exec(` 19 | def mutate(x): 20 | x[3] = 55; 21 | return x; 22 | `); 23 | 24 | const fn = python.eval('mutate'); 25 | const modifiedBuff = fn(buffer); // Call the function which mutates the buffer 26 | 27 | // Check that both were updated 28 | for (let i = 0; i < bufferView.length; i++) 29 | if (modifiedBuff[i] !== bufferView[i]) throw new Error('The buffer was not changed, or not returned correctly.'); 30 | 31 | -------------------------------------------------------------------------------- /include/PyIterableProxyHandler.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PyIterableProxyHandler.hh 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for creating JS proxy objects for iterables 5 | * @date 2024-04-08 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_PyIterableProxy_ 12 | #define PythonMonkey_PyIterableProxy_ 13 | 14 | 15 | #include "include/PyObjectProxyHandler.hh" 16 | 17 | 18 | /** 19 | * @brief This struct is the ProxyHandler for JS Proxy Iterable pythonmonkey creates to handle coercion from python iterables to JS Objects 20 | * 21 | */ 22 | struct PyIterableProxyHandler : public PyObjectProxyHandler { 23 | public: 24 | PyIterableProxyHandler() : PyObjectProxyHandler(&family) {}; 25 | static const char family; 26 | 27 | bool getOwnPropertyDescriptor( 28 | JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 29 | JS::MutableHandle> desc 30 | ) const override; 31 | }; 32 | 33 | #endif -------------------------------------------------------------------------------- /tests/js/js2py/promise-await-in-python.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/promise-await-in-python.simple 3 | * Simple test to await a javascript promise as a python awaitable, resolving with the correct value. 4 | * @author Ryan Saweczko ryansaweczko@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | var resolve; 10 | var valToResolve = 1; 11 | const examplePromise = new Promise((res, rej) => 12 | { 13 | resolve = res; 14 | }); 15 | 16 | const pythonCode = ` 17 | import asyncio 18 | 19 | async def awaitPromise(promise): 20 | ret = await promise 21 | return ret 22 | `; 23 | 24 | python.exec(pythonCode); 25 | const pythonFunction = python.eval('awaitPromise'); 26 | 27 | async function test() 28 | { 29 | const backValue = await pythonFunction(examplePromise); 30 | if (backValue !== 1) 31 | throw new Error(`Received value ${backValue} instead of ${valToResolve} from awaiting a JS promise in python`); 32 | } 33 | test(); 34 | 35 | resolve(valToResolve); 36 | 37 | -------------------------------------------------------------------------------- /include/DictType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DictType.hh 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct representing python dictionaries 5 | * @date 2022-08-10 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_DictType_ 12 | #define PythonMonkey_DictType_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * @brief This struct represents a dictionary in python. 20 | * 21 | * @author Giovanni 22 | */ 23 | struct DictType { 24 | public: 25 | /** 26 | * @brief Construct a new DictType object from a JSObject. 27 | * 28 | * @param cx - pointer to the JSContext 29 | * @param jsObject - pointer to the JSObject to be coerced 30 | * 31 | * @returns PyObject* pointer to the resulting PyObject 32 | */ 33 | static PyObject *getPyObject(JSContext *cx, JS::Handle jsObject); 34 | }; 35 | 36 | #endif -------------------------------------------------------------------------------- /tests/js/js2py/object-mutation.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/object-mutation.simple 3 | * Simple test which shows that converting objects from JS => Python uses 4 | * shared memory, and changes made in either language will affect the object 5 | * in the other. 6 | * @author Joash Mathew, 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const obj = { a: 1 }; 12 | const pcode = ` 13 | def change_and_return(obj): 14 | obj["a"] = 5; 15 | return obj; 16 | `; 17 | 18 | python.exec(pcode); 19 | 20 | const fun = python.eval('change_and_return'); 21 | const obj2 = fun(obj); 22 | 23 | if (obj.a !== 5 || obj2['a'] !== 5) 24 | { 25 | console.error('Object isn\'t sharing memory.'); 26 | throw new Error('Test failed'); 27 | } 28 | 29 | obj.a = 1000; 30 | 31 | if (obj.a !== 1000 || obj2['a'] !== 1000) 32 | { 33 | console.error('Object isn\'t sharing memory.'); 34 | throw new Error('Test failed'); 35 | } 36 | 37 | console.log('Test passed'); -------------------------------------------------------------------------------- /tests/js/js2py/function-curry.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/function-curry.simple 3 | * Curries a function in JS, pass it to python, apply it in JS. 4 | * @author Liang Wang, liang@distributive.network 5 | * @date July 2023 6 | */ 7 | 8 | 'use strict'; 9 | const curry = (fn) => 10 | { 11 | return function curried(...args) 12 | { 13 | if (args.length >= fn.length) 14 | { 15 | return fn.apply(null, args); 16 | } 17 | else 18 | { 19 | return function(...args2) 20 | { 21 | return curried.apply(null, args.concat(args2)); 22 | }; 23 | } 24 | }; 25 | }; 26 | 27 | const takeMiddle = (_x, y, _z) => y; 28 | 29 | const throughPython = python.eval('(lambda x: x)'); 30 | const pTakeMiddle = throughPython(curry(takeMiddle)); 31 | const middle = pTakeMiddle(1)(2)(3); 32 | 33 | if (middle !== 2) 34 | { 35 | console.error('expected', 2 , 'but got', middle); 36 | throw new Error('test failed'); 37 | } 38 | 39 | console.log('this thing can curry'); 40 | 41 | -------------------------------------------------------------------------------- /tests/js/set-timeout.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file set-timeout.simple 3 | * Simple test which ensures that setTimeout() fires, and that it fires at about the right 4 | * time. 5 | * @author Wes Garland, wes@distributive.network 6 | * @date March 2024 7 | */ 8 | 9 | const time = python.eval('__import__("time").time'); 10 | const start = time(); 11 | 12 | /** 13 | * - must fire later than 100ms 14 | * - must fire before 10s 15 | * - 10s is well before 100s but very CI-load-tolerant; the idea is not to check for accurancy, but 16 | * rather ensure we haven't mixed up seconds and milliseconds somewhere. 17 | */ 18 | function check100ms() 19 | { 20 | let end = time(); 21 | 22 | if (end - start < 0.1) 23 | throw new Error('timer fired too soon'); 24 | if (end - start > 10) 25 | throw new Error('timer fired too late'); 26 | 27 | console.log('done - timer fired after ', (end-start) * 1000, 'ms'); 28 | python.exit.code = 0; 29 | } 30 | 31 | python.exit.code = 2; 32 | setTimeout(check100ms, 100); 33 | -------------------------------------------------------------------------------- /tests/js/timer-throw.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file program-throw.bash 4 | # A peter-jr test which shows that uncaught exceptions in the program throw, get shown 5 | # on stderr, cause a non-zero exit code, and aren't delayed because of pending events. 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 9 | # 10 | # timeout: 5 11 | 12 | set -u 13 | 14 | panic() 15 | { 16 | echo "FAIL: $*" >&2 17 | exit 2 18 | } 19 | 20 | cd `dirname "$0"` || panic "could not change to test directory" 21 | 22 | "${PMJS:-../../pmjs}" ./timer-throw.js 2>&1 1>/dev/null \ 23 | | egrep 'hello|goodbye' \ 24 | | ( 25 | read line 26 | if [[ "$line" =~ hello ]]; then 27 | echo "found expected '$line'" 28 | else 29 | panic "expected hello, found '${line}'" 30 | fi 31 | 32 | read line 33 | if [[ "$line" =~ Error:.goodbye ]]; then 34 | echo "found expected '$line'" 35 | else 36 | panic "expected Error: goodbye, found '${line}'" 37 | fi 38 | ) 39 | -------------------------------------------------------------------------------- /include/pyTypeFactory.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file pyTypeFactory.hh 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Function for wrapping arbitrary PyObjects into the appropriate PyType class, and coercing JS types to python types 5 | * @date 2022-08-08 6 | * 7 | * @copyright Copyright (c) 2022, 2023, 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_PyTypeFactory_ 12 | #define PythonMonkey_PyTypeFactory_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | 19 | /** 20 | * @brief Function that takes a JS::Value and returns a corresponding PyObject* object, doing shared memory management when necessary 21 | * 22 | * @param cx - Pointer to the javascript context of the JS::Value 23 | * @param rval - The JS::Value who's type and value we wish to encapsulate 24 | * @return PyObject* - Pointer to the object corresponding to the JS::Value 25 | */ 26 | PyObject *pyTypeFactory(JSContext *cx, JS::HandleValue rval); 27 | 28 | #endif -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "(gdb) Debug", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "python", 9 | "args": [ 10 | "${file}" 11 | ], 12 | "pipeTransport": { 13 | "pipeCwd": "${workspaceFolder}", 14 | "pipeProgram": "poetry", 15 | "quoteArgs": false, 16 | "pipeArgs": [ 17 | "run" 18 | ], 19 | }, 20 | "preLaunchTask": "Fast build", 21 | "cwd": "${fileDirname}", 22 | "environment": [], 23 | "externalConsole": false, 24 | "MIMode": "gdb", 25 | "setupCommands": [ 26 | { 27 | "description": "Enable pretty-printing for gdb", 28 | "text": "-enable-pretty-printing", 29 | "ignoreFailures": true 30 | }, 31 | { 32 | "description": "Set Disassembly Flavor to Intel", 33 | "text": "-gdb-set disassembly-flavor intel", 34 | "ignoreFailures": true 35 | } 36 | ] 37 | }, 38 | ] 39 | } -------------------------------------------------------------------------------- /tests/js/js2py/array-change-index.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/array-chnage-index.simple 3 | * Simple test which demonstrates modifying an array 4 | * passed to Python and returning it. 5 | * @author Will Pringle, will@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict'; 9 | 10 | const numbers = [1,2,3,4,5,6,7,8,9]; 11 | 12 | python.exec(` 13 | def setArrayAtIndex(array, index, new): 14 | array[1] = new # can't index "array" based on "index"... probably because index is a float. So just pass "1" 15 | return array 16 | `); 17 | const setArrayAtIndex = python.eval('setArrayAtIndex'); 18 | const numbersBack = setArrayAtIndex(numbers, 1, 999); 19 | 20 | // check that the array data was modified by reference in python 21 | if (numbers[1] !== 999) 22 | throw new Error('array not modified by python'); 23 | 24 | // check that the array we get from python is the same reference as defined in js 25 | if (!Object.is(numbers, numbersBack)) 26 | throw new Error('array reference differs between JavaScript and Python'); 27 | 28 | console.log('pass'); 29 | 30 | -------------------------------------------------------------------------------- /tests/js/py2js/function-curry.simple.failing: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/function-curry.simple 3 | * Curry a JS function in python. 4 | * @author Liang Wang, liang@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict' 8 | 9 | // stolen from: https://www.askpython.com/python/examples/currying-in-python 10 | const curry = (() => { 11 | python.exec( 12 | ` 13 | from inspect import signature 14 | def curry(func): 15 | 16 | def inner(arg): 17 | 18 | #checking if the function has one argument, 19 | #then return function as it is 20 | if len(signature(func).parameters) == 1: 21 | return func(arg) 22 | 23 | return curry(partial(func, arg)) 24 | 25 | return inner 26 | ` 27 | ); 28 | 29 | return python.eval('curry') 30 | })(); 31 | const takeMiddle = (_x, y, _z) => y; 32 | const curried = curry(takeMiddle); 33 | const middle = curried(1)(2)(3); 34 | 35 | if (middle !== 2) 36 | { 37 | console.error('expected', 2, 'but got', middle); 38 | throw new Error('test failed'); 39 | } 40 | 41 | console.log('test passes, the thing knows how to curry'); 42 | -------------------------------------------------------------------------------- /src/JSStringProxy.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSStringProxy.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (plaporte@distributive.network) 4 | * @brief JSStringProxy is a custom C-implemented python type that derives from str. It acts as a proxy for JSStrings from Spidermonkey, and behaves like a str would. 5 | * @date 2024-05-15 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/JSStringProxy.hh" 12 | 13 | #include "include/StrType.hh" 14 | 15 | std::unordered_set jsStringProxies; 16 | extern JSContext *GLOBAL_CX; 17 | 18 | 19 | void JSStringProxyMethodDefinitions::JSStringProxy_dealloc(JSStringProxy *self) 20 | { 21 | jsStringProxies.erase(self); 22 | delete self->jsString; 23 | } 24 | 25 | PyObject *JSStringProxyMethodDefinitions::JSStringProxy_copy_method(JSStringProxy *self) { 26 | JS::RootedString selfString(GLOBAL_CX, ((JSStringProxy *)self)->jsString->toString()); 27 | JS::RootedValue selfStringValue(GLOBAL_CX, JS::StringValue(selfString)); 28 | return StrType::proxifyString(GLOBAL_CX, selfStringValue); 29 | } -------------------------------------------------------------------------------- /include/DateType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DateType.hh 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python dates 5 | * @date 2022-12-21 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_DateType_ 12 | #define PythonMonkey_DateType_ 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | /** 20 | * @brief This struct represents the 'datetime' type in Python from the datetime module, which is represented as a 'Date' object in JS 21 | */ 22 | struct DateType { 23 | public: 24 | /** 25 | * @brief Convert a JS Date object to Python datetime 26 | */ 27 | static PyObject *getPyObject(JSContext *cx, JS::HandleObject dateObj); 28 | 29 | /** 30 | * @brief Convert a Python datetime object to JS Date 31 | * 32 | * @param cx - javascript context pointer 33 | * @param pyObject - the python datetime object to be converted 34 | */ 35 | static JSObject *toJsDate(JSContext *cx, PyObject *pyObject); 36 | }; 37 | 38 | #endif -------------------------------------------------------------------------------- /tests/js/set-interval.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file set-interval.bash 4 | # A peter-jr test which ensures that the `setInterval` global function works properly. 5 | # 6 | # @author Tom Tang (xmader@distributive.network) 7 | # @date April 2024 8 | 9 | set -u 10 | set -o pipefail 11 | 12 | panic() 13 | { 14 | echo "FAIL: $*" >&2 15 | exit 2 16 | } 17 | 18 | cd `dirname "$0"` || panic "could not change to test directory" 19 | 20 | code=' 21 | let n = 0; 22 | const timer = setInterval(()=> 23 | { 24 | console.log("callback called"); 25 | 26 | n++; 27 | if (n >= 5) clearInterval(timer); // clearInterval should work inside the callback 28 | 29 | throw new Error("testing from the callback"); // timer should continue running regardless of whether the job function succeeds or not 30 | }, 50); 31 | ' 32 | 33 | "${PMJS:-pmjs}" \ 34 | -e "$code" \ 35 | < /dev/null 2> /dev/null \ 36 | | tr -d '\r' \ 37 | | grep -c '^callback called$' \ 38 | | while read qty 39 | do 40 | echo "callback called: $qty" 41 | [ "$qty" != "5" ] && panic qty should not be $qty 42 | break 43 | done || exit $? 44 | -------------------------------------------------------------------------------- /tests/js/console-stdio.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file console-stdio.bash 4 | # A peter-jr test which ensures that the console object uses the right file descriptors. 5 | # 6 | # @author Wes Garland, wes@distributive.network 7 | # @date July 2023 8 | 9 | set -u 10 | set -o pipefail 11 | 12 | panic() 13 | { 14 | echo "FAIL: $*" >&2 15 | exit 2 16 | } 17 | 18 | cd `dirname "$0"` || panic "could not change to test directory" 19 | 20 | "${PMJS:-pmjs}" \ 21 | -e 'console.log("stdout")' \ 22 | -e 'console.debug("stdout")' \ 23 | -e 'console.info("stdout")' \ 24 | < /dev/null \ 25 | | tr -d '\r' \ 26 | | grep -c '^stdout$' \ 27 | | while read qty 28 | do 29 | echo "stdout: $qty" 30 | [ "$qty" != "3" ] && panic qty should not be $qty 31 | break 32 | done || exit $? 33 | 34 | "${PMJS:-pmjs}" \ 35 | -e 'console.error("stderr")' \ 36 | -e 'console.warn("stderr")' \ 37 | < /dev/null 2>&1 \ 38 | | tr -d '\r' \ 39 | | grep -c '^stderr$' \ 40 | | while read qty 41 | do 42 | echo "stderr: $qty" 43 | [ "$qty" != "2" ] && panic qty should not be $qty 44 | break 45 | done || exit $? 46 | 47 | echo "done" 48 | -------------------------------------------------------------------------------- /tests/js/console-this.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file console-this.simple 3 | * 4 | * Test which ensures that the Console objects have unique own properties and bound this. 5 | * Regression test for issue #90. 6 | * 7 | * @author Wes Garland, wes@distributive.network 8 | * @date July 2023 9 | */ 10 | 'use strict'; 11 | 12 | var exitCode = 0; 13 | function assert(test) 14 | { 15 | if (!test) 16 | { 17 | exitCode = 1; 18 | console.error(new Error('Assertion failure')); 19 | } 20 | } 21 | 22 | const c = console; 23 | 24 | assert(c.log !== c.debug); 25 | assert(c.log !== c.warn); 26 | assert(c.log !== c.info); 27 | assert(c.warn !== c.error); 28 | assert(typeof c.log === 'function'); 29 | assert(typeof c.debug === 'function'); 30 | assert(typeof c.info === 'function'); 31 | assert(typeof c.error === 'function'); 32 | assert(typeof c.warn === 'function'); 33 | assert(c.hasOwnProperty('debug')); 34 | assert(c.hasOwnProperty('log')); 35 | assert(c.hasOwnProperty('info')); 36 | assert(c.hasOwnProperty('warn')); 37 | assert(c.hasOwnProperty('error')); 38 | 39 | c.log('test done'); /* throws if wrong this, issue #90 */ 40 | python.exit(exitCode); 41 | -------------------------------------------------------------------------------- /include/setSpiderMonkeyException.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file setSpiderMonkeyException.hh 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief Call this function whenever a JS_* function call fails in order to set an appropriate python exception (remember to also return NULL) 5 | * @date 2023-02-28 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_setSpiderMonkeyException_ 12 | #define PythonMonkey_setSpiderMonkeyException_ 13 | 14 | #include 15 | 16 | /** 17 | * @brief Convert the given SpiderMonkey exception stack to a Python string 18 | * 19 | * @param cx - pointer to the JS context 20 | * @param exceptionStack - reference to the SpiderMonkey exception stack 21 | * @param printStack - whether or not to print the JS stack 22 | */ 23 | PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack, bool printStack); 24 | 25 | /** 26 | * @brief This function sets a python error under the assumption that a JS_* function call has failed. Do not call this function if that is not the case. 27 | * 28 | * @param cx - pointer to the JS context 29 | */ 30 | void setSpiderMonkeyException(JSContext *cx); 31 | 32 | #endif -------------------------------------------------------------------------------- /tests/js/py2js/boolean.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/boolean.simple - Asserts that sending Python `bool`'s' to JS 3 | * retains the corresponding logical values, and don't become boxed 4 | * booleans. 5 | * 6 | * @author Bryan Hoang 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const throughJS = x => x; 12 | 13 | const pyTrue = python.eval('True'); 14 | const maybePyTrue = throughJS(pyTrue); 15 | if (maybePyTrue !== pyTrue) 16 | { 17 | console.error('Expected', pyTrue, 'but got', maybePyTrue); 18 | throw new Error('Test failed'); 19 | } 20 | 21 | if (typeof maybePyTrue !== 'boolean') 22 | { 23 | console.error('Expected typeof boolean', 'but got', typeof maybePyTrue); 24 | throw new Error('Test failed'); 25 | } 26 | 27 | const pyFalse = python.eval('False'); 28 | const maybePyFalse = throughJS(pyFalse); 29 | if (maybePyFalse !== pyFalse) 30 | { 31 | console.error('Expected', pyFalse, 'but got', maybePyFalse); 32 | throw new Error('test failed'); 33 | } 34 | 35 | if (typeof maybePyFalse !== 'boolean') 36 | { 37 | console.error('Expected typeof boolean', 'but got', typeof maybePyFalse); 38 | throw new Error('Test failed'); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /python/pminit/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pminit" 3 | version = "0" 4 | description = "Post-install hook for PythonMonkey" 5 | authors = [ 6 | "Distributive Corp. " 7 | ] 8 | license = "MIT" 9 | homepage = "https://pythonmonkey.io/" 10 | documentation = "https://docs.pythonmonkey.io/" 11 | repository = "https://github.com/Distributive-Network/PythonMonkey" 12 | 13 | include = [ 14 | # Install extra files into the pythonmonkey package 15 | "pythonmonkey/package*.json", 16 | { path = "pythonmonkey/node_modules/**/*", format = "wheel" }, 17 | # pip builds (and actually installs from) the wheel when installing from sdist 18 | # we don't want node_modules inside the sdist package 19 | ] 20 | 21 | [tool.poetry.dependencies] 22 | python = "^3.8" 23 | 24 | [tool.poetry-dynamic-versioning] 25 | enable = true 26 | vcs = "git" 27 | style = "pep440" 28 | bump = true 29 | 30 | [tool.poetry.build] 31 | script = "post-install-hook.py" 32 | generate-setup-file = false 33 | 34 | [tool.poetry.scripts] 35 | pminit = "pminit.cli:main" 36 | 37 | [build-system] 38 | requires = ["poetry-core>=1.1.1", "poetry-dynamic-versioning==1.1.1"] 39 | build-backend = "poetry_dynamic_versioning.backend" 40 | 41 | -------------------------------------------------------------------------------- /tests/js/timer-reject.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file timer-reject.bash 4 | # A peter-jr test which shows that unhandled rejections in timers get shown on stderr, 5 | # exit with status 1, and aren't delayed because of pending events. 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 9 | # 10 | # timeout: 10 11 | 12 | set -u 13 | set -o pipefail 14 | 15 | panic() 16 | { 17 | echo "FAIL: $*" >&2 18 | exit 2 19 | } 20 | 21 | cd `dirname "$0"` || panic "could not change to test directory" 22 | 23 | "${PMJS:-../../pmjs}" ./timer-reject.js 2>&1 1>/dev/null \ 24 | | egrep 'hello|goodbye|fire' \ 25 | | ( 26 | read line 27 | if [[ "$line" =~ hello ]]; then 28 | echo "found expected '$line'" 29 | else 30 | panic "expected hello, found '${line}'" 31 | fi 32 | 33 | read line 34 | if [[ "$line" =~ Error:.goodbye ]]; then 35 | echo "found expected '$line'" 36 | else 37 | panic "expected Error: goodbye, found '${line}'" 38 | fi 39 | ) 40 | exitCode="$?" 41 | 42 | if [ "${exitCode}" = "1" ]; then 43 | echo pass 44 | exit 0 45 | fi 46 | 47 | [ "$exitCode" = 2 ] || panic "Exit code was $exitCode" 48 | -------------------------------------------------------------------------------- /include/IntType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file IntType.hh 3 | * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network), Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python ints 5 | * @date 2023-03-16 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_IntType_ 12 | #define PythonMonkey_IntType_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * @brief This struct represents the 'int' type (arbitrary-precision) in Python 20 | */ 21 | struct IntType { 22 | public: 23 | /** 24 | * @brief Construct a new PyObject from a JS::BigInt. 25 | * 26 | * @param cx - javascript context pointer 27 | * @param bigint - JS::BigInt pointer 28 | * 29 | * @returns PyObject* pointer to the resulting PyObject 30 | */ 31 | static PyObject *getPyObject(JSContext *cx, JS::BigInt *bigint); 32 | 33 | /** 34 | * @brief Convert an int object to a JS::BigInt 35 | * 36 | * @param cx - javascript context pointer 37 | * @param pyObject - the int object to be converted 38 | */ 39 | static JS::BigInt *toJsBigInt(JSContext *cx, PyObject *pyObject); 40 | }; 41 | 42 | #endif -------------------------------------------------------------------------------- /python/pminit/pminit/cli.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import subprocess 3 | import argparse 4 | 5 | def execute(cmd: str, cwd: str): 6 | popen = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, 7 | shell = True, text = True, cwd = cwd ) 8 | for stdout_line in iter(popen.stdout.readline, ""): 9 | sys.stdout.write(stdout_line) 10 | sys.stdout.flush() 11 | 12 | popen.stdout.close() 13 | return_code = popen.wait() 14 | if return_code != 0: 15 | sys.exit(return_code) 16 | 17 | def commandType(value: str): 18 | if value != "npm": 19 | raise argparse.ArgumentTypeError("Value must be npm.") 20 | return value 21 | 22 | def main(): 23 | parser = argparse.ArgumentParser(description="A tool to enable running npm on the correct package.json location") 24 | parser.add_argument("executable", nargs=1, help="Should be npm.", type=commandType) 25 | parser.add_argument("args", nargs = argparse.REMAINDER) 26 | args = parser.parse_args() 27 | 28 | pythonmonkey_path= os.path.realpath( 29 | os.path.join( 30 | os.path.dirname(__file__), 31 | '..', 32 | 'pythonmonkey' 33 | ) 34 | ) 35 | 36 | execute(' '.join( args.executable + args.args ), pythonmonkey_path) 37 | -------------------------------------------------------------------------------- /tests/js/pmjs-require-cache.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file pmjs-require-cache.bvash 4 | # A peter-jr test which ensures that the require cache for the extra-module environment 5 | # is the same as the require cache for the program module. 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 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 | loaded=0 22 | "${PMJS:-pmjs}" -r ./modules/print-load -r ./modules/print-load program.js |\ 23 | tr -d '\r' |\ 24 | while read keyword rest 25 | do 26 | case "$keyword" in 27 | "LOADED") 28 | echo "${keyword} ${rest}" 29 | loaded=$[${loaded} + 1] 30 | [ "${loaded}" != 1 ] && panic "loaded module more than once!" 31 | ;; 32 | "FINISHED") 33 | echo "${keyword} ${rest}" 34 | if [ "${loaded}" = 1 ]; then 35 | echo "Done" 36 | exit 111 37 | fi 38 | ;; 39 | *) 40 | echo "Ignored: ${keyword} ${rest} (${loaded})" 41 | ;; 42 | esac 43 | done 44 | 45 | exitCode="$?" 46 | [ "$exitCode" = "111" ] && exit 0 47 | [ "$exitCode" = "2" ] && exit 2 48 | 49 | panic "Test did not run to completion" 50 | -------------------------------------------------------------------------------- /tests/python/test_list_array.py: -------------------------------------------------------------------------------- 1 | import pythonmonkey as pm 2 | 3 | 4 | def test_eval_array_is_list(): 5 | pythonList = pm.eval('[]') 6 | assert isinstance(pythonList, list) 7 | 8 | # extra nice but not necessary 9 | 10 | 11 | def test_eval_array_is_list_type_string(): 12 | pythonListTypeString = str(type(pm.eval('[]'))) 13 | assert pythonListTypeString == "" 14 | 15 | 16 | def test_eval_list_is_array(): 17 | items = [1, 2, 3] 18 | isArray = pm.eval('Array.isArray')(items) 19 | assert isArray 20 | 21 | 22 | def test_typeof_array(): 23 | items = [1, 2, 3] 24 | result = [None] 25 | pm.eval("(result, arr) => {result[0] = typeof arr}")(result, items) 26 | assert result[0] == 'object' 27 | 28 | 29 | def test_instanceof_array(): 30 | items = [1, 2, 3] 31 | result = [None] 32 | pm.eval("(result, arr) => {result[0] = arr instanceof Array}")(result, items) 33 | assert result[0] 34 | 35 | 36 | def test_instanceof_object(): 37 | items = [1, 2, 3] 38 | result = [None] 39 | pm.eval("(result, arr) => {result[0] = arr instanceof Object}")(result, items) 40 | assert result[0] 41 | 42 | 43 | def test_not_instanceof_string(): 44 | items = [1, 2, 3] 45 | result = [None] 46 | pm.eval("(result, arr) => {result[0] = arr instanceof String}")(result, items) 47 | assert not result[0] 48 | -------------------------------------------------------------------------------- /tests/js/program-module.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file program-module.simple 3 | * Test subtle details about program modules scopes 4 | * @author Wes Garland, wes@distributive.network 5 | * @date July 2023 6 | */ 7 | 'use strict'; 8 | 9 | var failures = 0; 10 | 11 | function check(message, testResult) 12 | { 13 | const facility = testResult ? 'log' : 'error'; 14 | 15 | console[facility](message + ':', testResult ? 'PASS' : 'FAIL'); 16 | if (!testResult) 17 | failures++; 18 | } 19 | 20 | check('require.main is an object', typeof require.main === 'object'); 21 | check('require.main is the current module', require.main === module); 22 | check('require is the global require', require === globalThis.require); 23 | check('exports is the global exports', exports === globalThis.exports); 24 | check('module is the global module', module === globalThis.module); 25 | 26 | // eslint-disable-next-line no-global-assign 27 | module = {}; 28 | check('free module symbol is global symbol', module === globalThis.module); 29 | check('arguments.length is ' + arguments.length, ...arguments); 30 | 31 | if (failures) 32 | console.log(`${failures} sub-tests failing`); 33 | else 34 | console.log('all sub-tests pass', typeof failures); 35 | 36 | python.exit(failures); 37 | -------------------------------------------------------------------------------- /tests/js/js2py/datetime.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/datetime.simple.failing 3 | * Simple test which shows that sending Dates to Python and getting them back into JS 4 | * works as expected 5 | * 6 | * @author Elijah Deluzio, elijah@distributive.network 7 | * @date July 2023 8 | */ 9 | 'use strict' 10 | 11 | const throughPython = python.eval('(lambda x: x)'); 12 | var expectedJsTimestamp; 13 | var jsDate; 14 | var pyDate; 15 | 16 | // Test 1: Date from timestamp of 0 (1970 - 01 - 01), timestamp = 0 17 | jsDate = new Date(Date.UTC(1970, 0, 1, 0, 0, 0)); 18 | expectedJsTimestamp = jsDate.getTime(); 19 | pyDate = throughPython(jsDate); 20 | 21 | if (expectedJsTimestamp !== pyDate.getTime()) 22 | { 23 | console.error('expected', expectedJsTimestamp, 'but got', pyDate.getTime()); 24 | throw new Error('test failed'); 25 | } 26 | 27 | console.log('pass -', pyDate); 28 | 29 | // Test 2: Date from 21st century (2222 - 02 - 03), timestamp = 7955193600000 30 | jsDate = new Date(Date.UTC(2222, 1, 3, 0, 0, 0)); 31 | expectedJsTimestamp = jsDate.getTime(); 32 | pyDate = throughPython(jsDate); 33 | 34 | if (expectedJsTimestamp !== pyDate.getTime()) 35 | { 36 | console.error('expected', expectedJsTimestamp, 'but got', pyDate.getTime()); 37 | throw new Error('test failed'); 38 | } 39 | 40 | console.log('pass -', pyDate); -------------------------------------------------------------------------------- /python/pminit/post-install-hook.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import shutil 4 | 5 | def execute(cmd: str): 6 | popen = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, 7 | shell = True, text = True ) 8 | for stdout_line in iter(popen.stdout.readline, ""): 9 | sys.stdout.write(stdout_line) 10 | sys.stdout.flush() 11 | 12 | popen.stdout.close() 13 | return_code = popen.wait() 14 | if return_code: 15 | raise subprocess.CalledProcessError(return_code, cmd) 16 | 17 | def main(): 18 | node_package_manager = 'npm' 19 | # check if npm is installed on the system 20 | if (shutil.which(node_package_manager) is None): 21 | print(""" 22 | 23 | PythonMonkey Build Error: 24 | 25 | 26 | * It appears npm is not installed on this system. 27 | * npm is required for PythonMonkey to build. 28 | * Please install NPM and Node.js before installing PythonMonkey. 29 | * Refer to the documentation for installing NPM and Node.js here: https://nodejs.org/en/download 30 | 31 | 32 | """) 33 | raise Exception("PythonMonkey build error: Unable to find npm on the system.") 34 | else: 35 | execute(f"cd pythonmonkey && {node_package_manager} i --no-package-lock") # do not update package-lock.json 36 | 37 | if __name__ == "__main__": 38 | main() 39 | 40 | -------------------------------------------------------------------------------- /python/pythonmonkey/lib/wtfpm.py: -------------------------------------------------------------------------------- 1 | # @file WTFPythonMonkey - A tool that detects any hanging setTimeout/setInterval timers when Ctrl-C is hit 2 | # @author Tom Tang 3 | # @date April 2024 4 | # @copyright Copyright (c) 2024 Distributive Corp. 5 | 6 | import pythonmonkey as pm 7 | 8 | 9 | def printTimersDebugInfo(): 10 | pm.eval("""(require) => { 11 | const internalBinding = require('internal-binding'); 12 | const { getAllRefedTimersDebugInfo: getDebugInfo } = internalBinding('timers'); 13 | console.log(getDebugInfo()) 14 | console.log(new Date()) 15 | }""")(pm.createRequire(__file__)) 16 | 17 | 18 | class WTF: 19 | """ 20 | WTFPythonMonkey to use as a Python context manager (`with`-statement) 21 | 22 | Usage: 23 | ```py 24 | from pythonmonkey.lib.wtfpm import WTF 25 | 26 | with WTF(): 27 | # the main entry point for the program utilizes PythonMonkey event-loop 28 | asyncio.run(pythonmonkey_main()) 29 | ``` 30 | """ 31 | 32 | def __enter__(self): 33 | pass 34 | 35 | def __exit__(self, errType, errValue, traceback): 36 | if errType is None: # no exception 37 | return 38 | elif issubclass(errType, KeyboardInterrupt): # except KeyboardInterrupt: 39 | printTimersDebugInfo() 40 | return True # exception suppressed 41 | else: # other exceptions 42 | return False 43 | -------------------------------------------------------------------------------- /tests/js/typeofs-segfaults.simple.failing: -------------------------------------------------------------------------------- 1 | /** 2 | * @file typeofs-segfaults.simple.failing 3 | * 4 | * Parts of typeofs.simple that segfault - should be moved back to typeofs once they work. 5 | * 6 | * @author Wes Garland, wes@distributive.network 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const throughJS = x => x; 12 | const throughBoth = python.eval('(lambda x: throughJS(x))', { throughJS }); 13 | 14 | function check(jsval, expected) 15 | { 16 | var disp; 17 | switch (typeof expected) 18 | { 19 | case 'function': 20 | disp = expected.name || '(anonymous function)' 21 | break; 22 | case 'object': 23 | disp = JSON.stringify(expected); 24 | break; 25 | default: 26 | disp = String(expected); 27 | } 28 | 29 | console.log(`${jsval}? -`, disp); 30 | 31 | switch (typeof expected) 32 | { 33 | default: 34 | throw new Error(`invalid expectation ${disp} (${typeof expected})`); 35 | case 'string': 36 | if (typeof jsval !== expected) 37 | throw new Error(`expected ${disp} but got ${typeof jsval}`); 38 | break; 39 | case 'function': 40 | if (!(jsval instanceof expected)) 41 | throw new Error(`expected instance of ${expected.name} but got ${jsval.constructor.name}`); 42 | } 43 | } 44 | 45 | check(throughBoth(new Date()), 'object') 46 | check(throughBoth(new Promise(()=>1)), Promise) 47 | -------------------------------------------------------------------------------- /include/ExceptionType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ExceptionType.hh 3 | * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing Python Exception objects from a corresponding JS Error object 5 | * @date 2023-04-11 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_ExceptionType_ 12 | #define PythonMonkey_ExceptionType_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * @brief This struct represents a Python Exception object from the corresponding JS Error object 20 | */ 21 | struct ExceptionType { 22 | public: 23 | /** 24 | * @brief Construct a new SpiderMonkeyError from the JS Error object. 25 | * 26 | * @param cx - javascript context pointer 27 | * @param error - JS Error object to be converted 28 | * 29 | * @returns PyObject* pointer to the resulting PyObject 30 | */ 31 | static PyObject *getPyObject(JSContext *cx, JS::HandleObject error); 32 | 33 | /** 34 | * @brief Convert a python Exception object to a JS Error object 35 | * 36 | * @param cx - javascript context pointer 37 | * @param exceptionValue - Exception object pointer, cannot be NULL 38 | * @param traceBack - Exception traceback pointer, can be NULL 39 | */ 40 | static JSObject *toJsError(JSContext *cx, PyObject *exceptionValue, PyObject *traceBack); 41 | }; 42 | 43 | #endif -------------------------------------------------------------------------------- /tests/js/py2js/object-methods.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/object.simple 3 | * Simple test which shows that sending objects from Python to JS still retains 4 | * the ability to perform basic Object methods like Object.keys(), Object.values() and 5 | * Object.entries() on them. 6 | * @author Joash Mathew, 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const obj = python.eval('{ "a": 1, "b": 2, "c": 3 }'); 12 | const throughJS = (x) => x; 13 | const jsObj = throughJS(obj); 14 | const standardJSObj = { a: 1, b: 2, c: 3 }; 15 | 16 | if (JSON.stringify(Object.keys(jsObj)) !== JSON.stringify(Object.keys(standardJSObj))) 17 | { 18 | console.error('The output of the PythonMonkey JS object does not match the output of a standard JS Object.'); 19 | throw new Error('Test failed'); 20 | } 21 | 22 | if (JSON.stringify(Object.values(jsObj)) !== JSON.stringify(Object.values(standardJSObj))) 23 | { 24 | console.error('The output of the PythonMonkey JS object does not match the output of a standard JS Object.'); 25 | throw new Error('Test failed'); 26 | } 27 | 28 | if (JSON.stringify(Object.entries(jsObj)) !== JSON.stringify(Object.entries(standardJSObj))) 29 | { 30 | console.error('The output of the PythonMonkey JS object does not match the output of a standard JS Object.'); 31 | throw new Error('Test failed'); 32 | } 33 | 34 | console.log('Test passed'); -------------------------------------------------------------------------------- /tests/js/pmjs-global-arguments.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file pmjs-global-arguments.bash 4 | # A peter-jr test which ensures that the free variable arguments in the program module's 5 | # context is equivalent to the pmjs argv. 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 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 | argc=0 22 | "${PMJS:-pmjs}" program.js abc easy as one two three |\ 23 | tr -d '\r' |\ 24 | while read keyword rest 25 | do 26 | case "$keyword" in 27 | "ARG") 28 | argc=$[${argc} + 1] 29 | echo "${argc} ${keyword} ${rest}" 30 | ;; 31 | "ARGC") 32 | echo "${keyword} ${rest}" 33 | if [ "${rest}" != "${argc}" ]; then 34 | panic "program reported argc=${rest} but we only counted ${argc} arguments" 35 | fi 36 | ;; 37 | "FINISHED") 38 | if [ "${argc}" -gt 2 ]; then 39 | echo "Done" 40 | exit 111 41 | fi 42 | panic "Found ${argc} arguments, but seems like too few" 43 | ;; 44 | *) 45 | echo "Ignored: ${keyword} ${rest} (${argc})" 46 | ;; 47 | esac 48 | done 49 | 50 | exitCode="$?" 51 | [ "$exitCode" = "111" ] && exit 0 52 | [ "$exitCode" = "2" ] && exit 2 53 | 54 | panic "Test did not run to completion" 55 | -------------------------------------------------------------------------------- /tests/js/uncaught-rejection-handler.bash: -------------------------------------------------------------------------------- 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 (xmader@distributive.network) 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 $? 54 | -------------------------------------------------------------------------------- /tests/js/js2py/error.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/string.simple 3 | * Ensures that an error constructed in PY and binded to JS retains error message. 4 | * Error properties are not retained. 5 | * 6 | * @author David Courtis, david@distributive.network 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const throughJS = x => x; 12 | let errorFlag = false; 13 | 14 | function tester(exceptionpy, exceptionjs) 15 | { 16 | if (!exceptionpy.toString() === exceptionjs.toString()) 17 | { 18 | console.error('Expected\n', exceptionpy.toString(), '\nbut got\n', exceptionjs.toString()); 19 | errorFlag = true; 20 | } 21 | else 22 | { 23 | console.log('pass -', exceptionpy.toString()); 24 | } 25 | } 26 | 27 | function inbuiltError() 28 | { 29 | const exceptionpy = python.eval('Exception(\'I know Python!\')'); 30 | const exceptionjs = throughJS(exceptionpy); 31 | tester(exceptionpy, exceptionjs); 32 | } 33 | 34 | function customError() 35 | { 36 | python.exec( 37 | `class IAmAnError(Exception): 38 | def __init__(self, message): 39 | super().__init__(message) 40 | ` 41 | ); 42 | const exceptionpy = python.eval('IAmAnError(\'I know Python!\')'); 43 | const exceptionjs = throughJS(exceptionpy); 44 | tester(exceptionpy, exceptionjs); 45 | } 46 | 47 | inbuiltError(); 48 | customError(); 49 | 50 | if (errorFlag) 51 | { 52 | throw new Error('test failed'); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /tests/js/pmjs-interactive-smoke.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file pmjs-global-arguments.bash 4 | # A peter-jr smoke test which takes the REPL through executing a multiline 5 | # expression 6 | # 7 | # @author Wes Garland, wes@distributive.network 8 | # @date July 2023 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 | rnd=$RANDOM 22 | dotLines=0 23 | ("${PMJS:-pmjs}" -i < 15 | 16 | #include 17 | 18 | /** 19 | * @brief This struct represents the 'string' type in Python, which is represented as a 'char*' in C++ 20 | */ 21 | struct StrType { 22 | public: 23 | /** 24 | * @brief Construct a new unicode PyObject from a JSString. Automatically handles encoding conversion for latin1 & UCS2: 25 | * codepoint | Python | Spidermonkey | identical representation? 26 | * 000000-0000FF | latin1 | latin1 | Yes 27 | * 000100-00D7FF | UCS2 | UTF16 | Yes 28 | * 00D800-00DFFF | UCS2 (unpaired) | UTF16 (unpaired) | Yes 29 | * 00E000-00FFFF | UCS2 | UTF16 | Yes 30 | * 010000-10FFFF | UCS4 | UTF16 | No, conversion and new backing store required, user must explicitly call asUCS4() -> static in code 31 | * 32 | * @param cx - javascript context pointer 33 | * @param str - JSString pointer 34 | * 35 | * @returns PyObject* pointer to the resulting PyObject 36 | */ 37 | static PyObject *getPyObject(JSContext *cx, JS::HandleValue str); 38 | 39 | static PyObject *proxifyString(JSContext *cx, JS::HandleValue str); 40 | }; 41 | 42 | #endif -------------------------------------------------------------------------------- /tests/js/commonjs-modules.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file commonjs-modules.bash 4 | # A peter-jr test which runs the CommonJS Modules/1.0 test suite. Any failure in the 5 | # suite causes this test to fail. 6 | # @author Wes Garland, wes@distributive.network 7 | # @date June 2023 8 | # 9 | # timeout: 40 10 | 11 | panic() 12 | { 13 | echo "$*" >&2 14 | exit 2 15 | } 16 | 17 | cd `dirname "$0"` 18 | git submodule update --init --recursive || panic "could not checkout the required git submodule" 19 | 20 | cd ../commonjs-official/tests/modules/1.0 || panic "could not change to test directory" 21 | 22 | runTest() 23 | { 24 | testName="`printf '%20s' \"$1\"`" 25 | set -o pipefail 26 | echo -n "${testName}: " 27 | 28 | PMJS_PATH="`pwd`" pmjs -e 'print=python.print' program.js\ 29 | | tr -d '\r'\ 30 | | while read word rest 31 | do 32 | case "$word" in 33 | "PASS"|"DONE") 34 | echo -n "$word $rest" 35 | return 0 36 | ;; 37 | "FAIL") 38 | echo -n "\r${testName}: $word $rest" >&2 39 | return 1 40 | ;; 41 | *) 42 | echo "$word $rest" 43 | echo -n "${testName}: " 44 | ;; 45 | esac 46 | (exit 2) 47 | done 48 | ret="$?" 49 | echo 50 | return "$ret" 51 | } 52 | 53 | find . -name program.js \ 54 | | while read program 55 | do 56 | testDir=`dirname "${program}"` 57 | cd "${testDir}" 58 | runTest "`basename ${testDir}`" || failures=$[${failures:-0} + 1] 59 | cd .. 60 | (exit ${failures-0}) 61 | done 62 | failures="$?" 63 | echo "Done; failures: $failures" 64 | [ "${failures}" = 0 ] 65 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${CMAKE_CURRENT_LIST_DIR}) 2 | 3 | set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) 4 | if(APPLE) 5 | set(CMAKE_INSTALL_RPATH "@loader_path") 6 | elseif(UNIX) 7 | set(CMAKE_INSTALL_RPATH "$ORIGIN") 8 | endif() 9 | 10 | list(APPEND PYTHONMONKEY_SOURCE_FILES ${SOURCE_FILES} "${CMAKE_SOURCE_DIR}/src/modules/pythonmonkey/pythonmonkey.cc") 11 | 12 | add_library(pythonmonkey SHARED 13 | ${PYTHONMONKEY_SOURCE_FILES} 14 | ) 15 | 16 | target_include_directories(pythonmonkey PUBLIC ..) 17 | target_compile_definitions(pythonmonkey PRIVATE BUILD_TYPE="${PM_BUILD_TYPE} $") 18 | 19 | if(WIN32) 20 | set_target_properties( 21 | pythonmonkey 22 | PROPERTIES 23 | PREFIX "" 24 | SUFFIX ".pyd" 25 | OUTPUT_NAME "pythonmonkey" 26 | ) 27 | elseif(UNIX) 28 | set_target_properties( 29 | pythonmonkey 30 | PROPERTIES 31 | PREFIX "" 32 | SUFFIX ".so" 33 | ) 34 | endif() 35 | 36 | # Don't link against `libpython` on Unix, see https://peps.python.org/pep-0513/#libpythonx-y-so-1 37 | if (WIN32) # linker error on Windows: `lld-link: could not open 'python311.lib': no such file or directory` 38 | target_link_libraries(pythonmonkey ${PYTHON_LIBRARIES}) 39 | endif() 40 | 41 | # Stop complaining about missing symbols 42 | # see https://blog.tim-smith.us/2015/09/python-extension-modules-os-x/ 43 | if(APPLE) 44 | target_link_options(pythonmonkey PRIVATE "SHELL:-undefined dynamic_lookup") 45 | endif() 46 | 47 | target_link_libraries(pythonmonkey ${SPIDERMONKEY_LIBRARIES}) 48 | 49 | target_include_directories(pythonmonkey PRIVATE ${PYTHON_INCLUDE_DIR}) 50 | target_include_directories(pythonmonkey PRIVATE ${SPIDERMONKEY_INCLUDE_DIR}) 51 | -------------------------------------------------------------------------------- /tests/js/py2js/datetime.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file py2js/datetime.simple.failing 3 | * Simple test which shows that sending Python datetime.datetimes to JS and getting them back into 4 | * Python works as expected 5 | * 6 | * @author Elijah Deluzio, elijah@distributive.network 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const throughJS = x => x; 12 | var expectedJsTimestamp; 13 | var actualJsTimestamp; 14 | var jsDate; 15 | var pyDate; 16 | 17 | // Test 1: Date from timestamp of 0 (1970 - 01 - 01), timestamp = 0 18 | expectedJsTimestamp = 0; 19 | python.exec('from datetime import timezone'); 20 | python.exec('import datetime'); 21 | pyDate = python.eval(`datetime.datetime.fromtimestamp(${expectedJsTimestamp}, tz=timezone.utc)`); 22 | jsDate = throughJS(pyDate); 23 | 24 | if (jsDate.getTime() !== expectedJsTimestamp) 25 | { 26 | console.error('expected', expectedJsTimestamp, 'but got', jsDate.getTime()); 27 | throw new Error('test failed'); 28 | } 29 | 30 | console.log('pass -', jsDate); 31 | 32 | // Test 2: Date from 21st century (2222 - 02 - 03), timestamp = 7955193600 33 | expectedJsTimestamp = 7955193600; 34 | python.exec('from datetime import timezone'); 35 | python.exec('import datetime'); 36 | pyDate = python.eval(`datetime.datetime.fromtimestamp(${expectedJsTimestamp}, tz=timezone.utc)`); 37 | jsDate = throughJS(pyDate); 38 | 39 | actualJsTimestamp = jsDate.getTime() / 1000; // JS timestamp is in milliseconds, convert it to seconds 40 | if (actualJsTimestamp !== expectedJsTimestamp) 41 | { 42 | console.error('expected', expectedJsTimestamp, 'but got', actualJsTimestamp); 43 | throw new Error('test failed'); 44 | } 45 | 46 | console.log('pass -', jsDate); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # @file Makefile 2 | # Not part of the PythonMonkey build - just workflow helper for Wes. 3 | # @author Wes Garland, wes@distributive.network 4 | # @date March 2024 5 | # 6 | 7 | BUILD = Debug # (case-insensitive) Release, DRelease, Debug, Sanitize, Profile, or None 8 | DOCS = false 9 | VERBOSE = true 10 | PYTHON = python3 11 | RUN = poetry run 12 | 13 | OS_NAME := $(shell uname -s) 14 | 15 | ifeq ($(OS_NAME),Linux) 16 | CPU_COUNT = $(shell cat /proc/cpuinfo | grep -c processor) 17 | MAX_JOBS = 10 18 | CPUS := $(shell test $(CPU_COUNT) -lt $(MAX_JOBS) && echo $(CPU_COUNT) || echo $(MAX_JOBS)) 19 | PYTHON_BUILD_ENV += CPUS=$(CPUS) 20 | endif 21 | 22 | ifeq ($(BUILD),Profile) 23 | PYTHON_BUILD_ENV += BUILD_TYPE=Profile 24 | else ifeq ($(BUILD),Sanitize) 25 | PYTHON_BUILD_ENV += BUILD_TYPE=Sanitize 26 | else ifeq ($(BUILD),Debug) 27 | PYTHON_BUILD_ENV += BUILD_TYPE=Debug 28 | else ifeq ($(BUILD),DRelease) 29 | PYTHON_BUILD_ENV += BUILD_TYPE=DRelease 30 | else ifeq ($(BUILD), None) 31 | PYTHON_BUILD_ENV += BUILD_TYPE=None 32 | else # Release build 33 | PYTHON_BUILD_ENV += BUILD_TYPE=Release 34 | endif 35 | 36 | ifeq ($(DOCS),true) 37 | PYTHON_BUILD_ENV += BUILD_DOCS=1 38 | endif 39 | 40 | ifeq ($(VERBOSE),true) 41 | PYTHON_BUILD_ENV += VERBOSE=1 42 | endif 43 | 44 | .PHONY: build test all clean debug 45 | build: 46 | $(PYTHON_BUILD_ENV) $(PYTHON) ./build.py 47 | 48 | test: 49 | $(RUN) ./peter-jr tests 50 | $(RUN) pytest tests/python 51 | 52 | all: build test 53 | 54 | clean: 55 | rm -rf build/src/CMakeFiles/pythonmonkey.dir 56 | rm -f build/src/pythonmonkey.so 57 | rm -f python/pythonmonkey/pythonmonkey.so 58 | 59 | debug: 60 | @echo JOBS=$(JOBS) 61 | @echo CPU_COUNT=$(CPU_COUNT) 62 | @echo OS_NAME=$(OS_NAME) -------------------------------------------------------------------------------- /include/PyBaseProxyHandler.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PyBaseProxyHandler.hh 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Structs for creating JS proxy objects. 5 | * @date 2023-04-20 6 | * 7 | * @copyright Copyright (c) 2023-2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_PyBaseProxy_ 12 | #define PythonMonkey_PyBaseProxy_ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | /** 22 | * @brief base class for PyDictProxyHandler and PyListProxyHandler 23 | */ 24 | struct PyBaseProxyHandler : public js::BaseProxyHandler { 25 | public: 26 | PyBaseProxyHandler(const void *family) : js::BaseProxyHandler(family) {}; 27 | 28 | bool getPrototypeIfOrdinary(JSContext *cx, JS::HandleObject proxy, bool *isOrdinary, JS::MutableHandleObject protop) const override final; 29 | bool preventExtensions(JSContext *cx, JS::HandleObject proxy, JS::ObjectOpResult &result) const override final; 30 | bool isExtensible(JSContext *cx, JS::HandleObject proxy, bool *extensible) const override final; 31 | }; 32 | 33 | enum ProxySlots {PyObjectSlot, OtherSlot}; 34 | 35 | typedef struct { 36 | const char *name; /* The name of the method */ 37 | JSNative call; /* The C function that implements it */ 38 | uint16_t nargs; /* The argument count for the method */ 39 | } JSMethodDef; 40 | 41 | /** 42 | * @brief Convert jsid to a PyObject to be used as dict keys 43 | */ 44 | PyObject *idToKey(JSContext *cx, JS::HandleId id); 45 | 46 | /** 47 | * @brief Convert Python dict key to jsid 48 | */ 49 | bool keyToId(PyObject *key, JS::MutableHandleId idp); 50 | 51 | bool idToIndex(JSContext *cx, JS::HandleId id, Py_ssize_t *index); 52 | 53 | #endif -------------------------------------------------------------------------------- /include/PyListProxyHandler.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PyListProxyHandler.hh 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for creating JS proxy objects for Lists 5 | * @date 2023-12-01 6 | * 7 | * @copyright Copyright (c) 2023-2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_PyListProxy_ 12 | #define PythonMonkey_PyListProxy_ 13 | 14 | #include "include/PyBaseProxyHandler.hh" 15 | 16 | 17 | /** 18 | * @brief This struct is the ProxyHandler for JS Proxy Objects pythonmonkey creates 19 | * to handle coercion from python lists to JS Array objects 20 | */ 21 | struct PyListProxyHandler : public PyBaseProxyHandler { 22 | public: 23 | PyListProxyHandler() : PyBaseProxyHandler(&family) {}; 24 | static const char family; 25 | 26 | /** 27 | * @brief Handles python object reference count when JS Proxy object is finalized 28 | * 29 | * @param gcx pointer to JS::GCContext 30 | * @param proxy the proxy object being finalized 31 | */ 32 | void finalize(JS::GCContext *gcx, JSObject *proxy) const override; 33 | 34 | bool getOwnPropertyDescriptor( 35 | JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 36 | JS::MutableHandle> desc 37 | ) const override; 38 | 39 | bool defineProperty( 40 | JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 41 | JS::Handle desc, JS::ObjectOpResult &result 42 | ) const override; 43 | 44 | bool ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const override; 45 | bool delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult &result) const override; 46 | bool isArray(JSContext *cx, JS::HandleObject proxy, JS::IsArrayAnswer *answer) const override; 47 | bool getBuiltinClass(JSContext *cx, JS::HandleObject proxy, js::ESClass *cls) const override; 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /tests/js/typeofs.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file typeofs.simple 3 | * 4 | * Simple smoke test which checks typeof values going back and forth across the JS/PY barrier. 5 | * 6 | * @author Wes Garland, wes@distributive.network 7 | * @date July 2023 8 | */ 9 | 'use strict'; 10 | 11 | const throughJS = x => x; 12 | const throughBoth = python.eval('(lambda func: lambda x: func(x))')(throughJS); 13 | 14 | function check(jsval, expected) 15 | { 16 | var disp; 17 | switch (typeof expected) 18 | { 19 | case 'function': 20 | disp = expected.name || '(anonymous function)'; 21 | break; 22 | case 'object': 23 | disp = JSON.stringify(expected); 24 | break; 25 | default: 26 | disp = String(expected); 27 | } 28 | 29 | console.log(`${jsval}? -`, disp); 30 | 31 | switch (typeof expected) 32 | { 33 | default: 34 | throw new Error(`invalid expectation ${disp} (${typeof expected})`); 35 | case 'string': 36 | if (typeof jsval !== expected) 37 | throw new Error(`expected ${disp} but got ${typeof jsval}`); 38 | break; 39 | case 'function': 40 | if (!(jsval instanceof expected)) 41 | throw new Error(`expected instance of ${expected.name} but got ${jsval.constructor.name}`); 42 | } 43 | } 44 | 45 | check(throughBoth(123) , 'number'); 46 | check(throughBoth(123n), 'bigint'); 47 | check(throughBoth('sn'), 'string'); 48 | check(throughBoth({}), 'object'); 49 | check(throughBoth(true), 'boolean'); 50 | check(throughBoth(false), 'boolean'); 51 | check(throughBoth(undefined), 'undefined'); 52 | check(throughBoth(null), 'object'); 53 | check(throughBoth([1,1]), 'object'); 54 | check(throughBoth(()=>1), 'function'); 55 | check(throughBoth([2,2]), Array); 56 | check(throughBoth(new Array(1)), Array); 57 | /** see also typeofs-segfaults.simple.failing */ 58 | -------------------------------------------------------------------------------- /tests/js/test-atob-btoa.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test-atob-btoa.simple 3 | * Simple test which ensures atob/btoa works 4 | * @author Tom Tang 5 | * @date July 2023 6 | */ 7 | 8 | function expect(a) 9 | { 10 | return { 11 | toBe(b) 12 | { 13 | if (a !== b) throw new Error(`'${a}' does not equal to '${b}'`); 14 | } 15 | }; 16 | } 17 | 18 | /* ! 19 | * Modified from https://github.com/oven-sh/bun/blob/bun-v0.6.12/test/js/web/util/atob.test.js 20 | * Bun 21 | * MIT License 22 | */ 23 | 24 | // 25 | // atob 26 | // 27 | expect(atob('YQ==')).toBe('a'); 28 | expect(atob('YWI=')).toBe('ab'); 29 | expect(atob('YWJj')).toBe('abc'); 30 | expect(atob('YWJjZA==')).toBe('abcd'); 31 | expect(atob('YWJjZGU=')).toBe('abcde'); 32 | expect(atob('YWJjZGVm')).toBe('abcdef'); 33 | expect(atob('zzzz')).toBe('Ï<ó'); 34 | expect(atob('')).toBe(''); 35 | expect(atob('null')).toBe('žée'); 36 | expect(atob('6ek=')).toBe('éé'); 37 | // expect(atob("6ek")).toBe("éé"); // FIXME (Tom Tang): binascii.Error: Incorrect padding 38 | expect(atob('gIE=')).toBe('€'); 39 | // expect(atob("zz")).toBe("Ï"); // FIXME (Tom Tang): same here 40 | // expect(atob("zzz")).toBe("Ï<"); 41 | expect(atob('zzz=')).toBe('Ï<'); 42 | expect(atob(' YQ==')).toBe('a'); 43 | expect(atob('YQ==\u000a')).toBe('a'); 44 | 45 | // 46 | // btoa 47 | // 48 | expect(btoa('a')).toBe('YQ=='); 49 | expect(btoa('ab')).toBe('YWI='); 50 | expect(btoa('abc')).toBe('YWJj'); 51 | expect(btoa('abcd')).toBe('YWJjZA=='); 52 | expect(btoa('abcde')).toBe('YWJjZGU='); 53 | expect(btoa('abcdef')).toBe('YWJjZGVm'); 54 | expect(typeof btoa).toBe('function'); 55 | expect(btoa('')).toBe(''); 56 | expect(btoa('null')).toBe('bnVsbA=='); 57 | expect(btoa('undefined')).toBe('dW5kZWZpbmVk'); 58 | expect(btoa('[object Window]')).toBe('W29iamVjdCBXaW5kb3dd'); 59 | expect(btoa('éé')).toBe('6ek='); 60 | expect(btoa('\u0080\u0081')).toBe('gIE='); 61 | -------------------------------------------------------------------------------- /include/PyBytesProxyHandler.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PyBytesProxyHandler.hh 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for creating JS Uint8Array-like proxy objects for immutable bytes objects 5 | * @date 2024-07-23 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_PyBytesProxy_ 12 | #define PythonMonkey_PyBytesProxy_ 13 | 14 | 15 | #include "include/PyObjectProxyHandler.hh" 16 | 17 | 18 | /** 19 | * @brief This struct is the ProxyHandler for JS Proxy Iterable pythonmonkey creates to handle coercion from python iterables to JS Objects 20 | * 21 | */ 22 | struct PyBytesProxyHandler : public PyObjectProxyHandler { 23 | public: 24 | PyBytesProxyHandler() : PyObjectProxyHandler(&family) {}; 25 | static const char family; 26 | 27 | /** 28 | * @brief [[Set]] 29 | * 30 | * @param cx pointer to JSContext 31 | * @param proxy The proxy object who's property we wish to set 32 | * @param id Key of the property we wish to set 33 | * @param v Value that we wish to set the property to 34 | * @param receiver The `this` value to use when executing any code 35 | * @param result whether or not the call succeeded 36 | * @return true call succeed 37 | * @return false call failed and an exception has been raised 38 | */ 39 | bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 40 | JS::HandleValue v, JS::HandleValue receiver, 41 | JS::ObjectOpResult &result) const override; 42 | 43 | bool getOwnPropertyDescriptor( 44 | JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 45 | JS::MutableHandle> desc 46 | ) const override; 47 | 48 | /** 49 | * @brief Handles python object reference count when JS Proxy object is finalized 50 | * 51 | * @param gcx pointer to JS::GCContext 52 | * @param proxy the proxy object being finalized 53 | */ 54 | void finalize(JS::GCContext *gcx, JSObject *proxy) const override; 55 | }; 56 | 57 | #endif -------------------------------------------------------------------------------- /tests/js/quint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file quint.js 3 | * A minimum testing framework with QUnit-like (https://qunitjs.com/) APIs 4 | * @author Tom Tang 5 | * @date Aug 2023 6 | */ 7 | 8 | const QUnitAssert = { 9 | arity(fn, length) 10 | { 11 | if (fn.length !== length) throw new Error(`'${fn}' does not have arity of ${length}`); 12 | }, 13 | isFunction(x) 14 | { 15 | if (typeof x !== 'function') throw new Error(`'${x}' is not a function`); 16 | }, 17 | name(x, name) 18 | { 19 | if (x.name !== name) throw new Error(`'${x}' does not have a name of ${name}`); 20 | }, 21 | true(x) 22 | { 23 | if (x !== true) throw new Error(`'${x}' is not true`); 24 | }, 25 | false(x) 26 | { 27 | if (x !== false) throw new Error(`'${x}' is not false`); 28 | }, 29 | same(a, b) 30 | { 31 | if (a !== b) throw new Error(`'${a}' does not equal to '${b}'`); 32 | }, 33 | arrayEqual(a, b) 34 | { 35 | if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`'${a}' does not equal to '${b}'`); 36 | }, 37 | throws(fn, error) 38 | { 39 | try 40 | { 41 | fn(); 42 | } 43 | catch (err) 44 | { 45 | if (!err.toString().includes(error)) throw new Error(`'${fn}' throws '${err}' but expects '${error}'`); 46 | return; 47 | } 48 | throw new Error(`'${fn}' does not throw`); 49 | }, 50 | looksNative(fn) 51 | { 52 | if (!fn.toString().includes('[native code]')) throw new Error(`'${fn}' does not look native`); 53 | }, 54 | enumerable(obj, propertyName) 55 | { 56 | const descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); 57 | if (!descriptor.enumerable) throw new Error(`'${obj[Symbol.toStringTag]}.${propertyName}' is not enumerable`); 58 | }, 59 | }; 60 | 61 | const QUnit = { 62 | test(name, callback) 63 | { 64 | callback(QUnitAssert); 65 | }, 66 | skip(name, callback) 67 | { 68 | // no op 69 | } 70 | }; 71 | 72 | module.exports = QUnit; 73 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file XMLHttpRequest-internal.d.ts 3 | * @brief TypeScript type declarations for the internal XMLHttpRequest helpers 4 | * @author Tom Tang 5 | * @date August 2023 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | */ 9 | 10 | /** 11 | * `processResponse` callback's argument type 12 | */ 13 | export declare interface XHRResponse { 14 | /** Response URL */ 15 | url: string; 16 | /** HTTP status */ 17 | status: number; 18 | /** HTTP status message */ 19 | statusText: string; 20 | /** The `Content-Type` header value */ 21 | contentLength: number; 22 | /** Implementation of the `xhr.getResponseHeader` method */ 23 | getResponseHeader(name: string): string | undefined; 24 | /** Implementation of the `xhr.getAllResponseHeaders` method */ 25 | getAllResponseHeaders(): string; 26 | /** Implementation of the `xhr.abort` method */ 27 | abort(): void; 28 | } 29 | 30 | /** 31 | * Send request 32 | */ 33 | export declare function request( 34 | method: string, 35 | url: string, 36 | headers: Record, 37 | body: string | Uint8Array, 38 | timeoutMs: number, 39 | // callbacks for request body progress 40 | processRequestBodyChunkLength: (bytesLength: number) => void, 41 | processRequestEndOfBody: () => void, 42 | // callbacks for response progress 43 | processResponse: (response: XHRResponse) => void, 44 | processBodyChunk: (bytes: Uint8Array) => void, 45 | processEndOfBody: () => void, 46 | // callbacks for known exceptions 47 | onTimeoutError: (err: Error) => void, 48 | onNetworkError: (err: Error) => void, 49 | // the debug logging function 50 | /** See `pm.bootstrap.require("debug")` */ 51 | debug: (selector: string) => ((...args: string[]) => void), 52 | ): Promise; 53 | 54 | /** 55 | * Decode data using the codec registered for encoding. 56 | */ 57 | export declare function decodeStr(data: Uint8Array, encoding?: string): string; 58 | -------------------------------------------------------------------------------- /src/PyBaseProxyHandler.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PyBaseProxyHandler.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for creating JS proxy objects 5 | * @date 2023-04-20 6 | * 7 | * @copyright Copyright (c) 2023-2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | 12 | #include "include/PyBaseProxyHandler.hh" 13 | 14 | #include 15 | 16 | #include 17 | 18 | 19 | PyObject *idToKey(JSContext *cx, JS::HandleId id) { 20 | JS::RootedValue idv(cx, js::IdToValue(id)); 21 | JS::RootedString idStr(cx); 22 | if (!id.isSymbol()) { // `JS::ToString` returns `nullptr` for JS symbols 23 | idStr = JS::ToString(cx, idv); 24 | } else { 25 | // TODO (Tom Tang): Revisit this once we have Symbol coercion support 26 | // FIXME (Tom Tang): key collision for symbols without a description string, or pure strings look like "Symbol(xxx)" 27 | idStr = JS_ValueToSource(cx, idv); 28 | } 29 | 30 | // We convert all types of property keys to string 31 | auto chars = JS_EncodeStringToUTF8(cx, idStr); 32 | return PyUnicode_FromString(chars.get()); 33 | } 34 | 35 | bool idToIndex(JSContext *cx, JS::HandleId id, Py_ssize_t *index) { 36 | if (id.isInt()) { // int-like strings have already been automatically converted to ints 37 | *index = id.toInt(); 38 | return true; 39 | } else { 40 | return false; // fail 41 | } 42 | } 43 | 44 | bool PyBaseProxyHandler::getPrototypeIfOrdinary(JSContext *cx, JS::HandleObject proxy, 45 | bool *isOrdinary, 46 | JS::MutableHandleObject protop) const { 47 | // We don't have a custom [[GetPrototypeOf]] 48 | *isOrdinary = true; 49 | protop.set(js::GetStaticPrototype(proxy)); 50 | return true; 51 | } 52 | 53 | bool PyBaseProxyHandler::preventExtensions(JSContext *cx, JS::HandleObject proxy, 54 | JS::ObjectOpResult &result) const { 55 | result.succeed(); 56 | return true; 57 | } 58 | 59 | bool PyBaseProxyHandler::isExtensible(JSContext *cx, JS::HandleObject proxy, 60 | bool *extensible) const { 61 | *extensible = false; 62 | return true; 63 | } -------------------------------------------------------------------------------- /.github/workflows/update-mozcentral-version.yaml: -------------------------------------------------------------------------------- 1 | name: 'Create pull requests to update mozilla-central version to the latest' 2 | 3 | on: 4 | # schedule: 5 | # - cron: "00 14 */100,1-7 * 1" # run on the first Monday of each month at 14:00 UTC (10:00 Eastern Daylight Time) 6 | # See https://blog.healthchecks.io/2022/09/schedule-cron-job-the-funky-way/ 7 | workflow_call: 8 | workflow_dispatch: # or you can run it manually 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | update: 16 | runs-on: ubuntu-20.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Check Version 20 | # Check the latest changes on mozilla-central via the Mercurial pushlog HTTP API 21 | # See https://mozilla-version-control-tools.readthedocs.io/en/latest/hgmo/pushlog.html#hgweb-commands 22 | run: | 23 | COMMIT_HASH=$( 24 | curl -L -s "https://hg.mozilla.org/mozilla-central/json-pushes?tipsonly=1&version=2" |\ 25 | jq --join-output '(.lastpushid | tostring) as $pushid | empty, .pushes[$pushid].changesets[0]' 26 | ) 27 | echo "MOZCENTRAL_VERSION=$COMMIT_HASH" >> $GITHUB_ENV 28 | echo "MOZCENTRAL_VERSION_SHORT=${COMMIT_HASH:0:7}" >> $GITHUB_ENV 29 | - name: Update `mozcentral.version` File 30 | run: echo -n $MOZCENTRAL_VERSION > mozcentral.version 31 | - name: Create Pull Request 32 | uses: peter-evans/create-pull-request@v6 33 | with: 34 | add-paths: mozcentral.version 35 | commit-message: | 36 | chore(deps): upgrade SpiderMonkey to `${{ env.MOZCENTRAL_VERSION }}` 37 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 38 | branch: chore/upgrade-spidermonkey-to-${{ env.MOZCENTRAL_VERSION_SHORT }} 39 | title: Upgrade SpiderMonkey to mozilla-central commit `${{ env.MOZCENTRAL_VERSION }}` 40 | body: | 41 | Changeset: https://hg.mozilla.org/mozilla-central/rev/${{ env.MOZCENTRAL_VERSION }} 42 | labels: dependencies 43 | assignees: Xmader 44 | -------------------------------------------------------------------------------- /cmake/docs/Doxyfile.in: -------------------------------------------------------------------------------- 1 | OUTPUT_DIRECTORY = "@CMAKE_BINARY_DIR@/docs/" 2 | INPUT = "@CMAKE_SOURCE_DIR@/README.md" \ 3 | "@CMAKE_SOURCE_DIR@/src/" \ 4 | "@CMAKE_SOURCE_DIR@/include/" \ 5 | "@CMAKE_SOURCE_DIR@/python/pythonmonkey/" 6 | RECURSIVE = YES 7 | 8 | PROJECT_NAME = "PythonMonkey" 9 | PROJECT_NUMBER = "  v@CMAKE_PROJECT_VERSION@ (dev)" 10 | PROJECT_BRIEF = "" 11 | PROJECT_LOGO = "@CMAKE_CURRENT_SOURCE_DIR@/PythonMonkey-icon.png" 12 | 13 | MARKDOWN_ID_STYLE = GITHUB 14 | USE_MDFILE_AS_MAINPAGE = "@CMAKE_SOURCE_DIR@/README.md" 15 | 16 | EXTRACT_ALL = YES 17 | GENERATE_LATEX = NO 18 | GENERATE_XML = YES 19 | OPTIMIZE_OUTPUT_JAVA = YES 20 | STRIP_FROM_PATH = "@CMAKE_SOURCE_DIR@" 21 | 22 | FILE_PATTERNS = *.cc *.hh *.py *.pyi *.js *.ts *.md 23 | EXTENSION_MAPPING = pyi=python js=javascript ts=javascript 24 | 25 | # Doxygen Theme 26 | # https://jothepro.github.io/doxygen-awesome-css/ 27 | GENERATE_TREEVIEW = YES 28 | DISABLE_INDEX = NO 29 | FULL_SIDEBAR = NO 30 | HTML_HEADER = @CMAKE_CURRENT_SOURCE_DIR@/header.html 31 | HTML_EXTRA_STYLESHEET = @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome.css \ 32 | @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-sidebar-only.css \ 33 | @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-sidebar-only-darkmode-toggle.css 34 | HTML_EXTRA_FILES = @CMAKE_CURRENT_SOURCE_DIR@/favicon.ico \ 35 | @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-darkmode-toggle.js \ 36 | @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-fragment-copy-button.js \ 37 | @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-paragraph-link.js \ 38 | @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-interactive-toc.js 39 | HTML_COLORSTYLE = LIGHT 40 | TIMESTAMP = YES 41 | 42 | # Diagrams with Graphviz 43 | HAVE_DOT = YES 44 | DOT_IMAGE_FORMAT = svg 45 | INTERACTIVE_SVG = YES 46 | -------------------------------------------------------------------------------- /tests/js/xhr-http-keep-alive.bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # @file xhr-http-keep-alive.bash 4 | # For testing HTTP-Keep-Alive automatically in the CI. 5 | # 6 | # We use `strace` to track system calls within the process that open a TCP connection. 7 | # If HTTP-Keep-Alive is working, the total number of TCP connections opened should be 1 for a single remote host. 8 | # 9 | # @author Tom Tang (xmader@distributive.network) 10 | # @date May 2024 11 | 12 | set -u 13 | set -o pipefail 14 | 15 | panic() 16 | { 17 | echo "FAIL: $*" >&2 18 | exit 2 19 | } 20 | 21 | cd `dirname "$0"` || panic "could not change to test directory" 22 | 23 | if [[ "$OSTYPE" != "linux-gnu"* ]]; then 24 | exit 0 25 | # Skip non-Linux for this test 26 | # TODO: add tests on Windows and macOS. What's the equivalence of `strace`? 27 | fi 28 | 29 | code=' 30 | function newRequest(url) { 31 | return new Promise(function (resolve, reject) 32 | { 33 | let xhr = new XMLHttpRequest(); 34 | xhr.open("GET", url); 35 | xhr.onload = function () 36 | { 37 | if (this.status >= 200 && this.status < 300) resolve(this.response); 38 | else reject(new Error(this.status)); 39 | }; 40 | xhr.onerror = (ev) => reject(ev.error); 41 | xhr.send(); 42 | }); 43 | } 44 | 45 | async function main() { 46 | await newRequest("http://www.example.org/"); 47 | await newRequest("http://www.example.org/"); 48 | await newRequest("http://http.badssl.com/"); 49 | } 50 | 51 | main(); 52 | ' 53 | 54 | # Trace the `socket` system call https://man.archlinux.org/man/socket.2 55 | # AF_INET: IPv4, IPPROTO_TCP: TCP connection 56 | TRACE=$(strace -f -e socket \ 57 | "${PMJS:-pmjs}" -e "$code" \ 58 | < /dev/null 2>&1 59 | ) 60 | 61 | # We have 3 HTTP requests, but we should only have 2 TCP connections open, 62 | # as HTTP-Keep-Alive reuses the socket for a single remote host. 63 | echo "$TRACE" \ 64 | | tr -d '\r' \ 65 | | grep -c -E 'socket\(AF_INET, \w*(\|\w*)+, IPPROTO_TCP\)' \ 66 | | while read qty 67 | do 68 | echo "$TRACE" 69 | echo "TCP connections opened: $qty" 70 | [ "$qty" != "2" ] && panic qty should not be $qty 71 | break 72 | done || exit $? 73 | -------------------------------------------------------------------------------- /include/PromiseType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PromiseType.hh 3 | * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing Promises 5 | * @date 2023-03-29 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_PromiseType_ 12 | #define PythonMonkey_PromiseType_ 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | /** 20 | * @brief This struct represents the JS Promise type in Python using our custom pythonmonkey.promise type 21 | */ 22 | struct PromiseType { 23 | public: 24 | /** 25 | * @brief Construct a new PromiseType object from a JS::PromiseObject. 26 | * 27 | * @param cx - javascript context pointer 28 | * @param promise - JS::PromiseObject to be coerced 29 | * 30 | * @returns PyObject* pointer to the resulting PyObject 31 | */ 32 | static PyObject *getPyObject(JSContext *cx, JS::HandleObject promise); 33 | 34 | /** 35 | * @brief Convert a Python [awaitable](https://docs.python.org/3/library/asyncio-task.html#awaitables) object to JS Promise 36 | * 37 | * @param cx - javascript context pointer 38 | * @param pyObject - the python awaitable to be converted 39 | */ 40 | static JSObject *toJsPromise(JSContext *cx, PyObject *pyObject); 41 | }; 42 | 43 | /** 44 | * @brief Check if the object can be used in Python await expression. 45 | * `PyAwaitable_Check` hasn't been and has no plan to be added to the Python C API as of CPython 3.9 46 | */ 47 | bool PythonAwaitable_Check(PyObject *obj); 48 | 49 | /** 50 | * @brief Callback to resolve or reject the JS Promise when the Future is done 51 | * @see https://docs.python.org/3.9/library/asyncio-future.html#asyncio.Future.add_done_callback 52 | * 53 | * @param futureCallbackTuple - tuple( javascript context pointer, rooted JS Promise object ) 54 | * @param args - Args tuple. The callback is called with the Future object as its only argument 55 | */ 56 | static PyObject *futureOnDoneCallback(PyObject *futureCallbackTuple, PyObject *args); 57 | 58 | /** 59 | * @brief Callbacks to settle the Python asyncio.Future once the JS Promise is resolved 60 | */ 61 | static bool onResolvedCb(JSContext *cx, unsigned argc, JS::Value *vp); 62 | 63 | #endif -------------------------------------------------------------------------------- /tests/js/js2py/datetime2.simple: -------------------------------------------------------------------------------- 1 | /** 2 | * @file js2py/datetime.simple.failing 3 | * Test which tests sending js Date() objects into Python get the right types and values 4 | * 5 | * @author Wes Garland, wes@distributive.network 6 | * @date July 2023 7 | */ 8 | 'use strict' 9 | 10 | var fail = false; 11 | function test(result, name) 12 | { 13 | if (result) 14 | console.log('pass -', name); 15 | else 16 | { 17 | console.log('fail -', name); 18 | fail = true; 19 | } 20 | } 21 | 22 | python.exec('import datetime'); 23 | const pyTypeOf = python.eval('(lambda x: str(type(x)))'); 24 | 25 | const ct = pyTypeOf(new Date()); 26 | if (ct.indexOf('datetime.datetime') !== -1) 27 | console.log('type check pass - converted type was', ct); 28 | else 29 | { 30 | fail = true; 31 | console.error('type check fail - converted type was', ct); 32 | } 33 | 34 | python.exec(` 35 | def eq(date1, date2): 36 | print('') 37 | print('compare', date1, date2); 38 | if (date1 == date2): 39 | return True 40 | if (type(date1) != type(date2)): 41 | #print("types do not match") 42 | return False 43 | 44 | diff = date2 - date1 45 | if (diff): 46 | print(f'Dates do not match, difference is {diff}') 47 | return False 48 | 49 | print('warning - dates are different but equal', diff) 50 | return True 51 | 52 | def eqUnixTime(date, timet): 53 | import datetime 54 | global eq 55 | return eq(date, datetime.datetime.fromtimestamp(timet, tz=datetime.timezone.utc)) 56 | `); 57 | 58 | const eq = python.eval('eq'); 59 | const eqUnixTime = python.eval('eqUnixTime'); 60 | 61 | const now = new Date(); 62 | const randomDate = new Date(Date.UTC(1973, 8, 16, 23, 2, 30)); 63 | const startOfEpoch = new Date(Date.UTC(1970, 0, 1, 0, 0, 0)); 64 | 65 | test(eq(now, now), 'same dates (now) are equal'); 66 | test(eq(startOfEpoch, startOfEpoch), 'same dates (0) are equal'); 67 | test(!eq(startOfEpoch, now), 'different dates are not equal 1'); 68 | test(!eq(startOfEpoch, randomDate), 'different dates are not equal 2'); 69 | test(!eq(randomDate, now), 'different dates are not equal 3'); 70 | test(eqUnixTime(startOfEpoch, 0), 'start of epoch'); 71 | 72 | python.exit(fail ? 2 : 0); 73 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | // 4 | // Setup 5 | // 6 | { 7 | "label": "Setup SpiderMonkey", 8 | "type": "process", 9 | "command": "./setup.sh", 10 | }, 11 | // 12 | // Build 13 | // 14 | { 15 | "label": "Build", 16 | "type": "process", 17 | "command": "poetry", 18 | "args": [ 19 | "install", 20 | ], 21 | "problemMatcher": [ 22 | "$gcc" 23 | ], 24 | "group": { 25 | "kind": "build", 26 | "isDefault": true 27 | } 28 | }, 29 | { 30 | "label": "Fast build", 31 | "type": "process", 32 | "command": "poetry", 33 | "args": [ 34 | "run", 35 | "python", 36 | "./build.py", 37 | ], 38 | "problemMatcher": [ 39 | "$gcc" 40 | ], 41 | "options": { 42 | "env": { 43 | "BUILD_TYPE": "Debug" 44 | } 45 | }, 46 | "group": { 47 | "kind": "build", 48 | } 49 | }, 50 | // 51 | // Test 52 | // 53 | { 54 | "label": "Install development dependencies", 55 | "type": "process", 56 | "command": "poetry", 57 | "args": [ 58 | "install", 59 | "--no-root", 60 | "--only=dev" 61 | ], 62 | }, 63 | { 64 | "label": "Run pytest", 65 | "type": "process", 66 | "command": "poetry", 67 | "args": [ 68 | "run", 69 | "pytest", 70 | "./tests/python" 71 | ], 72 | }, 73 | { 74 | "label": "Run JS tests", 75 | "type": "process", 76 | "command": "poetry", 77 | "args": [ 78 | "run", 79 | "bash", 80 | "./peter-jr", 81 | "./tests/js/" 82 | ], 83 | }, 84 | { 85 | "label": "Test", 86 | "dependsOrder": "sequence", 87 | "dependsOn": [ 88 | "Install development dependencies", 89 | "Run pytest", 90 | "Run JS tests", 91 | ], 92 | "group": { 93 | "kind": "test", 94 | "isDefault": true 95 | } 96 | }, 97 | ], 98 | "options": { 99 | "cwd": "${workspaceFolder}" 100 | }, 101 | "problemMatcher": [], 102 | "version": "2.0.0" 103 | } -------------------------------------------------------------------------------- /python/pythonmonkey/helpers.py: -------------------------------------------------------------------------------- 1 | # @file helpers.py - Python->JS helpers for PythonMonkey 2 | # - typeof operator wrapper 3 | # - new operator wrapper 4 | # 5 | # @author Wes Garland, wes@distributive.network 6 | # @date July 2023 7 | # 8 | # @copyright Copyright (c) 2023 Distributive Corp. 9 | 10 | from . import pythonmonkey as pm 11 | evalOpts = {'filename': __file__, 'fromPythonFrame': True} 12 | 13 | 14 | def typeof(jsval): 15 | """ 16 | typeof function - wraps JS typeof operator 17 | """ 18 | return pm.eval("""'use strict'; ( 19 | function pmTypeof(jsval) 20 | { 21 | return typeof jsval; 22 | } 23 | )""", evalOpts)(jsval) 24 | 25 | 26 | def new(ctor): 27 | """ 28 | new function - emits function which wraps JS new operator, emitting a lambda which constructs a new 29 | JS object upon invocation. 30 | """ 31 | if (typeof(ctor) == 'string'): 32 | ctor = pm.eval(ctor) 33 | 34 | newCtor = pm.eval("""'use strict'; ( 35 | function pmNewFactory(ctor) 36 | { 37 | return function newCtor(args) { 38 | args = Array.from(args || []); 39 | return new ctor(...args); 40 | }; 41 | } 42 | )""", evalOpts)(ctor) 43 | return (lambda *args: newCtor(list(args))) 44 | 45 | 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 | 57 | # List which symbols are exposed to the pythonmonkey module. 58 | __all__ = ["new", "typeof", "simpleUncaughtExceptionHandler"] 59 | 60 | # Add the non-enumerable properties of globalThis which don't collide with pythonmonkey.so as exports: 61 | globalThis = pm.eval('globalThis') 62 | pmGlobals = vars(pm) 63 | 64 | exports = pm.eval(""" 65 | Object.getOwnPropertyNames(globalThis) 66 | .filter(prop => Object.keys(globalThis).indexOf(prop) === -1); 67 | """, evalOpts) 68 | 69 | for index in range(0, len(exports)): 70 | name = exports[index] 71 | if (pmGlobals.get(name) is None): 72 | globals().update({name: globalThis[name]}) 73 | __all__.append(name) 74 | -------------------------------------------------------------------------------- /src/DateType.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DateType.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing python dates 5 | * @date 2022-12-21 6 | * 7 | * @copyright Copyright (c) 2022,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/DateType.hh" 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | PyObject *DateType::getPyObject(JSContext *cx, JS::HandleObject dateObj) { 19 | if (!PyDateTimeAPI) { PyDateTime_IMPORT; } // for PyDateTime_FromTimestamp 20 | 21 | JS::Rooted> args(cx); 22 | JS::Rooted year(cx); 23 | JS::Rooted month(cx); 24 | JS::Rooted day(cx); 25 | JS::Rooted hour(cx); 26 | JS::Rooted minute(cx); 27 | JS::Rooted second(cx); 28 | JS::Rooted usecond(cx); 29 | JS_CallFunctionName(cx, dateObj, "getUTCFullYear", args, &year); 30 | JS_CallFunctionName(cx, dateObj, "getUTCMonth", args, &month); 31 | JS_CallFunctionName(cx, dateObj, "getUTCDate", args, &day); 32 | JS_CallFunctionName(cx, dateObj, "getUTCHours", args, &hour); 33 | JS_CallFunctionName(cx, dateObj, "getUTCMinutes", args, &minute); 34 | JS_CallFunctionName(cx, dateObj, "getUTCSeconds", args, &second); 35 | JS_CallFunctionName(cx, dateObj, "getUTCMilliseconds", args, &usecond); 36 | 37 | PyObject *pyObject = PyDateTimeAPI->DateTime_FromDateAndTime( 38 | year.toNumber(), month.toNumber() + 1, day.toNumber(), 39 | hour.toNumber(), minute.toNumber(), second.toNumber(), 40 | usecond.toNumber() * 1000, 41 | PyDateTime_TimeZone_UTC, // Make the resulting Python datetime object timezone-aware 42 | // See https://docs.python.org/3/library/datetime.html#aware-and-naive-objects 43 | PyDateTimeAPI->DateTimeType 44 | ); 45 | Py_INCREF(PyDateTime_TimeZone_UTC); 46 | 47 | return pyObject; 48 | } 49 | 50 | JSObject *DateType::toJsDate(JSContext *cx, PyObject *pyObject) { 51 | // See https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp 52 | PyObject *timestamp = PyObject_CallMethod(pyObject, "timestamp", NULL); // the result is in seconds 53 | double milliseconds = PyFloat_AsDouble(timestamp) * 1000; 54 | Py_DECREF(timestamp); 55 | return JS::NewDateObject(cx, JS::TimeClip(milliseconds)); 56 | } -------------------------------------------------------------------------------- /include/JSFunctionProxy.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSFunctionProxy.hh 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief JSFunctionProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a function would. 5 | * @date 2023-09-28 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_JSFunctionProxy_ 12 | #define PythonMonkey_JSFunctionProxy_ 13 | 14 | #include 15 | 16 | #include 17 | /** 18 | * @brief The typedef for the backing store that will be used by JSFunctionProxy objects. All it contains is a pointer to the JSFunction 19 | * 20 | */ 21 | typedef struct { 22 | PyObject_HEAD 23 | JS::PersistentRootedObject *jsFunc; 24 | } JSFunctionProxy; 25 | 26 | /** 27 | * @brief This struct is a bundle of methods used by the JSFunctionProxy type 28 | * 29 | */ 30 | struct JSFunctionProxyMethodDefinitions { 31 | public: 32 | /** 33 | * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSFunction before freeing the JSFunctionProxy 34 | * 35 | * @param self - The JSFunctionProxy to be free'd 36 | */ 37 | static void JSFunctionProxy_dealloc(JSFunctionProxy *self); 38 | 39 | /** 40 | * @brief New method (.tp_new), creates a new instance of the JSFunctionProxy type, exposed as the __new()__ method in python 41 | * 42 | * @param type - The type of object to be created, will always be JSFunctionProxyType or a derived type 43 | * @param args - arguments to the __new()__ method, not used 44 | * @param kwds - keyword arguments to the __new()__ method, not used 45 | * @return PyObject* - A new instance of JSFunctionProxy 46 | */ 47 | static PyObject *JSFunctionProxy_new(PyTypeObject *type, PyObject *args, PyObject *kwds); 48 | 49 | /** 50 | * @brief Call method (.tp_call), called when the JSFunctionProxy is called 51 | * 52 | * @param self - this callable, might be a free function or a method 53 | * @param args - args to the function 54 | * @param kwargs - keyword args to the function 55 | * @return PyObject* - Result of the function call 56 | */ 57 | static PyObject *JSFunctionProxy_call(PyObject *self, PyObject *args, PyObject *kwargs); 58 | }; 59 | 60 | /** 61 | * @brief Struct for the JSFunctionProxyType, used by all JSFunctionProxy objects 62 | */ 63 | extern PyTypeObject JSFunctionProxyType; 64 | 65 | #endif -------------------------------------------------------------------------------- /src/JSFunctionProxy.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSFunctionProxy.cc 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief JSFunctionProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a function would. 5 | * @date 2023-09-28 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/JSFunctionProxy.hh" 12 | 13 | #include "include/modules/pythonmonkey/pythonmonkey.hh" 14 | #include "include/jsTypeFactory.hh" 15 | #include "include/pyTypeFactory.hh" 16 | #include "include/setSpiderMonkeyException.hh" 17 | 18 | #include 19 | 20 | #include 21 | 22 | void JSFunctionProxyMethodDefinitions::JSFunctionProxy_dealloc(JSFunctionProxy *self) 23 | { 24 | delete self->jsFunc; 25 | } 26 | 27 | PyObject *JSFunctionProxyMethodDefinitions::JSFunctionProxy_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) { 28 | JSFunctionProxy *self = (JSFunctionProxy *)subtype->tp_alloc(subtype, 0); 29 | if (self) { 30 | self->jsFunc = new JS::PersistentRootedObject(GLOBAL_CX); 31 | } 32 | return (PyObject *)self; 33 | } 34 | 35 | PyObject *JSFunctionProxyMethodDefinitions::JSFunctionProxy_call(PyObject *self, PyObject *args, PyObject *kwargs) { 36 | JSContext *cx = GLOBAL_CX; 37 | JS::RootedValue jsFunc(GLOBAL_CX, JS::ObjectValue(**((JSFunctionProxy *)self)->jsFunc)); 38 | JSObject *jsFuncObj = jsFunc.toObjectOrNull(); 39 | JS::RootedObject thisObj(GLOBAL_CX, JS::CurrentGlobalOrNull(GLOBAL_CX)); // if jsFunc is not bound, assume `this` is `globalThis` 40 | 41 | JS::RootedVector jsArgsVector(cx); 42 | Py_ssize_t nargs = PyTuple_Size(args); 43 | for (size_t i = 0; i < nargs; i++) { 44 | JS::Value jsValue = jsTypeFactory(cx, PyTuple_GetItem(args, i)); 45 | if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control 46 | return NULL; // Fail-fast 47 | } 48 | if (!jsArgsVector.append(jsValue)) { 49 | // out of memory 50 | setSpiderMonkeyException(cx); 51 | return NULL; 52 | } 53 | } 54 | 55 | JS::HandleValueArray jsArgs(jsArgsVector); 56 | JS::RootedValue jsReturnVal(cx); 57 | if (!JS_CallFunctionValue(cx, thisObj, jsFunc, jsArgs, &jsReturnVal)) { 58 | setSpiderMonkeyException(cx); 59 | return NULL; 60 | } 61 | 62 | if (PyErr_Occurred()) { 63 | return NULL; 64 | } 65 | 66 | return pyTypeFactory(cx, jsReturnVal); 67 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_pythonmonkey.yaml: -------------------------------------------------------------------------------- 1 | name: File a Bug Report for PythonMonkey 2 | description: Use this template to report PythonMonkey-related issues. Thank you so much for making an issue! 3 | body: 4 | - type: dropdown 5 | id: issue-type 6 | attributes: 7 | label: Issue type 8 | description: What type of issue would you like to report? 9 | multiple: false 10 | options: 11 | - Bug 12 | - Build/Install 13 | - Performance 14 | - Support 15 | - Documentation Bug / Error 16 | - Other 17 | validations: 18 | required: true 19 | 20 | - type: dropdown 21 | id: source 22 | attributes: 23 | label: How did you install PythonMonkey? 24 | options: 25 | - Source 26 | - Installed from pip 27 | - Other (Please specify in additional info) 28 | 29 | - type: input 30 | id: OS 31 | attributes: 32 | label: OS platform and distribution 33 | placeholder: e.g., Linux Ubuntu 22.04 34 | 35 | - type: input 36 | id: Python 37 | attributes: 38 | label: Python version (`python --version`) 39 | placeholder: e.g., 3.9 40 | 41 | - type: input 42 | id: PythonMonkey 43 | attributes: 44 | label: PythonMonkey version (`pip show pythonmonkey`) 45 | placeholder: 0.2.0 or 0.0.1.dev997+1eb883 46 | description: You can also get this with `pmjs --version`. 47 | 48 | - type: textarea 49 | id: what-happened 50 | attributes: 51 | label: Bug Description 52 | description: Please provide a clear and concise description of what the bug is. 53 | 54 | - type: textarea 55 | id: code-to-reproduce 56 | attributes: 57 | label: Standalone code to reproduce the issue 58 | description: Provide a reproducible test case that is the bare minimum necessary to generate the problem. 59 | value: 60 | render: shell 61 | 62 | - type: textarea 63 | id: logs 64 | attributes: 65 | label: Relevant log output or backtrace 66 | description: Please copy and paste any relevant log output. 67 | render: shell 68 | 69 | - type: textarea 70 | id: additional-info 71 | attributes: 72 | label: Additional info if applicable 73 | description: Anything else to add. 74 | render: shell 75 | 76 | - type: input 77 | id: branch 78 | attributes: 79 | label: What branch of PythonMonkey were you developing on? (If applicable) 80 | placeholder: main 81 | -------------------------------------------------------------------------------- /include/JSMethodProxy.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSMethodProxy.hh 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief JSMethodProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a method would, treating `self` as `this`. 5 | * @date 2023-11-14 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_JSMethodProxy_ 12 | #define PythonMonkey_JSMethodProxy_ 13 | 14 | #include "include/JSFunctionProxy.hh" 15 | 16 | #include 17 | 18 | #include 19 | /** 20 | * @brief The typedef for the backing store that will be used by JSMethodProxy objects. All it contains is a pointer to the JSFunction and a pointer to self 21 | * 22 | */ 23 | typedef struct { 24 | PyObject_HEAD 25 | PyObject *self; 26 | JS::PersistentRootedObject *jsFunc; 27 | } JSMethodProxy; 28 | 29 | /** 30 | * @brief This struct is a bundle of methods used by the JSMethodProxy type 31 | * 32 | */ 33 | struct JSMethodProxyMethodDefinitions { 34 | public: 35 | /** 36 | * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSFunction before freeing the JSMethodProxy 37 | * 38 | * @param self - The JSMethodProxy to be free'd 39 | */ 40 | static void JSMethodProxy_dealloc(JSMethodProxy *self); 41 | 42 | /** 43 | * @brief New method (.tp_new), creates a new instance of the JSMethodProxy type, exposed as the __new()__ method in python 44 | * 45 | * @param type - The type of object to be created, will always be JSMethodProxyType or a derived type 46 | * @param args - arguments to the __new()__ method, expected to be a JSFunctionProxy, and an object to bind self to 47 | * @param kwds - keyword arguments to the __new()__ method, not used 48 | * @return PyObject* - A new instance of JSMethodProxy 49 | */ 50 | static PyObject *JSMethodProxy_new(PyTypeObject *type, PyObject *args, PyObject *kwds); 51 | 52 | /** 53 | * @brief Call method (.tp_call), called when the JSMethodProxy is called, properly handling `self` and `this` 54 | * 55 | * @param self - the JSMethodProxy being called 56 | * @param args - args to the method 57 | * @param kwargs - keyword args to the method 58 | * @return PyObject* - Result of the method call 59 | */ 60 | static PyObject *JSMethodProxy_call(PyObject *self, PyObject *args, PyObject *kwargs); 61 | }; 62 | 63 | /** 64 | * @brief Struct for the JSMethodProxyType, used by all JSMethodProxy objects 65 | */ 66 | extern PyTypeObject JSMethodProxyType; 67 | 68 | #endif -------------------------------------------------------------------------------- /src/internalBinding.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file internalBinding.cc 3 | * @author Tom Tang (xmader@distributive.network) 4 | * @brief Create internal bindings to get C++-implemented functions in JS, (imported from NodeJS internal design decisions) 5 | * See function declarations in python/pythonmonkey/builtin_modules/internal-binding.d.ts 6 | * @copyright Copyright (c) 2023 Distributive Corp. 7 | */ 8 | 9 | #include "include/internalBinding.hh" 10 | #include "include/pyTypeFactory.hh" 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | JSObject *createInternalBindingsForNamespace(JSContext *cx, JSFunctionSpec *methodSpecs) { 17 | JS::RootedObject namespaceObj(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); // namespaceObj = Object.create(null) 18 | if (!JS_DefineFunctions(cx, namespaceObj, methodSpecs)) { return nullptr; } 19 | return namespaceObj; 20 | } 21 | 22 | // TODO (Tom Tang): figure out a better way to register InternalBindings to namespace 23 | JSObject *getInternalBindingsByNamespace(JSContext *cx, JSLinearString *namespaceStr) { 24 | if (JS_LinearStringEqualsLiteral(namespaceStr, "utils")) { 25 | return createInternalBindingsForNamespace(cx, InternalBinding::utils); 26 | } else if (JS_LinearStringEqualsLiteral(namespaceStr, "timers")) { 27 | return createInternalBindingsForNamespace(cx, InternalBinding::timers); 28 | } else { // not found 29 | return nullptr; 30 | } 31 | } 32 | 33 | /** 34 | * @brief Implement the `internalBinding(namespace)` function 35 | */ 36 | static bool internalBindingFn(JSContext *cx, unsigned argc, JS::Value *vp) { 37 | JS::CallArgs args = JS::CallArgsFromVp(argc, vp); 38 | 39 | // Get the `namespace` argument as string 40 | JS::HandleValue namespaceStrArg = args.get(0); 41 | JSLinearString *namespaceStr = JS_EnsureLinearString(cx, namespaceStrArg.toString()); 42 | 43 | args.rval().setObjectOrNull(getInternalBindingsByNamespace(cx, namespaceStr)); 44 | return true; 45 | } 46 | 47 | /** 48 | * @brief Create the JS `internalBinding` function 49 | */ 50 | JSFunction *createInternalBinding(JSContext *cx) { 51 | return JS_NewFunction(cx, internalBindingFn, 1, 0, "internalBinding"); 52 | } 53 | 54 | /** 55 | * @brief Convert the `internalBinding(namespace)` function to a Python function 56 | */ 57 | PyObject *getInternalBindingPyFn(JSContext *cx) { 58 | // Create the JS `internalBinding` function 59 | JSObject *jsFn = (JSObject *)createInternalBinding(cx); 60 | 61 | // Convert to a Python function 62 | JS::RootedValue jsFnVal(cx, JS::ObjectValue(*jsFn)); 63 | return pyTypeFactory(cx, jsFnVal); 64 | } -------------------------------------------------------------------------------- /include/JSStringProxy.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSStringProxy.hh 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief JSStringProxy is a custom C-implemented python type that derives from str. It acts as a proxy for JSStrings from Spidermonkey, and behaves like a str would. 5 | * @date 2024-01-03 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_JSStringProxy_ 12 | #define PythonMonkey_JSStringProxy_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | #include 19 | 20 | /** 21 | * @brief The typedef for the backing store that will be used by JSStringProxy objects. All it contains is a pointer to the JSString 22 | * 23 | */ 24 | typedef struct { 25 | PyUnicodeObject str; 26 | JS::PersistentRootedValue *jsString; 27 | } JSStringProxy; 28 | 29 | extern std::unordered_set jsStringProxies; // a collection of all JSStringProxy objects, used during a GCCallback to ensure they continue to point to the correct char buffer 30 | 31 | /** 32 | * @brief This struct is a bundle of methods used by the JSStringProxy type 33 | * 34 | */ 35 | struct JSStringProxyMethodDefinitions { 36 | public: 37 | /** 38 | * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSString before freeing the JSStringProxy 39 | * 40 | * @param self - The JSStringProxy to be free'd 41 | */ 42 | static void JSStringProxy_dealloc(JSStringProxy *self); 43 | 44 | /** 45 | * @brief copy protocol method for both copy and deepcopy 46 | * 47 | * @param self - The JSObjectProxy 48 | * @return a copy of the string 49 | */ 50 | static PyObject *JSStringProxy_copy_method(JSStringProxy *self); 51 | }; 52 | 53 | // docs for methods, copied from cpython 54 | PyDoc_STRVAR(stringproxy_deepcopy__doc__, 55 | "__deepcopy__($self, memo, /)\n" 56 | "--\n" 57 | "\n"); 58 | 59 | PyDoc_STRVAR(stringproxy_copy__doc__, 60 | "__copy__($self, /)\n" 61 | "--\n" 62 | "\n"); 63 | 64 | /** 65 | * @brief Struct for the other methods 66 | * 67 | */ 68 | static PyMethodDef JSStringProxy_methods[] = { 69 | {"__deepcopy__", (PyCFunction)JSStringProxyMethodDefinitions::JSStringProxy_copy_method, METH_O, stringproxy_deepcopy__doc__}, // ignores any memo argument 70 | {"__copy__", (PyCFunction)JSStringProxyMethodDefinitions::JSStringProxy_copy_method, METH_NOARGS, stringproxy_copy__doc__}, 71 | {NULL, NULL} /* sentinel */ 72 | }; 73 | 74 | /** 75 | * @brief Struct for the JSStringProxyType, used by all JSStringProxy objects 76 | */ 77 | extern PyTypeObject JSStringProxyType; 78 | 79 | #endif -------------------------------------------------------------------------------- /src/JSArrayIterProxy.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSArrayIterProxy.cc 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief JSArrayIterProxy is a custom C-implemented python type that derives from list iterator 5 | * @date 2024-01-15 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | 12 | #include "include/JSArrayIterProxy.hh" 13 | 14 | #include "include/JSArrayProxy.hh" 15 | 16 | #include "include/modules/pythonmonkey/pythonmonkey.hh" 17 | 18 | #include "include/pyTypeFactory.hh" 19 | 20 | #include 21 | 22 | #include 23 | 24 | 25 | void JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_dealloc(JSArrayIterProxy *self) 26 | { 27 | PyObject_GC_UnTrack(self); 28 | Py_XDECREF(self->it.it_seq); 29 | PyObject_GC_Del(self); 30 | } 31 | 32 | int JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_traverse(JSArrayIterProxy *self, visitproc visit, void *arg) { 33 | Py_VISIT(self->it.it_seq); 34 | return 0; 35 | } 36 | 37 | int JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_clear(JSArrayIterProxy *self) { 38 | Py_CLEAR(self->it.it_seq); 39 | return 0; 40 | } 41 | 42 | PyObject *JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_iter(JSArrayIterProxy *self) { 43 | Py_INCREF(&self->it); 44 | return (PyObject *)&self->it; 45 | } 46 | 47 | PyObject *JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_next(JSArrayIterProxy *self) { 48 | PyListObject *seq = self->it.it_seq; 49 | if (seq == NULL) { 50 | return NULL; 51 | } 52 | 53 | if (self->it.reversed) { 54 | if (self->it.it_index >= 0) { 55 | JS::RootedValue elementVal(GLOBAL_CX); 56 | JS_GetElement(GLOBAL_CX, *(((JSArrayProxy *)seq)->jsArray), self->it.it_index--, &elementVal); 57 | return pyTypeFactory(GLOBAL_CX, elementVal); 58 | } 59 | } 60 | else { 61 | if (self->it.it_index < JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)seq)) { 62 | JS::RootedValue elementVal(GLOBAL_CX); 63 | JS_GetElement(GLOBAL_CX, *(((JSArrayProxy *)seq)->jsArray), self->it.it_index++, &elementVal); 64 | return pyTypeFactory(GLOBAL_CX, elementVal); 65 | } 66 | } 67 | 68 | self->it.it_seq = NULL; 69 | Py_DECREF(seq); 70 | return NULL; 71 | } 72 | 73 | PyObject *JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_len(JSArrayIterProxy *self) { 74 | Py_ssize_t len; 75 | if (self->it.it_seq) { 76 | len = JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)self->it.it_seq) - self->it.it_index; 77 | if (len >= 0) { 78 | return PyLong_FromSsize_t(len); 79 | } 80 | } 81 | return PyLong_FromLong(0); 82 | } -------------------------------------------------------------------------------- /include/BufferType.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file BufferType.hh 3 | * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for representing ArrayBuffers 5 | * @date 2023-04-27 6 | * 7 | * @copyright Copyright (c) 2023,2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_BufferType_ 12 | #define PythonMonkey_BufferType_ 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | struct BufferType { 20 | public: 21 | /** 22 | * @brief Construct a new BufferType object from a JS TypedArray or ArrayBuffer, as a Python [memoryview](https://docs.python.org/3.9/c-api/memoryview.html) object 23 | * 24 | * @param cx - javascript context pointer 25 | * @param bufObj - JS object to be coerced 26 | * 27 | * @returns PyObject* pointer to the resulting PyObject 28 | */ 29 | static PyObject *getPyObject(JSContext *cx, JS::HandleObject bufObj); 30 | 31 | /** 32 | * @brief Convert a Python object that [provides the buffer interface](https://docs.python.org/3.9/c-api/typeobj.html#buffer-object-structures) to JS TypedArray. 33 | * The subtype (Uint8Array, Float64Array, ...) is automatically determined by the Python buffer's [format](https://docs.python.org/3.9/c-api/buffer.html#c.Py_buffer.format) 34 | * 35 | * @param cx - javascript context pointer 36 | * @param pyObject - the object to be converted 37 | */ 38 | static JSObject *toJsTypedArray(JSContext *cx, PyObject *pyObject); 39 | 40 | /** 41 | * @returns Is the given JS object either a TypedArray or an ArrayBuffer? 42 | */ 43 | static bool isSupportedJsTypes(JSObject *obj); 44 | 45 | protected: 46 | static PyObject *fromJsTypedArray(JSContext *cx, JS::HandleObject typedArray); 47 | static PyObject *fromJsArrayBuffer(JSContext *cx, JS::HandleObject arrayBuffer); 48 | 49 | private: 50 | static void _releasePyBuffer(Py_buffer *bufView); 51 | static void _releasePyBuffer(void *, void *bufView); // JS::BufferContentsFreeFunc callback for JS::NewExternalArrayBuffer 52 | 53 | static JS::Scalar::Type _getPyBufferType(Py_buffer *bufView); 54 | static const char *_toPyBufferFormatCode(JS::Scalar::Type subtype); 55 | 56 | /** 57 | * @brief Create a new typed array using up the given ArrayBuffer or SharedArrayBuffer for storage. 58 | * @see https://hg.mozilla.org/releases/mozilla-esr102/file/a03fde6/js/public/experimental/TypedData.h#l80 59 | * There's no SpiderMonkey API to assign the subtype at execution time 60 | */ 61 | static JSObject *_newTypedArrayWithBuffer(JSContext *cx, JS::Scalar::Type subtype, JS::HandleObject arrayBuffer); 62 | }; 63 | 64 | #endif -------------------------------------------------------------------------------- /src/JSMethodProxy.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSMethodProxy.cc 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief JSMethodProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a method would, treating `self` as `this`. 5 | * @date 2023-11-14 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/JSMethodProxy.hh" 12 | 13 | #include "include/modules/pythonmonkey/pythonmonkey.hh" 14 | #include "include/jsTypeFactory.hh" 15 | #include "include/pyTypeFactory.hh" 16 | #include "include/setSpiderMonkeyException.hh" 17 | 18 | #include 19 | 20 | #include 21 | 22 | void JSMethodProxyMethodDefinitions::JSMethodProxy_dealloc(JSMethodProxy *self) 23 | { 24 | delete self->jsFunc; 25 | return; 26 | } 27 | 28 | PyObject *JSMethodProxyMethodDefinitions::JSMethodProxy_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) { 29 | JSFunctionProxy *jsFunctionProxy; 30 | PyObject *im_self; 31 | 32 | if (!PyArg_ParseTuple(args, "O!O", &JSFunctionProxyType, &jsFunctionProxy, &im_self)) { 33 | return NULL; 34 | } 35 | 36 | JSMethodProxy *self = (JSMethodProxy *)subtype->tp_alloc(subtype, 0); 37 | if (self) { 38 | self->self = im_self; 39 | self->jsFunc = new JS::PersistentRootedObject(GLOBAL_CX); 40 | self->jsFunc->set(*(jsFunctionProxy->jsFunc)); 41 | } 42 | 43 | return (PyObject *)self; 44 | } 45 | 46 | PyObject *JSMethodProxyMethodDefinitions::JSMethodProxy_call(PyObject *self, PyObject *args, PyObject *kwargs) { 47 | JSContext *cx = GLOBAL_CX; 48 | JS::RootedValue jsFunc(GLOBAL_CX, JS::ObjectValue(**((JSMethodProxy *)self)->jsFunc)); 49 | JS::RootedValue selfValue(cx, jsTypeFactory(cx, ((JSMethodProxy *)self)->self)); 50 | JS::RootedObject selfObject(cx); 51 | JS_ValueToObject(cx, selfValue, &selfObject); 52 | 53 | JS::RootedVector jsArgsVector(cx); 54 | for (size_t i = 0; i < PyTuple_Size(args); i++) { 55 | JS::Value jsValue = jsTypeFactory(cx, PyTuple_GetItem(args, i)); 56 | if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control 57 | return NULL; // Fail-fast 58 | } 59 | if (!jsArgsVector.append(jsValue)) { 60 | // out of memory 61 | setSpiderMonkeyException(cx); 62 | return NULL; 63 | } 64 | } 65 | 66 | JS::HandleValueArray jsArgs(jsArgsVector); 67 | JS::RootedValue jsReturnVal(cx); 68 | if (!JS_CallFunctionValue(cx, selfObject, jsFunc, jsArgs, &jsReturnVal)) { 69 | setSpiderMonkeyException(cx); 70 | return NULL; 71 | } 72 | 73 | if (PyErr_Occurred()) { 74 | return NULL; 75 | } 76 | 77 | return pyTypeFactory(cx, jsReturnVal); 78 | } -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/internal-binding.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file internal-binding.d.ts 3 | * @author Tom Tang 4 | * @date June 2023 5 | * 6 | * @copyright Copyright (c) 2023 Distributive Corp. 7 | */ 8 | 9 | /** 10 | * Note: `internalBinding` APIs are generally unsafe as they do not perform argument type checking, etc. 11 | * Argument checking should be done on the JavaScript side. 12 | */ 13 | declare function internalBinding(namespace: string): any; // catch-all 14 | 15 | declare function internalBinding(namespace: "utils"): { 16 | defineGlobal(name: string, value: any): void; 17 | 18 | isAnyArrayBuffer(obj: any): obj is (ArrayBuffer | SharedArrayBuffer); 19 | isPromise(obj: any): obj is Promise; 20 | isRegExp(obj: any): obj is RegExp; 21 | isTypedArray(obj: any): obj is TypedArray; 22 | 23 | /** 24 | * Get the promise state (fulfilled/rejected/pending) and result (either fulfilled resolution or rejection reason) 25 | */ 26 | getPromiseDetails(promise: Promise): [state: PromiseState.Pending] | [state: PromiseState.Fulfilled | PromiseState.Rejected, result: any]; 27 | 28 | /** 29 | * Get the proxy target object and handler 30 | * @return `undefined` if it's not a proxy 31 | */ 32 | getProxyDetails(proxy: T): undefined | [target: T, handler: ProxyHandler]; 33 | }; 34 | 35 | declare type TimerDebugInfo = object; 36 | 37 | declare function internalBinding(namespace: "timers"): { 38 | /** 39 | * internal binding helper for the `setTimeout`/`setInterval` global functions 40 | * 41 | * **UNSAFE**, does not perform argument type checks 42 | * 43 | * @param repeat The call is to `setInterval` if true 44 | * @return timeoutId 45 | */ 46 | enqueueWithDelay(handler: Function, delaySeconds: number, repeat: boolean, debugInfo?: TimerDebugInfo): number; 47 | 48 | /** 49 | * internal binding helper for the `clearTimeout` global function 50 | */ 51 | cancelByTimeoutId(timeoutId: number): void; 52 | 53 | /** 54 | * internal binding helper for if a timer object has been ref'ed 55 | */ 56 | timerHasRef(timeoutId: number): boolean; 57 | 58 | /** 59 | * internal binding helper for ref'ing the timer 60 | */ 61 | timerAddRef(timeoutId: number): void; 62 | 63 | /** 64 | * internal binding helper for unref'ing the timer 65 | */ 66 | timerRemoveRef(timeoutId: number): void; 67 | 68 | /** 69 | * Retrieve debug info inside the timer for the WTFPythonMonkey tool 70 | */ 71 | getDebugInfo(timeoutId: number): TimerDebugInfo; 72 | 73 | /** 74 | * Retrieve the debug info for all timers that are still ref'ed 75 | */ 76 | getAllRefedTimersDebugInfo(): TimerDebugInfo[]; 77 | }; 78 | 79 | export = internalBinding; 80 | -------------------------------------------------------------------------------- /python/pythonmonkey/builtin_modules/dom-exception.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file dom-exception.d.ts 3 | * Type definitions for DOMException 4 | * 5 | * Copied from https://www.npmjs.com/package/@types/web 6 | * Apache License 2.0 7 | */ 8 | 9 | /** 10 | * An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API. 11 | * 12 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) 13 | */ 14 | export interface DOMException extends Error { 15 | /** 16 | * @deprecated 17 | * 18 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) 19 | */ 20 | readonly code: number; 21 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */ 22 | readonly message: string; 23 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */ 24 | readonly name: string; 25 | readonly INDEX_SIZE_ERR: 1; 26 | readonly DOMSTRING_SIZE_ERR: 2; 27 | readonly HIERARCHY_REQUEST_ERR: 3; 28 | readonly WRONG_DOCUMENT_ERR: 4; 29 | readonly INVALID_CHARACTER_ERR: 5; 30 | readonly NO_DATA_ALLOWED_ERR: 6; 31 | readonly NO_MODIFICATION_ALLOWED_ERR: 7; 32 | readonly NOT_FOUND_ERR: 8; 33 | readonly NOT_SUPPORTED_ERR: 9; 34 | readonly INUSE_ATTRIBUTE_ERR: 10; 35 | readonly INVALID_STATE_ERR: 11; 36 | readonly SYNTAX_ERR: 12; 37 | readonly INVALID_MODIFICATION_ERR: 13; 38 | readonly NAMESPACE_ERR: 14; 39 | readonly INVALID_ACCESS_ERR: 15; 40 | readonly VALIDATION_ERR: 16; 41 | readonly TYPE_MISMATCH_ERR: 17; 42 | readonly SECURITY_ERR: 18; 43 | readonly NETWORK_ERR: 19; 44 | readonly ABORT_ERR: 20; 45 | readonly URL_MISMATCH_ERR: 21; 46 | readonly QUOTA_EXCEEDED_ERR: 22; 47 | readonly TIMEOUT_ERR: 23; 48 | readonly INVALID_NODE_TYPE_ERR: 24; 49 | readonly DATA_CLONE_ERR: 25; 50 | } 51 | 52 | export declare var DOMException: { 53 | prototype: DOMException; 54 | new(message?: string, name?: string): DOMException; 55 | readonly INDEX_SIZE_ERR: 1; 56 | readonly DOMSTRING_SIZE_ERR: 2; 57 | readonly HIERARCHY_REQUEST_ERR: 3; 58 | readonly WRONG_DOCUMENT_ERR: 4; 59 | readonly INVALID_CHARACTER_ERR: 5; 60 | readonly NO_DATA_ALLOWED_ERR: 6; 61 | readonly NO_MODIFICATION_ALLOWED_ERR: 7; 62 | readonly NOT_FOUND_ERR: 8; 63 | readonly NOT_SUPPORTED_ERR: 9; 64 | readonly INUSE_ATTRIBUTE_ERR: 10; 65 | readonly INVALID_STATE_ERR: 11; 66 | readonly SYNTAX_ERR: 12; 67 | readonly INVALID_MODIFICATION_ERR: 13; 68 | readonly NAMESPACE_ERR: 14; 69 | readonly INVALID_ACCESS_ERR: 15; 70 | readonly VALIDATION_ERR: 16; 71 | readonly TYPE_MISMATCH_ERR: 17; 72 | readonly SECURITY_ERR: 18; 73 | readonly NETWORK_ERR: 19; 74 | readonly ABORT_ERR: 20; 75 | readonly URL_MISMATCH_ERR: 21; 76 | readonly QUOTA_EXCEEDED_ERR: 22; 77 | readonly TIMEOUT_ERR: 23; 78 | readonly INVALID_NODE_TYPE_ERR: 24; 79 | readonly DATA_CLONE_ERR: 25; 80 | }; 81 | -------------------------------------------------------------------------------- /include/modules/pythonmonkey/pythonmonkey.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file pythonmonkey.hh 3 | * @author Caleb Aikens (caleb@kingsds.network) 4 | * @brief This file defines the pythonmonkey module, along with its various functions. 5 | * @date 2022-09-06 6 | * 7 | * @copyright Copyright (c) 2022-2024 Distributive Corp. 8 | * 9 | */ 10 | #ifndef PythonMonkey_Module_PythonMonkey 11 | #define PythonMonkey_Module_PythonMonkey 12 | 13 | #include "include/JobQueue.hh" 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | 22 | extern JSContext *GLOBAL_CX; /**< pointer to PythonMonkey's JSContext */ 23 | extern JS::PersistentRootedObject jsFunctionRegistry; /** *global; /**< pointer to the global object of PythonMonkey's JSContext */ 25 | static JSAutoRealm *autoRealm; /**< pointer to PythonMonkey's AutoRealm */ 26 | static JobQueue *JOB_QUEUE; /**< pointer to PythonMonkey's event-loop job queue */ 27 | 28 | // Get handle on global object 29 | PyObject *getPythonMonkeyNull(); 30 | PyObject *getPythonMonkeyBigInt(); 31 | 32 | /** 33 | * @brief Destroys the JSContext and deletes associated memory. Called when python quits or faces a fatal exception. 34 | * 35 | */ 36 | static void cleanup(); 37 | 38 | /** 39 | * @brief Function exposed by the python module that calls the spidermonkey garbage collector 40 | * 41 | * @param self - Pointer to the module object 42 | * @param args - Pointer to the python tuple of arguments (not used) 43 | * @return PyObject* - returns python None 44 | */ 45 | static PyObject *collect(PyObject *self, PyObject *args); 46 | 47 | /** 48 | * @brief Function exposed by the python module for evaluating arbitrary JS code 49 | * 50 | * @param self - Pointer to the module object 51 | * @param args - Pointer to the python tuple of arguments (expected to contain JS program as a string as the first element) 52 | * @return PyObject* - The result of evaluating the JS program, coerced to a Python type, returned to the python user 53 | */ 54 | static PyObject *eval(PyObject *self, PyObject *args); 55 | 56 | /** 57 | * @brief Initialization function for the module. Starts the JSContext, creates the global object, and sets cleanup functions 58 | * 59 | * @return PyObject* - The module object to be passed to the python user 60 | */ 61 | PyMODINIT_FUNC PyInit_pythonmonkey(void); 62 | 63 | /** 64 | * @brief Array of method definitions for the pythonmonkey module 65 | * 66 | */ 67 | extern PyMethodDef PythonMonkeyMethods[]; 68 | 69 | /** 70 | * @brief Module definition for the pythonmonkey module 71 | * 72 | */ 73 | extern struct PyModuleDef pythonmonkey; 74 | 75 | /** 76 | * @brief PyObject for spidermonkey error type 77 | * 78 | */ 79 | extern PyObject *SpiderMonkeyError; 80 | #endif -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pythonmonkey" 3 | version = "0" # automatically set by poetry-dynamic-versioning 4 | description = "Seamless interop between Python and JavaScript." 5 | authors = ["Distributive Corp. "] 6 | license = "MIT" 7 | homepage = "https://pythonmonkey.io/" 8 | documentation = "https://docs.pythonmonkey.io/" 9 | repository = "https://github.com/Distributive-Network/PythonMonkey" 10 | readme = "README.md" 11 | packages = [ 12 | { include = "pythonmonkey", from = "python" }, 13 | ] 14 | include = [ 15 | # Linux and macOS 16 | { path = "python/pythonmonkey/pythonmonkey.so", format = ["sdist", "wheel"] }, 17 | { path = "python/pythonmonkey/libmozjs*", format = ["sdist", "wheel"] }, 18 | 19 | # Windows 20 | { path = "python/pythonmonkey/pythonmonkey.pyd", format = ["sdist", "wheel"] }, 21 | { path = "python/pythonmonkey/mozjs-*.dll", format = ["sdist", "wheel"] }, 22 | 23 | # include all files for source distribution 24 | { path = "src", format = "sdist" }, 25 | { path = "include", format = "sdist" }, 26 | { path = "cmake", format = "sdist" }, 27 | { path = "tests", format = "sdist" }, 28 | { path = "CMakeLists.txt", format = "sdist" }, 29 | { path = "*.sh", format = "sdist" }, 30 | { path = "mozcentral.version", format = "sdist" }, 31 | 32 | # Add marker file for pep561 33 | { path = "python/pythonmonkey/py.typed", format = ["sdist", "wheel"] }, 34 | ] 35 | 36 | 37 | [tool.poetry.dependencies] 38 | python = "^3.8" 39 | pyreadline3 = { version = "^3.4.1", platform = "win32" } 40 | aiohttp = { version = "^3.9.5", extras = ["speedups"] } 41 | pminit = { version = ">=0.4.0", allow-prereleases = true } 42 | 43 | 44 | [tool.poetry-dynamic-versioning] 45 | enable = true 46 | vcs = "git" 47 | style = "pep440" 48 | bump = true 49 | 50 | [tool.poetry-dynamic-versioning.substitution] 51 | # Set the project version number in CMakeLists.txt 52 | files = [ "CMakeLists.txt" ] 53 | patterns = [ '(set\(PYTHONMONKEY_VERSION ")0("\))' ] 54 | 55 | 56 | [tool.poetry.build] 57 | script = "build.py" 58 | generate-setup-file = false 59 | 60 | [tool.poetry.scripts] 61 | pmjs = "pythonmonkey.cli.pmjs:main" 62 | 63 | 64 | [tool.poetry.group.dev.dependencies] 65 | pytest = "^7.3.1" 66 | pip = "^23.1.2" 67 | numpy = [ 68 | {version = "^2.3.0", python = ">=3.11"}, 69 | {version = "^2.1.0", python = ">=3.10,<3.11"}, # NumPy 2.3.0 drops support for Python 3.10 70 | {version = "^2.0.1", python = ">=3.9,<3.10"}, # NumPy 2.1.0 drops support for Python 3.9 71 | {version = "^1.24.3", python = ">=3.8,<3.9"}, # NumPy 1.25.0 drops support for Python 3.8 72 | ] 73 | pminit = { path = "./python/pminit", develop = true } 74 | 75 | 76 | [build-system] 77 | requires = ["poetry-core>=1.1.1", "poetry-dynamic-versioning==1.1.1"] 78 | build-backend = "poetry_dynamic_versioning.backend" 79 | 80 | [tool.autopep8] 81 | max_line_length=120 82 | ignore="E111,E114,E121" # allow 2-space indents 83 | verbose=true 84 | indent-size=2 85 | aggressive=3 86 | exit-code=true -------------------------------------------------------------------------------- /include/jsTypeFactory.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file jsTypeFactory.hh 3 | * @author Caleb Aikens (caleb@distributive.network) 4 | * @brief 5 | * @date 2023-02-15 6 | * 7 | * @copyright Copyright (c) 2023 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_JsTypeFactory_ 12 | #define PythonMonkey_JsTypeFactory_ 13 | 14 | #include 15 | 16 | #include 17 | 18 | 19 | struct PythonExternalString : public JSExternalStringCallbacks { 20 | public: 21 | /** 22 | * @brief Get the PyObject using the given char buffer 23 | * 24 | * @param chars - the char buffer of the PyObject 25 | * @return PyObject* - the PyObject string 26 | */ 27 | static PyObject *getPyString(const char16_t *chars); 28 | static PyObject *getPyString(const JS::Latin1Char *chars); 29 | 30 | /** 31 | * @brief decrefs the underlying PyObject string when the JSString is finalized 32 | * 33 | * @param chars - The char buffer of the string 34 | */ 35 | void finalize(char16_t *chars) const override; 36 | void finalize(JS::Latin1Char *chars) const override; 37 | 38 | size_t sizeOfBuffer(const char16_t *chars, mozilla::MallocSizeOf mallocSizeOf) const override; 39 | size_t sizeOfBuffer(const JS::Latin1Char *chars, mozilla::MallocSizeOf mallocSizeOf) const override; 40 | }; 41 | extern PythonExternalString PythonExternalStringCallbacks; 42 | 43 | /** 44 | * @brief Function that makes a UTF16-encoded copy of a UCS4 string 45 | * 46 | * @param chars - pointer to the UCS4-encoded string 47 | * @param length - length of chars in code points 48 | * @param outStr - UTF16-encoded out-parameter string 49 | * @return size_t - length of outStr (counting surrogate pairs as 2) 50 | */ 51 | size_t UCS4ToUTF16(const uint32_t *chars, size_t length, uint16_t *outStr); 52 | 53 | /** 54 | * @brief Function that takes a PyObject and returns a corresponding JS::Value, doing shared memory management when necessary 55 | * 56 | * @param cx - Pointer to the JSContext 57 | * @param object - Pointer to the PyObject who's type and value we wish to encapsulate 58 | * @return JS::Value - A JS::Value corresponding to the PyType 59 | */ 60 | JS::Value jsTypeFactory(JSContext *cx, PyObject *object); 61 | /** 62 | * @brief same to jsTypeFactory, but it's guaranteed that no error would be set on the Python error stack, instead 63 | * return JS `null` on error, and output a warning in Python-land 64 | */ 65 | JS::Value jsTypeFactorySafe(JSContext *cx, PyObject *object); 66 | 67 | /** 68 | * @brief Helper function for jsTypeFactory to create a JSFunction* through JS_NewFunction that knows how to call a python function. 69 | * 70 | * @param cx - Pointer to the JSContext 71 | * @param argc - The number of arguments the JSFunction expects 72 | * @param vp - The return value of the JSFunction 73 | * @return true - Function executed successfully 74 | * @return false - Function did not execute successfully and an exception has been set 75 | */ 76 | bool callPyFunc(JSContext *cx, unsigned int argc, JS::Value *vp); 77 | #endif -------------------------------------------------------------------------------- /tests/python/test_xhr.py: -------------------------------------------------------------------------------- 1 | from http.server import HTTPServer, BaseHTTPRequestHandler 2 | import pythonmonkey as pm 3 | import threading 4 | import asyncio 5 | import json 6 | 7 | def test_xhr(): 8 | class TestHTTPRequestHandler(BaseHTTPRequestHandler): 9 | def log_request(self, *args) -> None: 10 | return 11 | 12 | def do_GET(self): 13 | self.send_response(200) 14 | self.end_headers() 15 | self.wfile.write(b"get response") 16 | 17 | def do_POST(self): 18 | self.send_response(200) 19 | self.send_header('Content-Type', 'application/json') 20 | self.end_headers() 21 | length = int(self.headers.get('Content-Length')) 22 | json_string = self.rfile.read(length).decode("utf-8") 23 | parameter_dict = json.loads(json_string) 24 | parameter_dict["User-Agent"] = self.headers['User-Agent'] 25 | data = json.dumps(parameter_dict).encode("utf-8") 26 | self.wfile.write(data) 27 | 28 | httpd = HTTPServer(('localhost', 4001), TestHTTPRequestHandler) 29 | thread = threading.Thread(target = httpd.serve_forever) 30 | thread.daemon = True 31 | thread.start() 32 | 33 | async def async_fn(): 34 | assert "get response" == await pm.eval(""" 35 | new Promise(function (resolve, reject) { 36 | let xhr = new XMLHttpRequest(); 37 | xhr.open('GET', 'http://localhost:4001'); 38 | 39 | xhr.onload = function () 40 | { 41 | if (this.status >= 200 && this.status < 300) 42 | { 43 | resolve(this.response); 44 | } 45 | else 46 | { 47 | reject(new Error(JSON.stringify({ 48 | status: this.status, 49 | statusText: this.statusText 50 | }))); 51 | } 52 | }; 53 | 54 | xhr.onerror = function (ev) 55 | { 56 | reject(ev.error); 57 | }; 58 | xhr.send(); 59 | }); 60 | """) 61 | 62 | post_result = await pm.eval(""" 63 | new Promise(function (resolve, reject) 64 | { 65 | let xhr = new XMLHttpRequest(); 66 | xhr.open('POST', 'http://localhost:4001'); 67 | 68 | xhr.onload = function () 69 | { 70 | if (this.status >= 200 && this.status < 300) 71 | { 72 | resolve(this.response); 73 | } 74 | else 75 | { 76 | reject(new Error(JSON.stringify({ 77 | status: this.status, 78 | statusText: this.statusText 79 | }))); 80 | } 81 | }; 82 | 83 | xhr.onerror = function (ev) 84 | { 85 | console.log(ev) 86 | reject(ev.error); 87 | }; 88 | 89 | xhr.send(JSON.stringify({fromPM: "snakesandmonkeys"})); 90 | }) 91 | """) 92 | 93 | result_json = json.loads(post_result) 94 | assert result_json["fromPM"] == "snakesandmonkeys" 95 | assert result_json["User-Agent"].startswith("Python/") 96 | httpd.shutdown() 97 | asyncio.run(async_fn()) -------------------------------------------------------------------------------- /python/pythonmonkey/lib/pmjs/global-init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file global-init.js 3 | * Set up global scope which is used to run either program or REPL code in the pmjs script 4 | * runner. 5 | * 6 | * @author Wes Garland, wes@distributive.network 7 | * @date June 2023 8 | * 9 | * @copyright Copyright (c) 2023 Distributive Corp. 10 | */ 11 | 'use strict'; 12 | 13 | /* Anything loaded with require() before the program started was a side effect and not part of the 14 | * program. This means that by now, whoever needed the resources should have memoized them someplace 15 | * safe, and we can remove them to keep the namespace clean. 16 | */ 17 | for (let mid in require.cache) 18 | delete require.cache[mid]; 19 | 20 | /* Recreate the python object as an EventEmitter */ 21 | const { EventEmitter } = require('events'); 22 | const originalPython = globalThis.python; 23 | const python = globalThis.python = new EventEmitter('python'); 24 | Object.assign(python, originalPython); 25 | 26 | /* Emulate node's process.on('error') behaviour with python.on('error'). */ 27 | python.on('error', function unhandledError(error) 28 | { 29 | if (python.listenerCount('error') > 1) 30 | return; 31 | if (python.listenerCount('error') === 0 || python.listeners('error')[0] === unhandledError) 32 | python.emit('unhandledException', error); 33 | }); 34 | 35 | /** 36 | * runProgramModule wants to include the require.cache from the pre-program loads (e.g. via -r or -e), but 37 | * due to current bugs in PythonMonkey, we can't access the cache property of require because it is a JS 38 | * function wrapped in a Python function wrapper exposed to script as a native function. 39 | * 40 | * This patch swaps in a descended version of require(), which has the same require.cache, but that has 41 | * side effects in terms of local module id resolution, so this patch happens only right before we want 42 | * to fire up the program module. 43 | */ 44 | exports.patchGlobalRequire = function pmjs$$patchGlobalRequire() 45 | { 46 | globalThis.require = require; 47 | }; 48 | 49 | exports.initReplLibs = function pmjs$$initReplLibs() 50 | { 51 | globalThis.util = require('util'); 52 | globalThis.events = require('events'); 53 | }; 54 | 55 | /** 56 | * Temporary API until we get EventEmitters working. Replace this export with a custom handler. 57 | */ 58 | exports.uncaughtExceptionHandler = function globalInit$$uncaughtExceptionHandler(error) 59 | { 60 | if (python._events && python._events['uncaughtException']) 61 | python.emit('uncaughtException', error); 62 | else 63 | { 64 | console.error('Uncaught', error); 65 | python.exit(1); 66 | } 67 | }; 68 | 69 | /** 70 | * Temporary API until we get EventEmitters working. Replace this export with a custom handler. 71 | */ 72 | exports.unhandledRejectionHandler = function globalInit$$unhandledRejectionHandler(error) 73 | { 74 | if (python._events && python._events['uncaughtRejection']) 75 | python.emit('unhandledRejection', error); 76 | else 77 | { 78 | console.error(error); 79 | python.exit(1); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /python/pythonmonkey/global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file global.d.ts 3 | * @author Tom Tang 4 | * @date May 2023 5 | * 6 | * @copyright Copyright (c) 2023 Distributive Corp. 7 | */ 8 | 9 | declare const python: { 10 | pythonMonkey: { 11 | /** root directory of the pythonmonkey package */ 12 | dir: string; 13 | }; 14 | /** Python `print` */ 15 | print(...values: any): void; 16 | /** Python `eval` */ 17 | eval(code: string, globals?: Record, locals?: Record): any; 18 | /** Python `exec` */ 19 | exec(code: string, globals?: Record, locals?: Record): void; 20 | /** Python `sys.stdout`. */ 21 | stdout: { 22 | /** Write the given string to stdout. */ 23 | write(s: string): number; 24 | read(n: number): string; 25 | }; 26 | /** Python `sys.stderr`. */ 27 | stderr: { 28 | /** Write the given string to stderr. */ 29 | write(s: string): number; 30 | read(n: number): string; 31 | }; 32 | /** Python `os.getenv`. Get an environment variable, return undefined if it doesn't exist. */ 33 | getenv(key: string): string | undefined; 34 | /** Python `exit`. Exit the program. */ 35 | exit(exitCode: number): never; 36 | /** Loads a python module using importlib, prefills it with an exports object and returns the module. */ 37 | load(filename: string): object; 38 | /** Python `sys.path` */ 39 | paths: string[]; 40 | }; 41 | 42 | declare var __filename: string; 43 | declare var __dirname: string; 44 | 45 | /** see `pm.eval` */ 46 | declare function pmEval(code: string): any; 47 | 48 | // Expose our own `console` as a property of the global object 49 | // XXX: ↓↓↓ we must use "var" here 50 | declare var console: import("console").Console; 51 | 52 | // Expose `atob`/`btoa` as properties of the global object 53 | declare var atob: typeof import("base64").atob; 54 | declare var btoa: typeof import("base64").btoa; 55 | 56 | // Expose `setTimeout`/`clearTimeout` APIs 57 | declare var setTimeout: typeof import("timers").setTimeout; 58 | declare var clearTimeout: typeof import("timers").clearTimeout; 59 | // Expose `setInterval`/`clearInterval` APIs 60 | declare var setInterval: typeof import("timers").setInterval; 61 | declare var clearInterval: typeof import("timers").clearInterval; 62 | 63 | // Expose `URL`/`URLSearchParams` APIs 64 | declare var URL: typeof import("url").URL; 65 | declare var URLSearchParams: typeof import("url").URLSearchParams; 66 | 67 | // Expose `XMLHttpRequest` (XHR) API 68 | declare var XMLHttpRequest: typeof import("XMLHttpRequest").XMLHttpRequest; 69 | 70 | // Keep this in sync with both https://hg.mozilla.org/releases/mozilla-esr102/file/a03fde6/js/public/Promise.h#l331 71 | // and https://github.com/nodejs/node/blob/v20.2.0/deps/v8/include/v8-promise.h#L30 72 | declare enum PromiseState { Pending = 0, Fulfilled = 1, Rejected = 2 } 73 | 74 | declare type TypedArray = 75 | | Uint8Array 76 | | Uint8ClampedArray 77 | | Uint16Array 78 | | Uint32Array 79 | | Int8Array 80 | | Int16Array 81 | | Int32Array 82 | | Float32Array 83 | | Float64Array 84 | | BigUint64Array 85 | | BigInt64Array; 86 | -------------------------------------------------------------------------------- /include/JSArrayIterProxy.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSArrayIterProxy.hh 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief JSArrayIterProxy is a custom C-implemented python type that derives from PyListIter 5 | * @version 0.1 6 | * @date 2024-01-15 7 | * 8 | * @copyright Copyright (c) 2024 Distributive Corp. 9 | * 10 | */ 11 | 12 | #ifndef PythonMonkey_JSArrayIterProxy_ 13 | #define PythonMonkey_JSArrayIterProxy_ 14 | 15 | 16 | #include 17 | 18 | #include 19 | 20 | 21 | // redeclare hidden type 22 | typedef struct { 23 | PyObject_HEAD 24 | int it_index; 25 | bool reversed; 26 | PyListObject *it_seq; /* Set to NULL when iterator is exhausted */ 27 | } PyListIterObject; 28 | 29 | /** 30 | * @brief The typedef for the backing store that will be used by JSArrayIterProxy objects. 31 | * 32 | */ 33 | typedef struct { 34 | PyListIterObject it; 35 | } JSArrayIterProxy; 36 | 37 | /** 38 | * @brief This struct is a bundle of methods used by the JSArrayProxy type 39 | * 40 | */ 41 | struct JSArrayIterProxyMethodDefinitions { 42 | public: 43 | /** 44 | * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSArrayProxy 45 | * 46 | * @param self - The JSArrayIterProxy to be free'd 47 | */ 48 | static void JSArrayIterProxy_dealloc(JSArrayIterProxy *self); 49 | 50 | /** 51 | * @brief .tp_traverse method 52 | * 53 | * @param self - The JSArrayIterProxy 54 | * @param visit - The function to be applied on each element of the list 55 | * @param arg - The argument to the visit function 56 | * @return 0 on success 57 | */ 58 | static int JSArrayIterProxy_traverse(JSArrayIterProxy *self, visitproc visit, void *arg); 59 | 60 | /** 61 | * @brief .tp_clear method 62 | * 63 | * @param self - The JSArrayIterProxy 64 | * @return 0 on success 65 | */ 66 | static int JSArrayIterProxy_clear(JSArrayIterProxy *self); 67 | 68 | /** 69 | * @brief .tp_iter method 70 | * 71 | * @param self - The JSArrayIterProxy 72 | * @return PyObject* - an interator over the iterator 73 | */ 74 | static PyObject *JSArrayIterProxy_iter(JSArrayIterProxy *self); 75 | 76 | /** 77 | * @brief .tp_next method 78 | * 79 | * @param self - The JSArrayIterProxy 80 | * @return PyObject* - next object in iteration 81 | */ 82 | static PyObject *JSArrayIterProxy_next(JSArrayIterProxy *self); 83 | 84 | /** 85 | * @brief length method 86 | * 87 | * @param self - The JSArrayIterProxy 88 | * @return PyObject* - number of objects left to iterate over in iteration 89 | */ 90 | static PyObject *JSArrayIterProxy_len(JSArrayIterProxy *self); 91 | }; 92 | 93 | 94 | PyDoc_STRVAR(length_hint_doc, "Private method returning an estimate of len(list(it))."); 95 | 96 | /** 97 | * @brief Struct for the other methods 98 | * 99 | */ 100 | static PyMethodDef JSArrayIterProxy_methods[] = { 101 | {"__length_hint__", (PyCFunction)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_len, METH_NOARGS, length_hint_doc}, 102 | {NULL, NULL} /* sentinel */ 103 | }; 104 | 105 | /** 106 | * @brief Struct for the JSArrayProxyType, used by all JSArrayProxy objects 107 | */ 108 | extern PyTypeObject JSArrayIterProxyType; 109 | 110 | #endif -------------------------------------------------------------------------------- /include/JSObjectIterProxy.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSObjectIterProxy.hh 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief JSObjectIterProxy is a custom C-implemented python type that derives from PyDictIterKey 5 | * @date 2024-01-17 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #ifndef PythonMonkey_JSObjectIterProxy_ 12 | #define PythonMonkey_JSObjectIterProxy_ 13 | 14 | 15 | #include 16 | 17 | #include 18 | 19 | #define KIND_KEYS 0 20 | #define KIND_VALUES 1 21 | #define KIND_ITEMS 2 22 | 23 | 24 | /** 25 | * @brief The typedef for the backing store that will be used by JSObjectIterProxy objects. 26 | * 27 | */ 28 | 29 | typedef struct { 30 | PyObject_HEAD 31 | JS::PersistentRootedIdVector *props; 32 | int it_index; 33 | bool reversed; 34 | int kind; 35 | PyDictObject *di_dict; /* Set to NULL when iterator is exhausted */ 36 | } dictiterobject; 37 | 38 | 39 | typedef struct { 40 | dictiterobject it; 41 | } JSObjectIterProxy; 42 | 43 | /** 44 | * @brief This struct is a bundle of methods used by the JSArrayProxy type 45 | * 46 | */ 47 | struct JSObjectIterProxyMethodDefinitions { 48 | public: 49 | /** 50 | * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSArrayProxy 51 | * 52 | * @param self - The JSObjectIterProxy to be free'd 53 | */ 54 | static void JSObjectIterProxy_dealloc(JSObjectIterProxy *self); 55 | 56 | /** 57 | * @brief .tp_traverse method 58 | * 59 | * @param self - The JSObjectIterProxy 60 | * @param visit - The function to be applied on each element of the list 61 | * @param arg - The argument to the visit function 62 | * @return 0 on success 63 | */ 64 | static int JSObjectIterProxy_traverse(JSObjectIterProxy *self, visitproc visit, void *arg); 65 | 66 | /** 67 | * @brief .tp_clear method 68 | * 69 | * @param self - The JSObjectIterProxy 70 | * @return 0 on success 71 | */ 72 | static int JSObjectIterProxy_clear(JSObjectIterProxy *self); 73 | 74 | /** 75 | * @brief .tp_iter method 76 | * 77 | * @param self - The JSObjectIterProxy 78 | * @return PyObject* - an interator over the iterator 79 | */ 80 | static PyObject *JSObjectIterProxy_iter(JSObjectIterProxy *self); 81 | 82 | /** 83 | * @brief .tp_next method 84 | * 85 | * @param self - The JSObjectIterProxy 86 | * @return PyObject* - next object in iteration 87 | */ 88 | static PyObject *JSObjectIterProxy_nextkey(JSObjectIterProxy *self); 89 | 90 | /** 91 | * @brief length method 92 | * 93 | * @param self - The JSObjectIterProxy 94 | * @return PyObject* - number of objects left to iterate over in iteration 95 | */ 96 | static PyObject *JSObjectIterProxy_len(JSObjectIterProxy *self); 97 | }; 98 | 99 | 100 | PyDoc_STRVAR(dict_length_hint_doc, "Private method returning an estimate of len(list(it))."); 101 | 102 | /** 103 | * @brief Struct for the other methods 104 | * 105 | */ 106 | static PyMethodDef JSObjectIterProxy_methods[] = { 107 | {"__length_hint__", (PyCFunction)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_len, METH_NOARGS, dict_length_hint_doc}, 108 | {NULL, NULL} /* sentinel */ 109 | }; 110 | 111 | /** 112 | * @brief Struct for the JSArrayProxyType, used by all JSArrayProxy objects 113 | */ 114 | extern PyTypeObject JSObjectIterProxyType; 115 | 116 | #endif -------------------------------------------------------------------------------- /tests/python/test_functions.py: -------------------------------------------------------------------------------- 1 | import pythonmonkey as pm 2 | 3 | 4 | def test_func_no_args(): 5 | def f(): 6 | return 42 7 | assert 42 == pm.eval("(f) => f()")(f) 8 | 9 | 10 | def test_func_too_many_args(): 11 | def f(a, b): 12 | return [a, b] 13 | assert [1, 2] == pm.eval("(f) => f(1, 2, 3)")(f) 14 | 15 | 16 | def test_func_equal_args(): 17 | def f(a, b): 18 | return [a, b] 19 | assert [1, 2] == pm.eval("(f) => f(1, 2)")(f) 20 | 21 | 22 | def test_func_too_few_args(): 23 | def f(a, b): 24 | return [a, b] 25 | assert [1, None] == pm.eval("(f) => f(1)")(f) 26 | 27 | 28 | def test_default_func_no_args(): 29 | def f(a, b, c=42, d=43): 30 | return [a, b, c, d] 31 | assert [None, None, 42, 43] == pm.eval("(f) => f()")(f) 32 | 33 | 34 | def test_default_func_too_many_default_args(): 35 | def f(a, b, c=42, d=43): 36 | return [a, b, c, d] 37 | assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4, 5)")(f) 38 | 39 | 40 | def test_default_func_equal_default_args(): 41 | def f(a, b, c=42, d=43): 42 | return [a, b, c, d] 43 | assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4)")(f) 44 | 45 | 46 | def test_default_func_too_few_default_args(): 47 | def f(a, b, c=42, d=43): 48 | return [a, b, c, d] 49 | assert [1, 2, 3, 43] == pm.eval("(f) => f(1, 2, 3)")(f) 50 | 51 | 52 | def test_default_func_equal_args(): 53 | def f(a, b, c=42, d=43): 54 | return [a, b, c, d] 55 | assert [1, 2, 42, 43] == pm.eval("(f) => f(1, 2)")(f) 56 | 57 | 58 | def test_default_func_too_few_args(): 59 | def f(a, b, c=42, d=43): 60 | return [a, b, c, d] 61 | assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f) 62 | 63 | 64 | def test_vararg_func_no_args(): 65 | def f(a, b, *args): 66 | return [a, b, *args] 67 | assert [None, None] == pm.eval("(f) => f()")(f) 68 | 69 | 70 | def test_vararg_func_too_many_args(): 71 | def f(a, b, *args): 72 | return [a, b, *args] 73 | assert [1, 2, 3] == pm.eval("(f) => f(1, 2, 3)")(f) 74 | 75 | 76 | def test_vararg_func_equal_args(): 77 | def f(a, b, *args): 78 | return [a, b, *args] 79 | assert [1, 2] == pm.eval("(f) => f(1, 2)")(f) 80 | 81 | 82 | def test_vararg_func_too_few_args(): 83 | def f(a, b, *args): 84 | return [a, b, *args] 85 | assert [1, None] == pm.eval("(f) => f(1)")(f) 86 | 87 | 88 | def test_default_vararg_func_no_args(): 89 | def f(a, b, c=42, d=43, *args): 90 | return [a, b, c, d, *args] 91 | assert [None, None, 42, 43] == pm.eval("(f) => f()")(f) 92 | 93 | 94 | def test_default_vararg_func_too_many_default_args(): 95 | def f(a, b, c=42, d=43, *args): 96 | return [a, b, c, d, *args] 97 | assert [1, 2, 3, 4, 5] == pm.eval("(f) => f(1, 2, 3, 4, 5)")(f) 98 | 99 | 100 | def test_default_vararg_func_equal_default_args(): 101 | def f(a, b, c=42, d=43, *args): 102 | return [a, b, c, d, *args] 103 | assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4)")(f) 104 | 105 | 106 | def test_default_vararg_func_too_few_default_args(): 107 | def f(a, b, c=42, d=43, *args): 108 | return [a, b, c, d, *args] 109 | assert [1, 2, 3, 43] == pm.eval("(f) => f(1, 2, 3)")(f) 110 | 111 | 112 | def test_default_vararg_func_equal_args(): 113 | def f(a, b, c=42, d=43, *args): 114 | return [a, b, c, d, *args] 115 | assert [1, 2, 42, 43] == pm.eval("(f) => f(1, 2)")(f) 116 | 117 | 118 | def test_default_vararg_func_too_few_args(): 119 | def f(a, b, c=42, d=43, *args): 120 | return [a, b, c, d, *args] 121 | assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f) 122 | -------------------------------------------------------------------------------- /src/JSObjectIterProxy.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSObjectIterProxy.cc 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief JSObjectIterProxy is a custom C-implemented python type that derives from list iterator 5 | * @date 2024-01-17 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | 12 | #include "include/JSObjectIterProxy.hh" 13 | 14 | #include "include/JSObjectProxy.hh" 15 | 16 | #include "include/modules/pythonmonkey/pythonmonkey.hh" 17 | 18 | #include "include/pyTypeFactory.hh" 19 | 20 | #include "include/PyDictProxyHandler.hh" 21 | 22 | #include 23 | 24 | #include 25 | 26 | #include 27 | 28 | 29 | void JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_dealloc(JSObjectIterProxy *self) 30 | { 31 | delete self->it.props; 32 | PyObject_GC_UnTrack(self); 33 | Py_XDECREF(self->it.di_dict); 34 | PyObject_GC_Del(self); 35 | } 36 | 37 | int JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_traverse(JSObjectIterProxy *self, visitproc visit, void *arg) { 38 | Py_VISIT(self->it.di_dict); 39 | return 0; 40 | } 41 | 42 | int JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_clear(JSObjectIterProxy *self) { 43 | Py_CLEAR(self->it.di_dict); 44 | return 0; 45 | } 46 | 47 | PyObject *JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_iter(JSObjectIterProxy *self) { 48 | Py_INCREF(&self->it); 49 | return (PyObject *)&self->it; 50 | } 51 | 52 | PyObject *JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_nextkey(JSObjectIterProxy *self) { 53 | PyDictObject *dict = self->it.di_dict; 54 | if (dict == NULL) { 55 | return NULL; 56 | } 57 | 58 | if (self->it.reversed) { 59 | if (self->it.it_index >= 0) { 60 | JS::HandleId id = (*(self->it.props))[(self->it.it_index)--]; 61 | PyObject *key = idToKey(GLOBAL_CX, id); 62 | PyObject *value; 63 | 64 | if (self->it.kind != KIND_KEYS) { 65 | JS::RootedValue jsVal(GLOBAL_CX); 66 | JS_GetPropertyById(GLOBAL_CX, *(((JSObjectProxy *)(self->it.di_dict))->jsObject), id, &jsVal); 67 | value = pyTypeFactory(GLOBAL_CX, jsVal); 68 | } 69 | 70 | PyObject *ret; 71 | if (self->it.kind == KIND_ITEMS) { 72 | ret = PyTuple_Pack(2, key, value); 73 | } 74 | else if (self->it.kind == KIND_VALUES) { 75 | ret = value; 76 | } 77 | else { 78 | ret = key; 79 | } 80 | 81 | Py_INCREF(ret); 82 | if (self->it.kind != KIND_KEYS) { 83 | Py_DECREF(value); 84 | } 85 | 86 | return ret; 87 | } 88 | } else { 89 | if (self->it.it_index < JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)dict)) { 90 | JS::HandleId id = (*(self->it.props))[(self->it.it_index)++]; 91 | PyObject *key = idToKey(GLOBAL_CX, id); 92 | PyObject *value; 93 | 94 | if (self->it.kind != KIND_KEYS) { 95 | JS::RootedValue jsVal(GLOBAL_CX); 96 | JS_GetPropertyById(GLOBAL_CX, *(((JSObjectProxy *)(self->it.di_dict))->jsObject), id, &jsVal); 97 | value = pyTypeFactory(GLOBAL_CX, jsVal); 98 | } 99 | 100 | PyObject *ret; 101 | if (self->it.kind == KIND_ITEMS) { 102 | ret = PyTuple_Pack(2, key, value); 103 | } 104 | else if (self->it.kind == KIND_VALUES) { 105 | ret = value; 106 | } 107 | else { 108 | ret = key; 109 | } 110 | 111 | Py_INCREF(ret); 112 | if (self->it.kind != KIND_KEYS) { 113 | Py_DECREF(value); 114 | } 115 | 116 | return ret; 117 | } 118 | } 119 | 120 | self->it.di_dict = NULL; 121 | Py_DECREF(dict); 122 | return NULL; 123 | } 124 | 125 | PyObject *JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_len(JSObjectIterProxy *self) { 126 | Py_ssize_t len; 127 | if (self->it.di_dict) { 128 | len = JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->it.di_dict) - self->it.it_index; 129 | if (len >= 0) { 130 | return PyLong_FromSsize_t(len); 131 | } 132 | } 133 | return PyLong_FromLong(0); 134 | } -------------------------------------------------------------------------------- /src/JSObjectItemsProxy.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSObjectItemsProxy.cc 3 | * @author Philippe Laporte (philippe@distributive.network) 4 | * @brief JSObjectItemsProxy is a custom C-implemented python type that derives from dict keys 5 | * @date 2024-01-19 6 | * 7 | * @copyright Copyright (c) 2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/JSObjectItemsProxy.hh" 12 | 13 | #include "include/JSObjectIterProxy.hh" 14 | #include "include/JSObjectProxy.hh" 15 | #include "include/JSArrayProxy.hh" 16 | 17 | #include "include/modules/pythonmonkey/pythonmonkey.hh" 18 | #include "include/jsTypeFactory.hh" 19 | #include "include/PyBaseProxyHandler.hh" 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | 27 | 28 | void JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_dealloc(JSObjectItemsProxy *self) 29 | { 30 | PyObject_GC_UnTrack(self); 31 | Py_XDECREF(self->dv.dv_dict); 32 | PyObject_GC_Del(self); 33 | } 34 | 35 | Py_ssize_t JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_length(JSObjectItemsProxy *self) 36 | { 37 | if (self->dv.dv_dict == NULL) { 38 | return 0; 39 | } 40 | return JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->dv.dv_dict); 41 | } 42 | 43 | int JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_traverse(JSObjectItemsProxy *self, visitproc visit, void *arg) { 44 | Py_VISIT(self->dv.dv_dict); 45 | return 0; 46 | } 47 | 48 | int JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_clear(JSObjectItemsProxy *self) { 49 | Py_CLEAR(self->dv.dv_dict); 50 | return 0; 51 | } 52 | 53 | PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_iter(JSObjectItemsProxy *self) { 54 | JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); 55 | if (iterator == NULL) { 56 | return NULL; 57 | } 58 | iterator->it.reversed = false; 59 | iterator->it.it_index = 0; 60 | iterator->it.kind = KIND_ITEMS; 61 | Py_INCREF(self->dv.dv_dict); 62 | iterator->it.di_dict = self->dv.dv_dict; 63 | iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); 64 | // Get **enumerable** own properties 65 | if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { 66 | return NULL; 67 | } 68 | PyObject_GC_Track(iterator); 69 | return (PyObject *)iterator; 70 | } 71 | 72 | PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_iter_reverse(JSObjectItemsProxy *self) { 73 | JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); 74 | if (iterator == NULL) { 75 | return NULL; 76 | } 77 | iterator->it.reversed = true; 78 | iterator->it.it_index = JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_length(self) - 1; 79 | iterator->it.kind = KIND_ITEMS; 80 | Py_INCREF(self->dv.dv_dict); 81 | iterator->it.di_dict = self->dv.dv_dict; 82 | iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); 83 | // Get **enumerable** own properties 84 | if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { 85 | return NULL; 86 | } 87 | PyObject_GC_Track(iterator); 88 | return (PyObject *)iterator; 89 | } 90 | 91 | PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_repr(JSObjectItemsProxy *self) { 92 | PyObject *seq; 93 | PyObject *result = NULL; 94 | 95 | Py_ssize_t rc = Py_ReprEnter((PyObject *)self); 96 | if (rc != 0) { 97 | return rc > 0 ? PyUnicode_FromString("...") : NULL; 98 | } 99 | 100 | seq = PySequence_List((PyObject *)self); 101 | if (seq == NULL) { 102 | goto Done; 103 | } 104 | 105 | result = PyUnicode_FromFormat("%s(%R)", PyDictItems_Type.tp_name, seq); 106 | Py_DECREF(seq); 107 | 108 | Done: 109 | Py_ReprLeave((PyObject *)self); 110 | return result; 111 | } 112 | 113 | PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)) { 114 | return PyDictProxy_New((PyObject *)((_PyDictViewObject *)self)->dv_dict); 115 | } -------------------------------------------------------------------------------- /src/PyDictProxyHandler.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PyDictProxyHandler.cc 3 | * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) 4 | * @brief Struct for creating JS proxy objects for Dicts 5 | * @date 2023-04-20 6 | * 7 | * @copyright Copyright (c) 2023-2024 Distributive Corp. 8 | * 9 | */ 10 | 11 | #include "include/PyDictProxyHandler.hh" 12 | 13 | #include "include/jsTypeFactory.hh" 14 | #include "include/pyTypeFactory.hh" 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | 26 | 27 | const char PyDictProxyHandler::family = 0; 28 | 29 | bool PyDictProxyHandler::ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const { 30 | PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); 31 | PyObject *keys = PyDict_Keys(self); 32 | 33 | size_t length = PyList_Size(keys); 34 | 35 | return handleOwnPropertyKeys(cx, keys, length, props); 36 | } 37 | 38 | bool PyDictProxyHandler::delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 39 | JS::ObjectOpResult &result) const { 40 | PyObject *attrName = idToKey(cx, id); 41 | PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); 42 | if (PyDict_DelItem(self, attrName) < 0) { 43 | return result.failCantDelete(); // raises JS exception 44 | } 45 | return result.succeed(); 46 | } 47 | 48 | bool PyDictProxyHandler::has(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 49 | bool *bp) const { 50 | return hasOwn(cx, proxy, id, bp); 51 | } 52 | 53 | bool PyDictProxyHandler::getOwnPropertyDescriptor( 54 | JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 55 | JS::MutableHandle> desc 56 | ) const { 57 | PyObject *attrName = idToKey(cx, id); 58 | PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); 59 | PyObject *item = PyDict_GetItemWithError(self, attrName); // returns NULL without an exception set if the key wasn’t present. 60 | 61 | return handleGetOwnPropertyDescriptor(cx, id, desc, item); 62 | } 63 | 64 | bool PyDictProxyHandler::set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 65 | JS::HandleValue v, JS::HandleValue receiver, 66 | JS::ObjectOpResult &result) const { 67 | JS::RootedValue rootedV(cx, v); 68 | PyObject *attrName = idToKey(cx, id); 69 | 70 | PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); 71 | PyObject *value = pyTypeFactory(cx, rootedV); 72 | if (PyDict_SetItem(self, attrName, value)) { 73 | Py_DECREF(value); 74 | return result.failCantSetInterposed(); // raises JS exception 75 | } 76 | Py_DECREF(value); 77 | return result.succeed(); 78 | } 79 | 80 | bool PyDictProxyHandler::enumerate(JSContext *cx, JS::HandleObject proxy, 81 | JS::MutableHandleIdVector props) const { 82 | return this->ownPropertyKeys(cx, proxy, props); 83 | } 84 | 85 | bool PyDictProxyHandler::hasOwn(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, 86 | bool *bp) const { 87 | PyObject *attrName = idToKey(cx, id); 88 | PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); 89 | *bp = PyDict_Contains(self, attrName) == 1; 90 | return true; 91 | } 92 | 93 | bool PyDictProxyHandler::getOwnEnumerablePropertyKeys( 94 | JSContext *cx, JS::HandleObject proxy, 95 | JS::MutableHandleIdVector props) const { 96 | return this->ownPropertyKeys(cx, proxy, props); 97 | } 98 | 99 | bool PyDictProxyHandler::defineProperty(JSContext *cx, JS::HandleObject proxy, 100 | JS::HandleId id, 101 | JS::Handle desc, 102 | JS::ObjectOpResult &result) const { 103 | // Block direct `Object.defineProperty` since we already have the `set` method 104 | return result.failInvalidDescriptor(); 105 | } 106 | 107 | bool PyDictProxyHandler::getBuiltinClass(JSContext *cx, JS::HandleObject proxy, 108 | js::ESClass *cls) const { 109 | *cls = js::ESClass::Object; 110 | return true; 111 | } -------------------------------------------------------------------------------- /python/pythonmonkey/pythonmonkey.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | stub file for type hints & documentations for the native module 3 | @see https://typing.readthedocs.io/en/latest/source/stubs.html 4 | """ 5 | 6 | import typing as _typing 7 | 8 | 9 | class EvalOptions(_typing.TypedDict, total=False): 10 | filename: str 11 | lineno: int 12 | column: int 13 | mutedErrors: bool 14 | noScriptRval: bool 15 | selfHosting: bool 16 | strict: bool 17 | module: bool 18 | fromPythonFrame: bool 19 | 20 | # pylint: disable=redefined-builtin 21 | 22 | 23 | def eval(code: str, evalOpts: EvalOptions = {}, /) -> _typing.Any: 24 | """ 25 | JavaScript evaluator in Python 26 | """ 27 | 28 | 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 | 48 | def wait() -> _typing.Awaitable[None]: 49 | """ 50 | Block until all asynchronous jobs (Promise/setTimeout/etc.) finish. 51 | 52 | ```py 53 | await pm.wait() 54 | ``` 55 | 56 | This is the event-loop shield that protects the loop from being prematurely terminated. 57 | """ 58 | 59 | 60 | def stop() -> None: 61 | """ 62 | Stop all pending asynchronous jobs, and unblock `await pm.wait()` 63 | """ 64 | 65 | 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 | 72 | def isCompilableUnit(code: str) -> bool: 73 | """ 74 | Hint if a string might be compilable Javascript without actual evaluation 75 | """ 76 | 77 | 78 | def collect() -> None: 79 | """ 80 | Calls the spidermonkey garbage collector 81 | """ 82 | 83 | 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 | 92 | class JSFunctionProxy(): 93 | """ 94 | JavaScript Function proxy 95 | """ 96 | 97 | 98 | class JSMethodProxy(JSFunctionProxy, object): 99 | """ 100 | JavaScript Method proxy 101 | This constructs a callable object based on the first argument, bound to the second argument 102 | Useful when you wish to implement a method on a class object with JavaScript 103 | Example: 104 | import pythonmonkey as pm 105 | 106 | jsFunc = pm.eval("(function(value) { this.value = value})") 107 | class Class: 108 | def __init__(self): 109 | self.value = 0 110 | self.setValue = pm.JSMethodProxy(jsFunc, self) #setValue will be bound to self, so `this` will always be `self` 111 | 112 | myObject = Class() 113 | print(myObject.value) # 0 114 | myObject.setValue(42) 115 | print(myObject.value) # 42.0 116 | """ 117 | 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 | """ 163 | 164 | 165 | null = _typing.Annotated[ 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", 174 | ] 175 | --------------------------------------------------------------------------------