├── .clang-format ├── .flake8 ├── .github └── workflows │ ├── docs.yml │ └── main.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build_mkdocs.sh ├── build_utils └── merge_js.py ├── docs ├── JavaScript_API.md ├── Python_API.md ├── design.md ├── embed.md ├── index.md ├── installation.md ├── try_from_js.md └── try_from_py.md ├── environment-dev.yml ├── examples ├── README.rst ├── js_api_tour.js └── py_api_tour.py ├── include └── pyjs │ ├── convert.hpp │ ├── export_js_module.hpp │ ├── export_js_proxy.hpp │ ├── export_py_object.hpp │ ├── export_pyjs_module.hpp │ ├── inflate.hpp │ ├── install_conda_file.hpp │ ├── post_js │ └── fixes.js │ ├── pre_js │ ├── apply.js │ ├── cleanup.js │ ├── constants.js │ ├── create_once_callable.js │ ├── dynload │ │ ├── LICENCE │ │ ├── README.md │ │ └── dynload.js │ ├── fetch.js │ ├── get_set_attr.js │ ├── get_type_string.js │ ├── init.js │ ├── load_pkg.js │ ├── make_proxy.js │ ├── operators.js │ ├── platform.js │ ├── promise.js │ ├── shortcuts.js │ ├── wait_for_dependencies.js │ └── wrap_result.js │ └── untar.hpp ├── mkdocs.yml ├── module └── pyjs │ ├── __init__.py │ ├── convert.py │ ├── convert_py_to_js.py │ ├── core.py │ ├── error_handling.py │ ├── extend_js_val.py │ ├── pyodide_polyfill.py │ ├── webloop.py │ ├── webloop_LICENCE │ └── webloop_README.md ├── pyjsConfig.cmake.in ├── src ├── convert.cpp ├── export_js_module.cpp ├── export_js_proxy.cpp ├── export_py_object.cpp ├── export_pyjs_module.cpp ├── inflate.cpp ├── install_conda_file.cpp ├── js_timestamp.cpp ├── runtime.cpp └── untar.cpp ├── stubs └── pyjs_core │ ├── __init__.py │ └── __init__.pyi └── tests ├── __init__.py ├── atests ├── __init__.py └── async_tests.py ├── js_tests ├── __init__.py └── test_main.js ├── main.py ├── test.js.in ├── test_utils.py └── tests ├── __init__.py ├── conftest.py ├── test_conversion.py └── test_pyjs.py /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Mozilla 2 | AccessModifierOffset: '-4' 3 | AlignAfterOpenBracket: Align 4 | AlignEscapedNewlinesLeft: 'false' 5 | AllowAllParametersOfDeclarationOnNextLine: 'true' 6 | AllowShortBlocksOnASingleLine: 'false' 7 | AllowShortCaseLabelsOnASingleLine: 'false' 8 | AllowShortFunctionsOnASingleLine: 'false' 9 | AllowShortIfStatementsOnASingleLine: 'false' 10 | AllowShortLoopsOnASingleLine: 'false' 11 | AlwaysBreakTemplateDeclarations: 'true' 12 | SpaceAfterTemplateKeyword: 'true' 13 | BreakBeforeBinaryOperators: All 14 | BreakBeforeBraces: Allman 15 | BreakBeforeTernaryOperators: 'true' 16 | BreakConstructorInitializersBeforeComma: 'true' 17 | BreakStringLiterals: 'false' 18 | ColumnLimit: '100' 19 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' 20 | ConstructorInitializerIndentWidth: '4' 21 | ContinuationIndentWidth: '4' 22 | Cpp11BracedListStyle: 'false' 23 | DerivePointerAlignment: 'false' 24 | DisableFormat: 'false' 25 | ExperimentalAutoDetectBinPacking: 'true' 26 | IndentCaseLabels: 'true' 27 | IndentWidth: '4' 28 | IndentWrappedFunctionNames: 'false' 29 | JavaScriptQuotes: Single 30 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 31 | Language: Cpp 32 | MaxEmptyLinesToKeep: '2' 33 | NamespaceIndentation: All 34 | ObjCBlockIndentWidth: '4' 35 | ObjCSpaceAfterProperty: 'false' 36 | ObjCSpaceBeforeProtocolList: 'false' 37 | PointerAlignment: Left 38 | ReflowComments: 'true' 39 | SortIncludes: 'false' 40 | SpaceAfterCStyleCast: 'true' 41 | SpaceBeforeAssignmentOperators: 'true' 42 | SpaceBeforeParens: ControlStatements 43 | SpaceInEmptyParentheses: 'false' 44 | SpacesBeforeTrailingComments: '2' 45 | SpacesInAngles: 'false' 46 | SpacesInCStyleCastParentheses: 'false' 47 | SpacesInContainerLiterals: 'false' 48 | SpacesInParentheses: 'false' 49 | SpacesInSquareBrackets: 'false' 50 | Standard: c++17 51 | TabWidth: '4' 52 | UseTab: Never 53 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=100 3 | extend-ignore=E203,D104,D100,I004,F821 4 | exclude=*/tests/*,docs/source/tools/* 5 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | 8 | 9 | defaults: 10 | run: 11 | shell: bash -l {0} 12 | 13 | 14 | jobs: 15 | 16 | build_pyjs_docs: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | include: 24 | - emsdk_ver: "3.1.73" 25 | python_version: "3.13" 26 | 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Get number of CPU cores 32 | uses: SimenB/github-actions-cpu-cores@v1 33 | 34 | - name: Install micromamba 35 | uses: mamba-org/setup-micromamba@v1 36 | with: 37 | environment-file: environment-dev.yml 38 | environment-name: pyjs-wasm 39 | condarc: | 40 | channels: 41 | - https://repo.mamba.pm/emscripten-forge 42 | - conda-forge 43 | 44 | - name: build the docs 45 | shell: bash -el {0} 46 | run: | 47 | ./build_mkdocs.sh ${{matrix.emsdk_ver}} ${{ matrix.python_version }} 48 | 49 | ################################################################ 50 | # upload to github pages 51 | ################################################################ 52 | - name: Upload Pages artifact 53 | uses: actions/upload-pages-artifact@v3 54 | with: 55 | path: docs_build/mkdocs 56 | 57 | 58 | deploy: 59 | # only run on main branch 60 | if: github.ref == 'refs/heads/main' && github.repository == 'emscripten-forge/pyjs' 61 | 62 | # Add a dependency to the build job 63 | needs: build_pyjs_docs 64 | 65 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 66 | permissions: 67 | contents: read # to read the Pages artifact 68 | pages: write # to deploy to Pages 69 | id-token: write # to verify the deployment originates from an appropriate source 70 | 71 | # Deploy to the github-pages environment 72 | environment: 73 | name: github-pages 74 | url: ${{ steps.deployment.outputs.page_url }} 75 | 76 | # Specify runner + deployment step 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Deploy to GitHub Pages 80 | id: deployment 81 | uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action 82 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | 8 | 9 | defaults: 10 | run: 11 | shell: bash -l {0} 12 | 13 | 14 | jobs: 15 | 16 | test-browser: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | include: 24 | - emsdk_ver: "3.1.73" 25 | python_version: "3.13" 26 | pybind11_version: "" 27 | steps: 28 | - uses: actions/checkout@v2 29 | 30 | - name: Install micromamba 31 | uses: mamba-org/setup-micromamba@v1 32 | with: 33 | environment-file: environment-dev.yml 34 | environment-name: pyjs-wasm 35 | condarc: | 36 | channels: 37 | - https://repo.prefix.dev/emscripten-forge-dev 38 | - conda-forge 39 | 40 | 41 | - name: Install Playwright 42 | run: | 43 | playwright install 44 | 45 | - name: Build pyjs 46 | run: | 47 | micromamba activate pyjs-wasm 48 | 49 | micromamba create -n pyjs-build-wasm \ 50 | --platform=emscripten-wasm32 \ 51 | -c https://repo.prefix.dev/emscripten-forge-dev\ 52 | -c https://repo.mamba.pm/conda-forge \ 53 | --yes \ 54 | python=${{matrix.python_version}} \ 55 | "pybind11${{matrix.pybind11_version}}" \ 56 | nlohmann_json pybind11_json numpy \ 57 | pytest bzip2 sqlite zlib zstd libffi \ 58 | exceptiongroup emscripten-abi==${{matrix.emsdk_ver}} \ 59 | openssl liblzma 60 | 61 | 62 | mkdir build 63 | pushd build 64 | 65 | 66 | export PREFIX=$MAMBA_ROOT_PREFIX/envs/pyjs-build-wasm 67 | export CMAKE_PREFIX_PATH=$PREFIX 68 | export CMAKE_SYSTEM_PREFIX_PATH=$PREFIX 69 | 70 | # build pyjs 71 | emcmake cmake \ 72 | -DCMAKE_BUILD_TYPE=Release \ 73 | -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ON \ 74 | -DBUILD_RUNTIME_BROWSER=ON \ 75 | -DBUILD_RUNTIME_NODE=OFF \ 76 | -DCMAKE_INSTALL_PREFIX=$PREFIX \ 77 | .. 78 | 79 | make -j2 80 | 81 | make install 82 | 83 | popd 84 | 85 | - name: setup env with numpy 86 | run: | 87 | micromamba activate pyjs-wasm 88 | micromamba create -n pyjs-build-wasm-with-numpy \ 89 | --platform=emscripten-wasm32 \ 90 | -c https://repo.prefix.dev/emscripten-forge-dev\ 91 | -c https://repo.mamba.pm/conda-forge \ 92 | --yes \ 93 | "python=${{matrix.python_version}}" pytest numpy exceptiongroup 94 | 95 | 96 | - name: Test in browser-main 97 | run: | 98 | micromamba activate pyjs-wasm 99 | pyjs_code_runner run script \ 100 | browser-main \ 101 | --conda-env $MAMBA_ROOT_PREFIX/envs/pyjs-build-wasm-with-numpy \ 102 | --mount $(pwd)/tests:/tests \ 103 | --mount $(pwd)/module/pyjs:/lib/python${{matrix.python_version}}/site-packages/pyjs \ 104 | --script main.py \ 105 | --work-dir /tests \ 106 | --pyjs-dir $(pwd)/build \ 107 | --headless \ 108 | --async-main 109 | 110 | - name: Test in browser-worker 111 | run: | 112 | micromamba activate pyjs-wasm 113 | 114 | 115 | pyjs_code_runner run script \ 116 | browser-worker \ 117 | --conda-env $MAMBA_ROOT_PREFIX/envs/pyjs-build-wasm-with-numpy \ 118 | --mount $(pwd)/tests:/tests \ 119 | --mount $(pwd)/module/pyjs:/lib/python${{matrix.python_version}}/site-packages/pyjs \ 120 | --script main.py \ 121 | --work-dir /tests \ 122 | --pyjs-dir $(pwd)/build \ 123 | --headless \ 124 | --async-main 125 | 126 | - name: setup minimal env without numpy 127 | run: | 128 | micromamba activate pyjs-wasm 129 | micromamba create -n pyjs-build-wasm-no-numpy \ 130 | --platform=emscripten-wasm32 \ 131 | -c https://repo.prefix.dev/emscripten-forge-dev\ 132 | -c https://repo.mamba.pm/conda-forge \ 133 | --yes \ 134 | "python=${{matrix.python_version}}" pytest exceptiongroup 135 | 136 | - name: Test in browser-main-no-numpy 137 | run: | 138 | micromamba activate pyjs-wasm 139 | 140 | 141 | pyjs_code_runner run script \ 142 | browser-main \ 143 | --conda-env $MAMBA_ROOT_PREFIX/envs/pyjs-build-wasm-no-numpy \ 144 | --mount $(pwd)/tests:/tests \ 145 | --mount $(pwd)/module/pyjs:/lib/python${{matrix.python_version}}/site-packages/pyjs \ 146 | --script main.py \ 147 | --work-dir /tests \ 148 | --pyjs-dir $(pwd)/build \ 149 | --headless \ 150 | --async-main \ 151 | --no-cache -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **__pycache__ 2 | sandbox 3 | build_local.sh 4 | build 5 | build_repl 6 | build_tests/ 7 | node_modules 8 | package-lock.json 9 | docs/_build 10 | *.wasm 11 | nxtgm_javascript_runtime.js 12 | .DS_Store 13 | xeus-python/ 14 | emsdk_* 15 | docs_build/ 16 | empack_config.yaml -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | known_third_party=pybind11,conda,conda_env 4 | multi_line_output=3 5 | include_trailing_comma=True 6 | force_grid_wrap=0 7 | use_parentheses=True 8 | profile=black 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: libmamba/tests/repodata_json_cache* 2 | repos: 3 | - repo: https://github.com/psf/black 4 | rev: 22.3.0 5 | hooks: 6 | - id: black 7 | args: [--safe, --quiet] 8 | - repo: https://github.com/asottile/blacken-docs 9 | rev: v1.12.1 10 | hooks: 11 | - id: blacken-docs 12 | additional_dependencies: [black==22.3.0] 13 | - repo: https://github.com/pre-commit/pre-commit-hooks 14 | rev: v4.1.0 15 | hooks: 16 | - id: trailing-whitespace 17 | - id: end-of-file-fixer 18 | - id: fix-encoding-pragma 19 | args: [--remove] 20 | - id: check-yaml 21 | exclude: tests 22 | - id: check-toml 23 | - id: check-json 24 | - id: check-merge-conflict 25 | - id: pretty-format-json 26 | args: [--autofix] 27 | - id: debug-statements 28 | language_version: python3 29 | - repo: https://github.com/pre-commit/mirrors-isort 30 | rev: v5.10.1 31 | hooks: 32 | - id: isort 33 | exclude: tests/data 34 | - repo: https://gitlab.com/pycqa/flake8 35 | rev: 3.9.2 36 | hooks: 37 | - id: flake8 38 | language_version: python3 39 | additional_dependencies: 40 | - flake8-typing-imports==1.12.0 41 | - flake8-builtins==1.5.3 42 | - flake8-bugbear==22.1.11 43 | - flake8-isort==4.1.1 44 | - repo: https://github.com/pre-commit/mirrors-clang-format 45 | rev: v13.0.1 46 | hooks: 47 | - id: clang-format 48 | args: [--style=file, -i] 49 | exclude: ".json|.js" 50 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.9) 2 | 3 | project(pyjs VERSION 2.7.3 DESCRIPTION "pyjs") 4 | 5 | option(BUILD_RUNTIME_BROWSER "Build runtime" ON) 6 | option(WITH_NODE_TESTS "With node tests" OFF) 7 | option(LINK_LIBEXPAT "Link libexpat" OFF) 8 | option(LINK_LIBMPDEC "Link libmpdec" OFF) 9 | 10 | option(LINK_LIBLZMA "Link liblzma" OFF) 11 | 12 | # set PY_VERSION to 3.11 if the user has not set it 13 | if (NOT DEFINED PY_VERSION) 14 | set(PY_VERSION 3.11) 15 | endif() 16 | 17 | file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pycpp_includes) 18 | 19 | ########################################################## 20 | # generate prejs and postjs 21 | ########################################################## 22 | set(PREJS_FILES 23 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/constants.js 24 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/get_type_string.js 25 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/wrap_result.js 26 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/apply.js 27 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/get_set_attr.js 28 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/promise.js 29 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/create_once_callable.js 30 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/shortcuts.js 31 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/operators.js 32 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/platform.js 33 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/wait_for_dependencies.js 34 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/init.js 35 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/cleanup.js 36 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/make_proxy.js 37 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/dynload/dynload.js 38 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/fetch.js 39 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/pre_js/load_pkg.js 40 | ) 41 | set(POSTJS_FILES 42 | ${CMAKE_CURRENT_SOURCE_DIR}/include/pyjs/post_js/fixes.js 43 | ) 44 | 45 | 46 | add_custom_target(merge_pyjs ALL 47 | DEPENDS 48 | "pyjs_pre.js" 49 | "pyjs_post.js" 50 | ) 51 | 52 | 53 | add_custom_command(OUTPUT "pyjs_pre.js" 54 | COMMAND $ENV{CONDA_PREFIX}/bin/python 55 | ${CMAKE_SOURCE_DIR}/build_utils/merge_js.py 56 | ${PREJS_FILES} 57 | pyjs_pre.js 58 | ${CMAKE_CURRENT_SOURCE_DIR}/src/js_timestamp.cpp 59 | DEPENDS ${PREJS_FILES} 60 | ) 61 | 62 | add_custom_command(OUTPUT "pyjs_post.js" 63 | COMMAND $ENV{CONDA_PREFIX}/bin/python 64 | ${CMAKE_SOURCE_DIR}/build_utils/merge_js.py 65 | ${POSTJS_FILES} 66 | pyjs_post.js 67 | ${CMAKE_CURRENT_SOURCE_DIR}/src/js_timestamp.cpp 68 | DEPENDS ${POSTJS_FILES}) 69 | 70 | 71 | ########################################################## 72 | # headers to install 73 | ########################################################## 74 | set(PYJS_HEADERS 75 | include/pyjs/convert.hpp 76 | include/pyjs/export_js_module.hpp 77 | include/pyjs/export_js_proxy.hpp 78 | include/pyjs/export_py_object.hpp 79 | include/pyjs/export_pyjs_module.hpp 80 | include/pyjs/untar.hpp 81 | include/pyjs/install_conda_file.hpp 82 | include/pyjs/inflate.hpp 83 | ${CMAKE_CURRENT_BINARY_DIR}/pyjs_pre.js 84 | ${CMAKE_CURRENT_BINARY_DIR}/pyjs_post.js 85 | ) 86 | 87 | 88 | 89 | set(CMAKE_CXX_STANDARD 14) 90 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 91 | 92 | 93 | set(pybind11_REQUIRED_VERSION 2.6.1) 94 | 95 | if (NOT TARGET pybind11::headers) 96 | find_package(pybind11 ${pybind11_REQUIRED_VERSION} REQUIRED) 97 | endif () 98 | 99 | 100 | add_library(pyjs STATIC 101 | src/export_js_module.cpp 102 | src/export_js_proxy.cpp 103 | src/export_py_object.cpp 104 | src/convert.cpp 105 | src/export_pyjs_module.cpp 106 | src/js_timestamp.cpp 107 | src/inflate.cpp 108 | src/untar.cpp 109 | src/install_conda_file.cpp 110 | ${PYCPPSOURCES} 111 | ) 112 | 113 | target_link_libraries(pyjs PRIVATE ${PYTHON_UTIL_LIBS} pybind11::embed) 114 | 115 | target_compile_options(pyjs 116 | PUBLIC --std=c++17 117 | PUBLIC -Wno-deprecated 118 | PUBLIC "SHELL: -fexceptions" 119 | ) 120 | 121 | target_link_options(pyjs 122 | PRIVATE -lembind 123 | PUBLIC -Wno-unused-command-line-argument 124 | PUBLIC "SHELL: -fexceptions" 125 | #PUBLIC "SHELL:-s EXPORT_EXCEPTION_HANDLING_HELPERS" 126 | #PUBLIC "SHELL:-s EXCEPTION_CATCHING_ALLOWED=['we only want to allow exception handling in side modules']" 127 | ) 128 | 129 | 130 | set_target_properties(pyjs PROPERTIES 131 | PUBLIC_HEADER "${PYJS_HEADERS}" 132 | # DEPENDS "pyjs_pre.js" "pyjs_post.js" 133 | ) 134 | 135 | add_dependencies(pyjs merge_pyjs) 136 | 137 | target_link_libraries(pyjs PRIVATE pybind11::pybind11) 138 | target_compile_options(pyjs PRIVATE -fPIC) 139 | 140 | 141 | target_include_directories(pyjs 142 | PUBLIC 143 | $ 144 | $ 145 | ) 146 | 147 | SET(PYTHON_UTIL_LIBS 148 | ${CMAKE_INSTALL_PREFIX}/lib/libbz2.a 149 | ${CMAKE_INSTALL_PREFIX}/lib/libz.a 150 | ${CMAKE_INSTALL_PREFIX}/lib/libsqlite3.a 151 | ${CMAKE_INSTALL_PREFIX}/lib/libffi.a 152 | ${CMAKE_INSTALL_PREFIX}/lib/libzstd.a 153 | # ssl 154 | ${CMAKE_INSTALL_PREFIX}/lib/libssl.a 155 | # crypto 156 | ${CMAKE_INSTALL_PREFIX}/lib/libcrypto.a 157 | # lzma 158 | ${CMAKE_INSTALL_PREFIX}/lib/liblzma.a 159 | ) 160 | 161 | 162 | if (LINK_LIBEXPAT) 163 | SET(PYTHON_UTIL_LIBS ${PYTHON_UTIL_LIBS} ${CMAKE_INSTALL_PREFIX}/lib/libexpat.a) 164 | endif() 165 | 166 | if (LINK_LIBMPDEC) 167 | SET(PYTHON_UTIL_LIBS ${PYTHON_UTIL_LIBS} ${CMAKE_INSTALL_PREFIX}/lib/libmpdec.a) 168 | endif() 169 | 170 | set_target_properties(pyjs 171 | PROPERTIES 172 | CXX_STANDARD 17 173 | ) 174 | 175 | 176 | add_executable(pyjs_runtime_browser src/runtime.cpp ) 177 | SET(ENVIRONMENT "web,worker") 178 | target_compile_definitions(pyjs_runtime_browser PUBLIC -DPYTEST_DRIVER_WEB) 179 | 180 | target_include_directories(pyjs_runtime_browser 181 | PRIVATE 182 | ${CMAKE_SOURCE_DIR}/include 183 | ${CMAKE_CURRENT_BINARY_DIR}/pycpp_includes 184 | ${ZLIB_INCLUDE_DIRS} 185 | ) 186 | 187 | set_target_properties(pyjs_runtime_browser 188 | PROPERTIES 189 | CXX_STANDARD 17 190 | ) 191 | target_compile_definitions(pyjs_runtime_browser PUBLIC -DPYJS_WEB) 192 | target_compile_options(pyjs_runtime_browser PRIVATE -fPIC) 193 | target_link_libraries(pyjs_runtime_browser PRIVATE pyjs pybind11::embed ${PYTHON_UTIL_LIBS}) 194 | 195 | 196 | 197 | target_compile_options(pyjs_runtime_browser 198 | PUBLIC --std=c++17 199 | PUBLIC -Wno-deprecated 200 | PUBLIC "SHELL: -s ENVIRONMENT=${ENVIRONMENT}" 201 | PUBLIC "SHELL: -fexceptions" 202 | #PUBLIC "SHELL:-s EXPORT_EXCEPTION_HANDLING_HELPERS" 203 | PUBLIC "SHELL: -s FORCE_FILESYSTEM" 204 | PUBLIC "SHELL: -s LZ4=1" 205 | PUBLIC "SHELL: -flto" 206 | # PUBLIC "SHELL: -s WASM_BIGINT" 207 | ) 208 | 209 | target_link_options(pyjs_runtime_browser 210 | PRIVATE -lembind 211 | PUBLIC -Wno-unused-command-line-argument 212 | PUBLIC "SHELL: -s MODULARIZE=1" 213 | PUBLIC "SHELL: -s EXPORT_NAME=\"createModule\"" 214 | PUBLIC "SHELL: -s EXPORT_ES6=0" 215 | PUBLIC "SHELL: -s USE_ES6_IMPORT_META=0" 216 | PUBLIC "SHELL: -s DEMANGLE_SUPPORT=0" 217 | PUBLIC "SHELL: -s ASSERTIONS=0" 218 | PUBLIC "SHELL: -s ALLOW_MEMORY_GROWTH=1" 219 | PUBLIC "SHELL: -s EXIT_RUNTIME=1" 220 | PUBLIC "SHELL: -s WASM=1" 221 | PUBLIC "SHELL: -s USE_PTHREADS=0" 222 | PUBLIC "SHELL: -s ENVIRONMENT=${ENVIRONMENT}" 223 | PUBLIC "SHELL: -fexceptions" 224 | PUBLIC "SHELL: -s MAIN_MODULE=1" 225 | PUBLIC "SHELL: -s ENVIRONMENT=${ENVIRONMENT}" 226 | PUBLIC "SHELL: -s TOTAL_STACK=16mb" 227 | PUBLIC "SHELL: -s INITIAL_MEMORY=64mb" 228 | PUBLIC "SHELL: -s FORCE_FILESYSTEM" 229 | PUBLIC "SHELL: -s LZ4=1" 230 | PUBLIC "SHELL: --post-js pyjs_post.js" 231 | PUBLIC "SHELL: --pre-js pyjs_pre.js" 232 | PUBLIC "SHELL: -flto" 233 | PUBLIC "SHELL: -lidbfs.js" 234 | PUBLIC "SHELL: -s WASM_BIGINT" 235 | ) 236 | 237 | 238 | install(TARGETS pyjs_runtime_browser 239 | DESTINATION ${CMAKE_INSTALL_PREFIX}/lib_js/pyjs) 240 | install(FILES 241 | "$/pyjs_runtime_browser.wasm" 242 | DESTINATION ${CMAKE_INSTALL_PREFIX}/lib_js/pyjs) 243 | 244 | 245 | 246 | 247 | 248 | include(GNUInstallDirs) 249 | include(CMakePackageConfigHelpers) 250 | 251 | set(PYJS_CMAKECONFIG_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" CACHE STRING "install path for pyjsConfig.cmake") 252 | 253 | # Configure 'pyjsConfig.cmake' for an install tree 254 | set(PYJS_CONFIG_CODE "") 255 | 256 | # Configure 'pyjs-pythonConfig.cmake' for a build tree 257 | set(PYJS_CONFIG_CODE "####### Expanded from \@PYJS_CONFIG_CODE\@ #######\n") 258 | set(PYJS_CONFIG_CODE "${PYJS_CONFIG_CODE}set(CMAKE_MODULE_PATH \"${CMAKE_CURRENT_SOURCE_DIR}/cmake;\${CMAKE_MODULE_PATH}\")\n") 259 | set(PYJS_CONFIG_CODE "${PYJS_CONFIG_CODE}##################################################") 260 | configure_package_config_file(${PROJECT_NAME}Config.cmake.in 261 | "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" 262 | INSTALL_DESTINATION ${PROJECT_BINARY_DIR}) 263 | 264 | # Configure 'pyjs-pythonConfig.cmake' for an install tree 265 | set(PYJS_CONFIG_CODE "") 266 | 267 | configure_package_config_file(${PROJECT_NAME}Config.cmake.in 268 | "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${PROJECT_NAME}Config.cmake" 269 | INSTALL_DESTINATION ${PYJS_CMAKECONFIG_INSTALL_DIR}) 270 | 271 | 272 | write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake 273 | VERSION ${${PROJECT_NAME}_VERSION} 274 | COMPATIBILITY AnyNewerVersion) 275 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${PROJECT_NAME}Config.cmake 276 | ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake 277 | DESTINATION ${PYJS_CMAKECONFIG_INSTALL_DIR}) 278 | 279 | # export(EXPORT ${PROJECT_NAME}-targets 280 | # FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Targets.cmake") 281 | 282 | set(PYJS_PYTHON_TARGETS "") 283 | list(APPEND PYJS_PYTHON_TARGETS pyjs) 284 | 285 | install(TARGETS ${PYJS_PYTHON_TARGETS} 286 | EXPORT pyjs-targets 287 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 288 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 289 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 290 | PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pyjs) 291 | 292 | export(EXPORT ${PROJECT_NAME}-targets 293 | FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Targets.cmake") 294 | 295 | install(EXPORT ${PROJECT_NAME}-targets 296 | FILE ${PROJECT_NAME}Targets.cmake 297 | DESTINATION ${PYJS_CMAKECONFIG_INSTALL_DIR}) 298 | 299 | 300 | 301 | install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/module/pyjs 302 | DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python${PY_VERSION}/site-packages 303 | FILES_MATCHING PATTERN "*.py" 304 | PATTERN "__pycache__" EXCLUDE 305 | ) 306 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyjs 2 | [![CI](https://github.com/emscripten-forge/pyjs/actions/workflows/main.yml/badge.svg)](https://github.com/emscripten-forge/pyjs/actions/workflows/main.yml) 3 | [![CI](https://img.shields.io/badge/pyjs-docs-yellow)](https://emscripten-forge.github.io/pyjs/) 4 | 5 | ## What is pyjs 6 | 7 | pyjs is modern [pybind11](https://github.com/pybind/pybind11) + emscripten [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html) based 8 | Python <-> JavaScript foreign function interface (FFI) for wasm/emscripten compiled Python. 9 | 10 | The API is loosly based on the FFI of [pyodide](https://pyodide.org/en/stable/usage/type-conversions.html). 11 | 12 | ## Full Documentation 13 | See the [documentation](https://emscripten-forge.github.io/pyjs/) for a full documentation. 14 | 15 | ## Try it out 16 | * To try it out, you can use the [playground](https://emscripten-forge.github.io/pyjs/lite/). 17 | * To create your own jupyterlite deployments use 18 | [xeus-lite-demo](https://github.com/jupyterlite/xeus-lite-demo) 19 | 20 | ## Quickstart 21 | 22 | Access Javascript from Python: 23 | 24 | ```python 25 | import pyjs 26 | 27 | # hello world 28 | pyjs.js.console.log("Hello, World!") 29 | 30 | # create a JavaScript function to add two numbers 31 | js_function = pyjs.js.Function("a", "b", """ 32 | console.log("Adding", a, "and", b) 33 | return a + b 34 | """) 35 | 36 | # call the function 37 | result = js_function(1, 2) 38 | ``` 39 | 40 | Access Python from Javascript: 41 | 42 | ```JavaScript 43 | // hello world 44 | pyjs.eval("print('Hello, World!')") 45 | 46 | // eval a python expression and get the result 47 | const py_list = pyjs.eval("[i for i in range(10)]") 48 | 49 | /// access 50 | console.log(py_list.get(0)) // same as py_list[0] on the python side 51 | ``` 52 | -------------------------------------------------------------------------------- /build_mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # dir of this script 5 | THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | WASM_ENV_NAME=pyjs-wasm-dev 7 | WASM_ENV_PREFIX=$MAMBA_ROOT_PREFIX/envs/$WASM_ENV_NAME 8 | EMSDK_DIR=$WASM_ENV_PREFIX/opt/emsdk 9 | EMSDK_VERSION=$1 10 | PYTHON_VERSION=$2 11 | 12 | 13 | 14 | 15 | PYJS_PROBE_FILE=$WASM_ENV_PREFIX/lib_js/pyjs/pyjs_runtime_browser.js 16 | 17 | if [ ! -d "$WASM_ENV_PREFIX" ]; then 18 | echo "Creating wasm env $WASM_ENV_NAME" 19 | micromamba create -n $WASM_ENV_NAME \ 20 | --platform=emscripten-wasm32 \ 21 | -c https://repo.prefix.dev/emscripten-forge-dev\ 22 | -c https://repo.prefix.dev/conda-forge \ 23 | --yes \ 24 | python=$PYTHON_VERSION "pybind11" nlohmann_json pybind11_json numpy \ 25 | bzip2 sqlite zlib zstd libffi exceptiongroup\ 26 | "xeus" "xeus-lite" xeus-python "xeus-javascript" xtl "ipython" "traitlets>=5.14.2" \ 27 | openssl liblzma 28 | 29 | else 30 | echo "Wasm env $WASM_ENV_NAME already exists" 31 | fi 32 | 33 | 34 | 35 | 36 | if true; then 37 | echo "Building pyjs" 38 | 39 | cd $THIS_DIR 40 | 41 | mkdir -p build 42 | cd build 43 | 44 | export PREFIX=$WASM_ENV_PREFIX 45 | export CMAKE_PREFIX_PATH=$PREFIX 46 | export CMAKE_SYSTEM_PREFIX_PATH=$PREFIX 47 | 48 | emcmake cmake \ 49 | -DCMAKE_BUILD_TYPE=Release \ 50 | -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ON \ 51 | -DBUILD_RUNTIME_BROWSER=ON \ 52 | -DBUILD_RUNTIME_NODE=OFF \ 53 | -DCMAKE_INSTALL_PREFIX=$PREFIX \ 54 | .. 55 | 56 | emmake make -j2 57 | emmake make install 58 | 59 | else 60 | echo "Skipping build pyjs" 61 | fi 62 | 63 | # if there is no xeus-python dir, clone it 64 | if [ ! -d "$THIS_DIR/xeus-python" ]; then 65 | cd $THIS_DIR 66 | git clone https://github.com/jupyter-xeus/xeus-python/ 67 | else 68 | echo "xeus-python dir already exists" 69 | fi 70 | 71 | 72 | 73 | if true; then 74 | 75 | echo "Building xeus-python" 76 | 77 | cd $THIS_DIR 78 | # source $EMSDK_DIR/emsdk_env.sh 79 | 80 | 81 | cd xeus-python 82 | 83 | export PREFIX=$WASM_ENV_PREFIX 84 | export CMAKE_PREFIX_PATH=$PREFIX 85 | export CMAKE_SYSTEM_PREFIX_PATH=$PREFIX 86 | 87 | 88 | # remove the fake python 89 | rm -rf $PREFIX/bin/python* 90 | rm -rf $PREFIX/bin/pip* 91 | mkdir -p build_wasm 92 | cd build_wasm 93 | 94 | 95 | # # this is stupid 96 | # cp -r $WASM_ENV_PREFIX/include/python3.11/ $WASM_ENV_PREFIX/include/ 97 | 98 | emcmake cmake .. \ 99 | -DCMAKE_BUILD_TYPE=Release \ 100 | -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ON \ 101 | -DCMAKE_INSTALL_PREFIX=$PREFIX \ 102 | -DXPYT_EMSCRIPTEN_WASM_BUILD=ON \ 103 | -DCMAKE_INCLUDE_PATH=$WASM_ENV_PREFIX/include/python3.13 104 | 105 | 106 | emmake make -j8 install 107 | if [ -d "$WASM_ENV_PREFIX/share/jupyter/kernels/xpython-raw" ]; then 108 | rm -rf $WASM_ENV_PREFIX/share/jupyter/kernels/xpython-raw 109 | fi 110 | 111 | else 112 | echo "Skipping build xeus-python" 113 | fi 114 | 115 | 116 | if false; then 117 | echo "Building xeus-javascript" 118 | 119 | cd $THIS_DIR 120 | # source $EMSDK_DIR/emsdk_env.sh 121 | 122 | 123 | cd ~/src/xeus-javascript 124 | mkdir -p build_wasm 125 | cd build_wasm 126 | 127 | export PREFIX=$WASM_ENV_PREFIX 128 | export CMAKE_PREFIX_PATH=$PREFIX 129 | export CMAKE_SYSTEM_PREFIX_PATH=$PREFIX 130 | 131 | 132 | emcmake cmake .. \ 133 | -DCMAKE_BUILD_TYPE=Release \ 134 | -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ON \ 135 | -DCMAKE_INSTALL_PREFIX=$PREFIX \ 136 | -DXPYT_EMSCRIPTEN_WASM_BUILD=ON\ 137 | 138 | emmake make -j8 install 139 | else 140 | echo "Skipping build xeus-javascript" 141 | fi 142 | 143 | 144 | 145 | if true ; then 146 | 147 | rm -rf $THIS_DIR/docs_build 148 | mkdir -p $THIS_DIR/docs_build 149 | 150 | # convert *.py to *.ipynb using jupytext 151 | NOTEBOOK_OUTPUT_DIR=$THIS_DIR/docs_build/notebooks 152 | rm -rf $NOTEBOOK_OUTPUT_DIR 153 | mkdir -p $NOTEBOOK_OUTPUT_DIR 154 | 155 | for f in $THIS_DIR/examples/*.py; do 156 | # get the filename without the extension and path 157 | filename=$(basename -- "$f") 158 | jupytext $f --to ipynb --output $NOTEBOOK_OUTPUT_DIR/${filename%.*}.ipynb --update-metadata '{"kernelspec": {"name": "xpython"}}' 159 | done 160 | for f in $THIS_DIR/examples/*.js; do 161 | # get the filename without the extension and path 162 | filename=$(basename -- "$f") 163 | jupytext $f --to ipynb --output $NOTEBOOK_OUTPUT_DIR/${filename%.*}.ipynb --update-metadata '{"kernelspec": {"name": "xjavascript"}}' 164 | done 165 | 166 | 167 | 168 | # lite 169 | if true; then 170 | cd $THIS_DIR 171 | rm -rf docs_build/_output 172 | rm -rf docs_build/.jupyterlite.doit.db 173 | mkdir -p docs_build 174 | cd docs_build 175 | 176 | jupyter lite build \ 177 | --contents=$NOTEBOOK_OUTPUT_DIR \ 178 | --XeusAddon.prefix=$WASM_ENV_PREFIX \ 179 | --XeusAddon.mounts=$THIS_DIR/module/pyjs:/lib/python3.11/site-packages/pyjs 180 | fi 181 | fi 182 | 183 | 184 | # the docs itself 185 | if true ; then 186 | 187 | export PREFIX=$MAMBA_ROOT_PREFIX/envs/pyjs-wasm 188 | echo "Building docs" 189 | 190 | cd $THIS_DIR 191 | mkdir -p docs_build/mkdocs 192 | export PYTHONPATH=$PYTHONPATH:$THIS_DIR/stubs 193 | export PYTHONPATH=$PYTHONPATH:$THIS_DIR/module 194 | mkdocs build --site-dir=docs_build/mkdocs 195 | 196 | 197 | fi 198 | 199 | if true ; then 200 | # # copy lite _output to docs_build 201 | cp -r $THIS_DIR/docs_build/_output $THIS_DIR/docs_build/mkdocs/lite 202 | # copy pyjs binary to docs_build 203 | cp $WASM_ENV_PREFIX/lib_js/pyjs/* $THIS_DIR/docs_build/mkdocs/lite/xeus/bin/ 204 | 205 | fi -------------------------------------------------------------------------------- /build_utils/merge_js.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import datetime 3 | 4 | 5 | def merge(*filenames): 6 | filenames = list(filenames) 7 | filename_timestamp = filenames.pop() 8 | filename_out = filenames.pop() 9 | filenames_in = filenames 10 | 11 | print("filenames_in", filenames_in) 12 | print("filename_out", filename_out) 13 | print("filename_timestamp", filename_timestamp) 14 | with open(filename_out, "w") as outfile: 15 | for fname in filenames_in: 16 | with open(fname) as infile: 17 | for line in infile: 18 | outfile.write(line) 19 | outfile.write("\n") 20 | 21 | with open(filename_timestamp, "w") as outfile: 22 | timestamp = str(datetime.datetime.utcnow()) 23 | outfile.write(f'#define PYJS_JS_UTC_TIMESTAMP "{timestamp}"') 24 | 25 | 26 | if __name__ == "__main__": 27 | print("merging js files:") 28 | merge(*sys.argv[1:]) 29 | -------------------------------------------------------------------------------- /docs/JavaScript_API.md: -------------------------------------------------------------------------------- 1 | # JavaScript API 2 | 3 | ## `pyjs` 4 | The main module for the JavaScript API. 5 | ### `exec` 6 | Execute a string of Python code. 7 | 8 | Example: 9 | ```javascript 10 | pyjs.exec(` 11 | import numpy 12 | print(numpy.random.rand(3)) 13 | `); 14 | ``` 15 | 16 | ### `exec_eval` 17 | Execute a string of Python code and return the last expression. 18 | 19 | Example: 20 | ```javascript 21 | const arr = pyjs.exec_eval(` 22 | import numpy 23 | numpy.random.rand(3) 24 | `); 25 | // use the array 26 | console.log(arr); 27 | // delete the array 28 | arr.delete(); 29 | ``` 30 | 31 | ### `eval` 32 | Evaluate a string with a Python expression. 33 | 34 | Example: 35 | ```javascript 36 | const result = pyjs.eval("sum([i for i in range(100)])") 37 | console.log(result); // 4950 38 | ``` 39 | 40 | ### `async_exec_eval` 41 | Schedule the execution of a string of Python code and return a promise. 42 | The last expression is returned as the result of the promise. 43 | 44 | Example: 45 | ```javascript 46 | const py_code = ` 47 | import asyncio 48 | await asyncio.sleep(2) 49 | sum([i for i in range(100)]) 50 | ` 51 | result = await pyjs.async_exec_eval(py_code) 52 | console.log(result); // 4950 53 | ``` 54 | 55 | ### `eval_file` 56 | Evaluate a file from the virtual file system. 57 | 58 | Example: 59 | ```javascript 60 | const file_content = ` 61 | import numpy 62 | 63 | def fobar(): 64 | return "fubar" 65 | 66 | def foo(): 67 | return "foo" 68 | 69 | if __name__ == "__main__": 70 | print(fobar()) 71 | ` 72 | pyjs.FS.writeFile("/hello_world.py", file_content); 73 | 74 | // evaluate the file 75 | // print "fubar" 76 | pyjs.eval_file("/hello_world.py") 77 | 78 | // use content from files scope 79 | // prints foo 80 | pyjs.eval("foo()") ; 81 | ``` 82 | 83 | ### `pyobject` 84 | A Python object exported as a JavaScript class. 85 | In Python, allmost everything is an object. This class holds the Python object 86 | compiled to JavaScript. 87 | 88 | 89 | 90 | 91 | #### `py_call` 92 | Call the `__call__` method of a Python object. 93 | 94 | Example: 95 | ```javascript 96 | const py_code = ` 97 | class Foobar: 98 | def __init__(self, foo): 99 | self.foo = foo 100 | def bar(self): 101 | return f"I am {self.foo}" 102 | 103 | def __call__(self, foobar): 104 | print(f"called Foobar.__call__ with foobar {foobar}") 105 | 106 | # last statement is returned 107 | Foobar 108 | ` 109 | 110 | // py_foobar_cls is a pyobject on 111 | // the JavaScript side and the class Foobar 112 | // on the Python side 113 | var py_foobar_cls = pyjs.exec_eval(py_code) 114 | 115 | // all function call-like statements (ie Foobar(2) need to be done via py_call) 116 | var instance = py_foobar_cls.py_call(2) 117 | // prints 2 118 | console.log(instance.foo) 119 | 120 | // prints "I am 2" 121 | console.log(instance.bar.py_call()) 122 | 123 | // prints called Foobar.__call__ with foobar 42 124 | instance.py_call(42) 125 | ``` 126 | 127 | #### `py_apply` 128 | Call the `__call__` method of a Python object with an array of arguments. 129 | 130 | #### `get` 131 | call the bracket operator `[]` of a Python object. 132 | 133 | Example: 134 | ```javascript 135 | const arr = pyjs.exec_eval("import numpy;numpy.eye(2)"); 136 | console.log(arr.get(0,0)) // prints 1 137 | console.log(arr.get(0,1)) // prints 0 138 | console.log(arr.get(1,0)) // prints 0 139 | console.log(arr.get(1,1)) // prints 1 140 | ``` -------------------------------------------------------------------------------- /docs/Python_API.md: -------------------------------------------------------------------------------- 1 | # Python API 2 | 3 | ::: pyjs_core 4 | handler: python 5 | options: 6 | members: 7 | - JsValue 8 | 9 | 10 | ::: pyjs 11 | handler: python 12 | options: 13 | show_submodules: true 14 | members: 15 | - pyjs_core 16 | - to_js 17 | - to_py 18 | - register_converter 19 | - JsToPyConverterOptions 20 | - new 21 | - create_callable 22 | - callable_context 23 | - create_once_callable 24 | - promise 25 | - apply 26 | - WebLoop 27 | - JsException 28 | - JsGenericError 29 | - JsError 30 | - JsInternalError 31 | - JsRangeError 32 | - JsReferenceError 33 | - JsSyntaxError 34 | - JsTypeError 35 | - JsURIError 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | ## Main Idea 4 | ### pybind11 5 | [Pybind11](https://github.com/pybind/pybind11) is a library that exposes C++ types in Python. It is a wrapper around the Python C API that allows for seamless integration of C++ and Python. 6 | To export a C++ class like the following to Python, you would use pybind11: 7 | 8 | ```C++ 9 | // foo.h 10 | class Foo { 11 | public: 12 | void say_hello() { 13 | std::cout << "Hello, World!" << std::endl; 14 | } 15 | }; 16 | ``` 17 | 18 | ```C++ 19 | // main.cpp 20 | #include 21 | #include 22 | 23 | PYBIND11_MODULE(example, m) { 24 | py::class_(m, "Foo") 25 | .def(py::init<>()) 26 | .def("say_hello", &Foo::say_hello); 27 | } 28 | ``` 29 | Not only can Python call C++ functions, but C++ can also call Python functions. In particular, one can interact 30 | with Python objects. An object is represented by the `py::object` type on the C++ side. 31 | 32 | ```C++ 33 | // main.cpp 34 | py::object sys = py::module::import("sys"); 35 | py::object version = sys.attr("version"); 36 | std::string version_string = version.cast(); 37 | std::cout << "Python version: " << version_string << std::endl; 38 | 39 | ``` 40 | 41 | 42 | 43 | 44 | ### embind 45 | There is a simmilar for emscripten called [embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html). It allows you to expose C++ types to JavaScript. 46 | 47 | ```C++ 48 | // main.cpp 49 | #include 50 | #include 51 | using namespace emscripten; 52 | // Binding code 53 | EMSCRIPTEN_BINDINGS(my_class_example) { 54 | class_("Foo") 55 | .constructor<>() 56 | .function("say_hello", &Foo::say_hello) 57 | ; 58 | } 59 | ``` 60 | To access JavasScript from C++, you would use the `emscripten::val` type. This is the pendant to `py::object` in pybind11. 61 | 62 | ```C++ 63 | emscripten::val console = emscripten::val::global("console"); 64 | console.call("log", "Hello, World!"); 65 | ``` 66 | 67 | ### pyjs 68 | The main idea of pyjs is, to export emscripten `emscripten::val` objects to Python with pybind11 and to export pybind11 `py::object` objects to JavaScript with embind. 69 | 70 | That way, we get a seamless integration of Python and JavaScript with relatively little effort and high level C++ 71 | code. 72 | 73 | 74 | 75 | ## Error Handling 76 | 77 | To catch JavaScript exceptions from Python, we wrap all JavaScript code in a try-catch block. If an exception is thrown, we catch it and raise a Python exception with the same message. 78 | The Python exceptions are directly translated to JavaScript exceptions. 79 | 80 | 81 | 82 | ## Memory Management 83 | Any C++ class that is exported via embind needs to be deleted by hand with `delete` method. This is because the JavaScript garbage collector does not know about the C++ objects. 84 | Therefore all `py::object` objects that are created from javascript objects need to be deleted by hand. This is done by calling the `delete` method on the `pyobject` object JavaScript side. 85 | 86 | 87 | ## Performance 88 | 89 | Compared to pyodide, pyjs is slowwer when crossing the language barrier. Yet itm is fast enough for all practical purposes. 90 | 91 | ## Packaging 92 | 93 | Pyjs is exclusively via [emscripten-forge](https://github.com/emscripten-forge/recipes). 94 | 95 | ## Testing 96 | 97 | To test pyjs without manually inspecting a web page, we use [pyjs-code-runner](https://github.com/emscripten-forge/pyjs-code-runner). This is a tool that runs a Python script in a headless browser and returns the output. 98 | -------------------------------------------------------------------------------- /docs/embed.md: -------------------------------------------------------------------------------- 1 | # Embedding pyjs in C++ 2 | Not only can `pyjs` be used as a standalone Python interpreter, but it can also be embedded in a C++ program. This allows you to run Python code in a C++ program. 3 | Pyjs is compiled as a static library that can be linked to a C++ program (when compiled with emscripten). 4 | 5 | 6 | To include `pyjs` in a C++ program, the following code is needed: 7 | 8 | ```C++ 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | // export the python core module of pyjs 17 | PYBIND11_EMBEDDED_MODULE(pyjs_core, m) { 18 | pyjs::export_pyjs_module(m); 19 | } 20 | 21 | // export the javascript module of pyjs 22 | EMSCRIPTEN_BINDINGS(my_module) { 23 | pyjs::export_js_module(); 24 | } 25 | 26 | ``` 27 | 28 | In the CMakelists.txt file, the following lines are needed to link the `pyjs` library: 29 | 30 | ```CMake 31 | find_package(pyjs ${pyjs_REQUIRED_VERSION} REQUIRED) 32 | 33 | target_link_libraries(my_target PRIVATE pyjs) 34 | 35 | target_link_options(my_target 36 | PUBLIC "SHELL: -s LZ4=1" 37 | PUBLIC "SHELL: --post-js ${pyjs_PRO_JS_PATH}" 38 | PUBLIC "SHELL: --pre-js ${pyjs_PRE_JS_PATH}" 39 | PUBLIC "SHELL: -s MAIN_MODULE=1" 40 | PUBLIC "SHELL: -s WASM_BIGINT" 41 | PUBLIC "-s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=\"['\$Browser', '\$ERRNO_CODES']\" " 42 | ) 43 | ``` 44 | As described in the [deployment](../installation) section, the needs to be packed. 45 | See the [pack the environment](../installation/#pack-the-environment)-section for instructions on to pack the environment with [`empack`](https://github.com/emscripten-forge/empack). 46 | 47 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to `pyjs` 2 | 3 | Pyjs is a python - javascript FFI for webassembly. 4 | It allows you to write python code and run it in the browser. 5 | 6 | 7 | ## Quickstart 8 | 9 | Access Javascript from Python: 10 | 11 | ```python 12 | import pyjs 13 | 14 | # hello world 15 | pyjs.js.console.log("Hello, World!") 16 | 17 | # create a JavaScript function to add two numbers 18 | js_function = pyjs.js.Function("a", "b", """ 19 | console.log("Adding", a, "and", b) 20 | return a + b 21 | """) 22 | 23 | # call the function 24 | result = js_function(1, 2) 25 | ``` 26 | 27 | Access Python from Javascript: 28 | 29 | ```JavaScript 30 | // hello world 31 | pyjs.eval("print('Hello, World!')") 32 | 33 | // eval a python expression and get the result 34 | const py_list = pyjs.eval("[i for i in range(10)]") 35 | 36 | /// access 37 | console.log(py_list.get(0)) // same as py_list[0] on the python side 38 | ``` 39 | 40 | ## Try it out 41 | 42 | To try it out, you can use [jupyterlite](../lite), 43 | the [JavaScript REPL](try_from_js) or the [Python REPL](try_from_py). -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Deploying pyjs 2 | 3 | ## Prerequisites: 4 | 5 | Before we start, lets introduce a few concepts and tools that are used in the pyjs workflow. 6 | ### Conda-forge Emscripten-Forge 7 | 8 | [Emscripten-forge](https://github.com/emscripten-forge/recipes) is similar to [conda-forge](https://conda-forge.org/) and provides packages compiled to webassembly using emscripten. 9 | 10 | ### Empack 11 | https://github.com/emscripten-forge/empack is a tool to "pack" conda environments into a set of files that can be consumed by pyjs. 12 | 13 | 14 | ## Installation Steps 15 | 16 | So we assume there is a directory called `/path/to/deploy` where we will 17 | put all tools which need to be served to the user. 18 | 19 | ### Define a conda environment 20 | Pyjs has a conda-like workflow. This means the first step 21 | is to create a environment with the `pyjs` package installed 22 | and all packages required for the project. 23 | 24 | ```yaml 25 | name: my-pyjs-env 26 | channels: 27 | - https://repo.prefix.dev/emscripten-forge-dev 28 | - https://repo.prefix.dev/conda-forge 29 | dependencies: 30 | - pyjs 31 | - numpy 32 | ``` 33 | 34 | The name of the environment can be choosen by the user. 35 | The `channels` section specifies the conda channels to use. 36 | The `https://repo.mamba.pm/emscripten-forge` is mandatory to install the `pyjs` package. 37 | The `conda-forge` channel is used to install `noarch`. 38 | All compiled packages need to be available in the `emscripten-forge` channel. 39 | 40 | ### Create the environment 41 | Assuming the yaml file above is called `environment.yml` and is in the current directory, the environment can be created using `micromamba`: 42 | 43 | ```Bash 44 | micromamba create -f environment.yml --platform emscripten-wasm32 --prefix /path/to/env 45 | ``` 46 | 47 | ### Copy pyjs 48 | Copy the pyjs binary from the environment to the deploy directory. 49 | This should move pyjs_runtime_browser.js and pyjs_runtime_browser.wasm to the deployment directory. 50 | 51 | ```Bash 52 | cp /path/to/env/lib_js/pyjs/* /path/to/deploy 53 | ``` 54 | 55 | 56 | 57 | ### Pack the environment 58 | 59 | After the environment is defined, the next step is to pack the environment using `empack`. 60 | 61 | ```Bash 62 | empack pack env --env-prefix /path/to/env --outdir /path/to/deploy 63 | ``` 64 | This will create a tarball for each package in the environment and a `empack_env_meta.json` file that describes the environment. 65 | 66 | 67 | ### The html/JavaScript code 68 | 69 | The last step is to create a html file that loads the pyjs runtime and the packed environment. 70 | 71 | ```html 72 | 73 | 74 | 75 | Pyjs Example 76 | 77 | 101 | 102 | 103 |

Pyjs Example

104 | 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/try_from_js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Try pyjs from JavaScript 4 | For a full fledged example have a look at the [jupyterlite-deployment](../lite). 5 | 6 | 23 | 24 | -------------------------------------------------------------------------------- /docs/try_from_py.md: -------------------------------------------------------------------------------- 1 | # Try pyjs from Python 2 | For a full fledged example have a look at the [jupyterlite-deployment](../lite). 3 | 8 | 9 | -------------------------------------------------------------------------------- /environment-dev.yml: -------------------------------------------------------------------------------- 1 | name: pyjs-wasm 2 | channels: 3 | - conda-forge 4 | - https://repo.prefix.dev/emscripten-forge-dev 5 | dependencies: 6 | - cmake 7 | - pip 8 | - python 9 | - yarn 10 | - click 11 | - microsoft::playwright <= 1.50 12 | - ninja 13 | - nodejs 14 | - pyjs_code_runner >= 2.0.1 15 | - exceptiongroup 16 | # documentation 17 | - jupytext 18 | - mkdocs 19 | - mkdocstrings 20 | - mkdocstrings-python 21 | - mkdocs-material 22 | - empack >=3.2.0 23 | - jupyter_server # to enable contents 24 | - jupyterlite 25 | - jupyterlite-xeus >= 3.1.8 26 | - jupyterlite-sphinx 27 | - notebook >=7,<8 # to include the extension to switch between JupyterLab and Notebook 28 | # pyjs_code_runner dev deps 29 | - hatchling 30 | - emscripten_emscripten-wasm32 31 | - pip: 32 | - JLDracula 33 | - pyjs_code_runner >= 3.0 34 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | Example Code 2 | ================== 3 | 4 | Below is a gallery of examples 5 | -------------------------------------------------------------------------------- /examples/js_api_tour.js: -------------------------------------------------------------------------------- 1 | // %% [markdown] 2 | // # pyjs JavaScript API Tour 3 | // Welcome to the tour of the pyjs JavaScript API. This notebook demonstrates how to use the PYJS JavaScript API to run Python code in the browser. 4 | 5 | // %% [markdown] 6 | // # Loading the pyjs module 7 | 8 | // %% [code] 9 | // load the pyjs runtime by importing the pyjs_runtime_browser.js file 10 | // the url differs depending on the deployment 11 | importScripts("../../../../xeus/bin/pyjs_runtime_browser.js"); 12 | 13 | // the locateFile function is used to locate the wasm file 14 | // which sits next to the pyjs_runtime_browser.js file 15 | // in thism deployment 16 | let locateFile = function(filename){ 17 | if(filename.endsWith('pyjs_runtime_browser.wasm')){ 18 | return `../../../../xeus/bin/pyjs_runtime_browser.wasm`; 19 | } 20 | }; 21 | 22 | // the createModule function in from the pyjs runtime 23 | // is used to create the pyjs module 24 | let pyjs = await createModule({locateFile:locateFile}); 25 | 26 | // load the python packages (includung the python standard library) 27 | // from the empack environment 28 | packages_json_url = "../../../../xeus/kernels/xpython/empack_env_meta.json" 29 | package_tarballs_root_url = "../../../../xeus/kernel_packages/" 30 | await pyjs.bootstrap_from_empack_packed_environment( 31 | packages_json_url, 32 | package_tarballs_root_url 33 | ); 34 | 35 | 36 | // %% [markdown] 37 | // # Evaluating Python expressions: 38 | // From now on, you can use the pyjs module to run python code. 39 | // Here we use "eval" to evaluate a python expression 40 | 41 | // %% [code] 42 | pyjs.eval("print('hello world')"); 43 | 44 | // %% [markdown] 45 | // # Executing Python code 46 | // Here we execute a python code block using "exec" function 47 | 48 | // %% [code] 49 | pyjs.exec(` 50 | import numpy 51 | print(numpy.random.rand(3)) 52 | `); 53 | 54 | // %% [markdown] 55 | // # Executing Python code and returning the last expression 56 | // Here we execute a python code block using "exec" function and return the last expression. 57 | 58 | 59 | // %% [code] 60 | let rand_arr = pyjs.exec_eval(` 61 | import numpy 62 | numpy.random.rand(2,4,3) 63 | `); 64 | rand_arr.delete() 65 | 66 | // %% [markdown] 67 | 68 | // # Using the pyobject class 69 | // When a python object is returned, it is wrapped in a pyobject class. 70 | // This class provides methods to interact with the python object. 71 | // Any created instance of the pyobject class needs to be deleted using the "delete" method. 72 | 73 | // %% [code] 74 | // create a numpy array with [0,1,2,3] as value 75 | let arr = pyjs.exec_eval(` 76 | import numpy 77 | numpy.arange(0,4) 78 | `); 79 | 80 | // get the shape 81 | let arr_shape = arr.shape 82 | 83 | // get the square function 84 | const square_func = pyjs.eval('numpy.square') 85 | 86 | // any function call / __call__ like operator on the python side 87 | // is called via "py_call" 88 | const res = square_func.py_call(arr) 89 | 90 | // print the result 91 | console.log(res) 92 | 93 | // delete all the created pyobjects 94 | res.delete() 95 | square_func.delete() 96 | arr_shape.delete() 97 | arr.delete() 98 | 99 | // %% [markdown] 100 | // # Type Conversion 101 | // pyjs provides methods to convert between JavaScript and Python types. 102 | // ## Explicit conversion 103 | 104 | // %% [code] 105 | // python list to javascript array 106 | const py_list = pyjs.eval("[1,2,3]") 107 | const js_arr = pyjs.to_js(py_list) 108 | py_list.delete() 109 | console.log(js_arr) 110 | 111 | // python dict to js map 112 | const py_dict = pyjs.eval("dict(a=1, b='fobar')") 113 | const js_map = pyjs.to_js(py_dict) 114 | py_dict.delete() 115 | 116 | // values 117 | console.log(Array.from(js_map.keys())) 118 | // keys 119 | console.log(Array.from(js_map.values())) 120 | 121 | // %% [markdown] 122 | // ## Implicit conversion 123 | // Fundamental types are automatically converted between Python and JavaScript. 124 | // This includes numbers, strings, booleans and null. 125 | 126 | // %% [code] 127 | // sum is a plain javascript number 128 | const sum = pyjs.eval("sum([i for i in range(0,101)])") 129 | sum 130 | 131 | // %% [code] 132 | // is_true is a plain javascript boolean 133 | const is_true = pyjs.eval("sum([i for i in range(0,101)]) == 5050") 134 | is_true 135 | 136 | // %% [code] 137 | // none will be undefined 138 | let none = pyjs.eval('None') 139 | console.log(none) 140 | 141 | // %% [markdown] 142 | // # Asynchronous execution 143 | // The pyjs module provides a way to run python code asynchronously using the "exec_async" function. 144 | 145 | // %% [code] 146 | const py_code = ` 147 | import asyncio 148 | await asyncio.sleep(2) 149 | sum([i for i in range(100)]) 150 | ` 151 | result = await pyjs.async_exec_eval(py_code) 152 | console.log(result); -------------------------------------------------------------------------------- /examples/py_api_tour.py: -------------------------------------------------------------------------------- 1 | # %% [markdown] 2 | # # A tour of the Python API 3 | 4 | 5 | # %% [code] 6 | import pyjs 7 | 8 | # %% [markdown] 9 | # # Accessing the the JavaScript global object 10 | # 11 | # The global object in javascript is accessible via `pyjs.js`. 12 | # Since this example runs **not** in the main thread, but only 13 | # in a worker thread, we can not acces the window object, but 14 | # only whats available in the workers global scope / globalThis. 15 | # We can for instance print the page origin like this: 16 | # 17 | 18 | # %% [code] 19 | pyjs.js.location.origin # equivalent to the javascript expression `location.origin` / `globalThis.location.origin` 20 | 21 | # %% [markdown] 22 | # # Create JavaScript functions on the fly 23 | 24 | # %% [code] 25 | # define the function 26 | js_function = pyjs.js.Function("a", "b", "return a + b") 27 | 28 | # %% [code] 29 | # call the function 30 | result = js_function(1, 2) 31 | result 32 | 33 | # %% [markdown] 34 | # # Type conversion 35 | # 36 | # Pyjs allows to convert between python and javascript types. 37 | # 38 | # ## Explicit conversion 39 | 40 | # %% [code] 41 | # convert a python list to a javascript array 42 | js_list = pyjs.js.eval("[1,2,3]") 43 | # pylist is a vanilla python list 44 | py_list = pyjs.to_py(js_list) 45 | py_list 46 | 47 | # %% [code] 48 | # convert a nested javascript object to a python dict 49 | js_nested_object = pyjs.js.Function("return{ foo:42,bar:[1,{a:1,b:2}]};")() 50 | py_dict = pyjs.to_py(js_nested_object) 51 | py_dict 52 | 53 | # %% [markdown] 54 | # ### Custom converters 55 | # 56 | # Pyjs allows to register custom converters for specific javascript classes. 57 | 58 | # %% [code] 59 | # Define JavaScript Rectangle class 60 | # and create an instance of it 61 | rectangle = pyjs.js.Function(""" 62 | class Rectangle { 63 | constructor(height, width) { 64 | this.height = height; 65 | this.width = width; 66 | } 67 | } 68 | return new Rectangle(10,20) 69 | """)() 70 | 71 | # A Python Rectangle class 72 | class Rectangle(object): 73 | def __init__(self, height, width): 74 | self.height = height 75 | self.width = width 76 | 77 | # the custom converter 78 | def rectangle_converter(js_val, depth, cache, converter_options): 79 | return Rectangle(js_val.height, js_val.width) 80 | 81 | # Register the custom converter 82 | pyjs.register_converter("Rectangle", rectangle_converter) 83 | 84 | # Convert the JavaScript Rectangle to a Python Rectangle 85 | r = pyjs.to_py(rectangle) 86 | assert isinstance(r, Rectangle) 87 | assert r.height == 10 88 | assert r.width == 20 89 | 90 | # %% [markdown] 91 | # ## Implicit conversion 92 | # ## Implicit conversion 93 | # Fundamental types are automatically converted between Javascript and Python. 94 | # This includes numbers, strings, booleans and undefined and null. 95 | 96 | # %% [code] 97 | # this will convert the javascript string to a python string 98 | origin = pyjs.js.location.origin 99 | assert isinstance(origin, str) 100 | 101 | # or results from a javascript function 102 | js_function = pyjs.js.Function("a", "b", "return a + b") 103 | result = js_function("hello", "world") 104 | assert isinstance(js_function("hello", "world"), str) 105 | assert isinstance(js_function(1, 2), int) 106 | assert isinstance(js_function(1.5, 2.0), float) 107 | assert isinstance(js_function(1.5, 2.5), int) # (!) -------------------------------------------------------------------------------- /include/pyjs/convert.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | namespace em = emscripten; 10 | 11 | namespace pyjs 12 | { 13 | 14 | 15 | struct TypedArrayBuffer{ 16 | 17 | 18 | TypedArrayBuffer( 19 | em::val js_array, const std::string & format_descriptor 20 | ); 21 | ~TypedArrayBuffer(); 22 | 23 | unsigned m_size; 24 | unsigned m_bytes_per_element; 25 | uint8_t * m_data; 26 | std::string m_format_descriptor; 27 | }; 28 | 29 | 30 | 31 | 32 | enum class JsType : char 33 | { 34 | JS_NULL = '0', 35 | JS_UNDEFINED = '1', 36 | JS_OBJECT = '2', 37 | JS_STR = '3', 38 | JS_INT = '4', 39 | JS_FLOAT = '5', 40 | JS_BOOL = '6', 41 | JS_FUNCTION = '7' 42 | }; 43 | 44 | std::pair implicit_py_to_js(py::object & py_ret); 45 | 46 | bool instanceof (em::val instance, const std::string& cls_name); 47 | 48 | 49 | 50 | 51 | 52 | 53 | TypedArrayBuffer* typed_array_to_buffer(em::val js_array); 54 | py::object implicit_js_to_py(em::val val); 55 | py::object implicit_js_to_py(em::val val, const std::string& type_string); 56 | 57 | 58 | em::val py_1d_buffer_to_typed_array(py::buffer buffer, bool view); 59 | em::val bytes_to_js(char* data); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /include/pyjs/export_js_module.hpp: -------------------------------------------------------------------------------- 1 | # pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace pyjs 7 | { 8 | 9 | 10 | namespace em = emscripten; 11 | 12 | int n_unfinished(); 13 | 14 | std::string run_pickled(const std::string& in); 15 | 16 | em::val eval(py::scoped_interpreter & ,const std::string & code, py::object & globals, py::object & locals); 17 | 18 | em::val exec(py::scoped_interpreter & ,const std::string & code, py::object & globals, py::object & locals); 19 | 20 | em::val eval_file(py::scoped_interpreter & ,const std::string & filename, py::object & globals, py::object & locals); 21 | 22 | 23 | void export_js_module(); 24 | } 25 | -------------------------------------------------------------------------------- /include/pyjs/export_js_proxy.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | 6 | namespace py = pybind11; 7 | 8 | namespace pyjs 9 | { 10 | void export_js_proxy(py::module_& m); 11 | } 12 | -------------------------------------------------------------------------------- /include/pyjs/export_py_object.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace pyjs 4 | { 5 | void export_py_object(); 6 | } 7 | -------------------------------------------------------------------------------- /include/pyjs/export_pyjs_module.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace py = pybind11; 5 | 6 | namespace pyjs 7 | { 8 | void export_pyjs_module(py::module_& pyjs_module); 9 | } 10 | -------------------------------------------------------------------------------- /include/pyjs/inflate.hpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "zlib.h" 4 | 5 | namespace pyjs{ 6 | 7 | /* Decompress from file source to file dest until stream ends or EOF. */ 8 | void inflate(gzFile_s *source, FILE *dest); 9 | 10 | } -------------------------------------------------------------------------------- /include/pyjs/install_conda_file.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pyjs 6 | { 7 | 8 | em::val install_conda_file(const std::string& zstd_file_path, 9 | const std::string& working_dir, 10 | const std::string& path); 11 | 12 | } -------------------------------------------------------------------------------- /include/pyjs/post_js/fixes.js: -------------------------------------------------------------------------------- 1 | if (!('wasmTable' in Module)) { 2 | Module['wasmTable'] = wasmTable 3 | } 4 | 5 | Module['FS'] = FS 6 | Module['PATH'] = PATH 7 | Module['LDSO'] = LDSO 8 | Module['getDylinkMetadata'] = getDylinkMetadata 9 | Module['loadDynamicLibrary'] = loadDynamicLibrary 10 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/apply.js: -------------------------------------------------------------------------------- 1 | 2 | function isPromise(p) { 3 | try{ 4 | if ( 5 | p !== null && 6 | typeof p === 'object' && 7 | typeof p.then === 'function' && 8 | typeof p.catch === 'function' 9 | ) { 10 | return true; 11 | } 12 | } catch (e) { 13 | } 14 | return false; 15 | } 16 | 17 | 18 | Module['_apply_try_catch'] = function(obj, args, is_generated_proxy) { 19 | try { 20 | let res = obj(...args); 21 | if(isPromise(res)){ 22 | res.then((value) => { 23 | for(let i=0; i pyobject.delete()) 7 | Module["_is_initialized"] = false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/constants.js: -------------------------------------------------------------------------------- 1 | const _NULL = "0" 2 | const _UNDEFINED = "1" 3 | const _OBJECT = "2" 4 | const _STR = "3" 5 | const _INT = "4" 6 | const _FLOAT = "5" 7 | const _BOOL = "6" 8 | const _FUNCTION = "7" 9 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/create_once_callable.js: -------------------------------------------------------------------------------- 1 | 2 | Module['_create_once_callable'] = function(py_object) { 3 | 4 | let already_called = false; 5 | 6 | var once_callable = function(...args) { 7 | if (already_called) { 8 | throw new Error("once_callable can only be called once"); 9 | } 10 | already_called = true; 11 | 12 | // make the call 13 | ret = py_object.py_call(...args); 14 | 15 | // delete 16 | py_object.delete() 17 | 18 | return ret; 19 | } 20 | return once_callable 21 | } 22 | 23 | Module['_create_once_callable_unsave_void_void'] = function(py_object) { 24 | 25 | let already_called = false; 26 | 27 | var once_callable = function() { 28 | if (already_called) { 29 | throw new Error("once_callable can only be called once"); 30 | } 31 | already_called = true; 32 | 33 | // make the call 34 | py_object.__usafe_void_void__(); 35 | 36 | // delete 37 | py_object.delete() 38 | } 39 | return once_callable 40 | } 41 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/dynload/LICENCE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emscripten-forge/pyjs/b7f3a08768a1116ca856a512bca231247e3a4d25/include/pyjs/pre_js/dynload/LICENCE -------------------------------------------------------------------------------- /include/pyjs/pre_js/dynload/README.md: -------------------------------------------------------------------------------- 1 | the file dynload has been taken from pyodide (https://github.com/pyodide/pyodide/blob/main/src/js/dynload.ts) and 2 | has been modified to fit pyjs 3 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/dynload/dynload.js: -------------------------------------------------------------------------------- 1 | const memoize = (fn) => { 2 | let cache = {}; 3 | return (...args) => { 4 | let n = args[0]; 5 | if (n in cache) { 6 | return cache[n]; 7 | } else { 8 | let result = fn(n); 9 | cache[n] = result; 10 | return result; 11 | } 12 | }; 13 | }; 14 | 15 | 16 | function createLock() { 17 | let _lock = Promise.resolve(); 18 | 19 | async function acquireLock() { 20 | const old_lock = _lock; 21 | let releaseLock = () => { }; 22 | _lock = new Promise((resolve) => (releaseLock = resolve)); 23 | await old_lock; 24 | return releaseLock; 25 | } 26 | return acquireLock; 27 | } 28 | 29 | function isInSharedLibraryPath(prefix, libPath){ 30 | if (libPath.startsWith("/")){ 31 | const dirname = libPath.substring(0, libPath.lastIndexOf("/")); 32 | if(prefix == "/"){ 33 | return (dirname == `/lib`); 34 | } 35 | else{ 36 | return (dirname == `${prefix}/lib`); 37 | } 38 | } 39 | else{ 40 | return false; 41 | } 42 | } 43 | 44 | 45 | async function loadDynlibsFromPackage( 46 | prefix, 47 | python_version, 48 | pkg_file_name, 49 | pkg_is_shared_library, 50 | dynlibPaths, 51 | ) { 52 | 53 | // for(const path of dynlibPaths){ 54 | // console.log(path); 55 | // } 56 | 57 | // assume that shared libraries of a package are located in .libs directory, 58 | // following the convention of auditwheel. 59 | if(prefix == "/"){ 60 | var sitepackages = `/lib/python${python_version[0]}.${python_version[1]}/site-packages` 61 | } 62 | else{ 63 | var sitepackages = `${prefix}/lib/python${python_version[0]}.${python_version[1]}/site-packages` 64 | } 65 | const auditWheelLibDir = `${sitepackages}/${ 66 | pkg_file_name.split("-")[0] 67 | }.libs`; 68 | 69 | // This prevents from reading large libraries multiple times. 70 | const readFileMemoized = memoize(Module.FS.readFile); 71 | 72 | const forceGlobal = !!pkg_is_shared_library; 73 | 74 | 75 | 76 | let dynlibs = []; 77 | 78 | if (forceGlobal) { 79 | dynlibs = dynlibPaths.map((path) => { 80 | return { 81 | path: path, 82 | global: true, 83 | }; 84 | }); 85 | } else { 86 | const globalLibs = calculateGlobalLibs( 87 | dynlibPaths, 88 | readFileMemoized, 89 | ); 90 | 91 | dynlibs = dynlibPaths.map((path) => { 92 | const global = globalLibs.has(Module.PATH.basename(path)); 93 | return { 94 | path: path, 95 | global: global || !! pkg_is_shared_library || isInSharedLibraryPath(prefix, path) || path.startsWith(auditWheelLibDir), 96 | }; 97 | }); 98 | } 99 | 100 | dynlibs.sort((lib1, lib2) => Number(lib2.global) - Number(lib1.global)); 101 | 102 | for (const { path, global } of dynlibs) { 103 | await loadDynlib(prefix, path, global, [auditWheelLibDir], readFileMemoized); 104 | } 105 | } 106 | 107 | function createDynlibFS( 108 | prefix, 109 | lib, 110 | searchDirs, 111 | readFileFunc 112 | ) { 113 | const dirname = lib.substring(0, lib.lastIndexOf("/")); 114 | 115 | let _searchDirs = searchDirs || []; 116 | 117 | if(prefix == "/"){ 118 | _searchDirs = _searchDirs.concat([dirname], [`/lib`]); 119 | } 120 | else{ 121 | _searchDirs = _searchDirs.concat([dirname], [`${prefix}/lib`]); 122 | } 123 | 124 | 125 | const resolvePath = (path) => { 126 | //console.log("resolvePath", path); 127 | 128 | if (Module.PATH.basename(path) !== Module.PATH.basename(lib)) { 129 | //console.debug(`Searching a library from ${path}, required by ${lib}`); 130 | } 131 | 132 | for (const dir of _searchDirs) { 133 | const fullPath = Module.PATH.join2(dir, path); 134 | //console.log("SERARCHING", fullPath); 135 | if (Module.FS.findObject(fullPath) !== null) { 136 | //console.log("FOUND", fullPath); 137 | return fullPath; 138 | } 139 | } 140 | return path; 141 | }; 142 | 143 | let readFile = (path) => 144 | Module.FS.readFile(resolvePath(path)); 145 | 146 | if (readFileFunc !== undefined) { 147 | readFile = (path) => readFileFunc(resolvePath(path)); 148 | } 149 | 150 | const fs = { 151 | findObject: (path, dontResolveLastLink) => { 152 | let obj = Module.FS.findObject(resolvePath(path), dontResolveLastLink); 153 | 154 | if (obj === null) { 155 | console.debug(`Failed to find a library: ${resolvePath(path)}`); 156 | } 157 | 158 | return obj; 159 | }, 160 | readFile: readFile, 161 | }; 162 | 163 | return fs; 164 | } 165 | 166 | 167 | function calculateGlobalLibs( 168 | libs, 169 | readFileFunc 170 | ) { 171 | let readFile = Module.FS.readFile; 172 | if (readFileFunc !== undefined) { 173 | readFile = readFileFunc; 174 | } 175 | 176 | const globalLibs = new Set(); 177 | 178 | libs.forEach((lib) => { 179 | const binary = readFile(lib); 180 | const needed = Module.getDylinkMetadata(binary).neededDynlibs; 181 | needed.forEach((lib) => { 182 | globalLibs.add(lib); 183 | }); 184 | }); 185 | 186 | return globalLibs; 187 | } 188 | 189 | 190 | // Emscripten has a lock in the corresponding code in library_browser.js. I 191 | // don't know why we need it, but quite possibly bad stuff will happen without 192 | // it. 193 | const acquireDynlibLock = createLock(); 194 | 195 | async function loadDynlib(prefix, lib, global, searchDirs, readFileFunc) { 196 | if (searchDirs === undefined) { 197 | searchDirs = []; 198 | } 199 | const releaseDynlibLock = await acquireDynlibLock(); 200 | 201 | try { 202 | const fs = createDynlibFS(prefix, lib, searchDirs, readFileFunc); 203 | 204 | const libName = Module.PATH.basename(lib); 205 | 206 | // contains cpython-3 and with wasm32-emscripten 207 | const is_cython_lib = libName.includes("cpython-3") && libName.includes("wasm32-emscripten"); 208 | 209 | // load cython library from full path 210 | const load_name = is_cython_lib ? lib : libName; 211 | 212 | await Module.loadDynamicLibrary(load_name, { 213 | loadAsync: true, 214 | nodelete: true, 215 | allowUndefined: true, 216 | global: global && !is_cython_lib, 217 | fs: fs 218 | }) 219 | 220 | const dsoOnlyLibName = Module.LDSO.loadedLibsByName[libName]; 221 | const dsoFullLib = Module.LDSO.loadedLibsByName[lib]; 222 | 223 | if(!dsoOnlyLibName && !dsoFullLib){ 224 | console.execption(`Failed to load ${libName} from ${lib} LDSO not found`); 225 | } 226 | 227 | if(!is_cython_lib){ 228 | if (!dsoOnlyLibName) { 229 | Module.LDSO.loadedLibsByName[libName] = dsoFullLib 230 | } 231 | 232 | if(!dsoFullLib){ 233 | Module.LDSO.loadedLibsByName[lib] = dsoOnlyLibName; 234 | } 235 | } 236 | } finally { 237 | releaseDynlibLock(); 238 | } 239 | } 240 | 241 | Module["_loadDynlibsFromPackage"] = loadDynlibsFromPackage; 242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/fetch.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | async function fetchByteArray(url){ 5 | let response = await fetch(url) 6 | if (!response.ok) { 7 | throw new Error(`HTTP error! status: ${response.status}`); 8 | } 9 | let arrayBuffer = await response.arrayBuffer() 10 | let byte_array = new Uint8Array(arrayBuffer) 11 | return byte_array 12 | } 13 | 14 | Module["_fetch_byte_array"] = fetchByteArray; 15 | 16 | 17 | async function fetchJson(url){ 18 | let response = await fetch(url) 19 | if (!response.ok) { 20 | throw new Error(`HTTP error! status: ${response.status}`); 21 | } 22 | let json = await response.json() 23 | return json 24 | } 25 | 26 | 27 | 28 | Module["_parallel_fetch_array_buffer"] = async function (urls){ 29 | let promises = urls.map(url => fetch(url).then(response => response.arrayBuffer())); 30 | return await Promise.all(promises); 31 | } 32 | 33 | Module["_parallel_fetch_arraybuffers_with_progress_bar"] = async function (urls, done_callback, progress_callback){ 34 | if(done_callback === undefined || done_callback === null){ 35 | done_callback = async function( 36 | index, byte_array 37 | ){}; 38 | } 39 | 40 | if(progress_callback===undefined || progress_callback===null) 41 | { 42 | 43 | let f = async function(index){ 44 | let res = await fetch(urls[index]); 45 | if (!res.ok) { 46 | throw new Error(`HTTP error! when fetching ${urls[index]} status: ${res.status}`); 47 | } 48 | const arrayBuffer = await res.arrayBuffer(); 49 | const byteArray = new Uint8Array(arrayBuffer); 50 | await done_callback(index, byteArray); 51 | return byteArray; 52 | } 53 | let futures = [] 54 | for(let i=0;i partialSum + a, 0); 99 | let recived = receivedArr.reduce((partialSum, a) => partialSum + a, 0); 100 | let n_finished = finishedArr.reduce((partialSum, a) => partialSum + a, 0); 101 | 102 | if(progress_callback !== undefined){ 103 | progress_callback(recived,total,n_finished, n_urls); 104 | } 105 | } 106 | 107 | function report_finished(index){ 108 | finishedArr[index] = 1; 109 | on_progress(); 110 | } 111 | 112 | function report_total_length(index, total){ 113 | totalArr[index] = total; 114 | on_progress(); 115 | } 116 | function report_progress(index, p){ 117 | receivedArr[index] = p; 118 | on_progress(); 119 | } 120 | 121 | let futures = urls.map((url, index) => { 122 | return fetch_arraybuffer_with_progress_bar(url,index, report_total_length,report_progress, report_finished) 123 | }) 124 | return await Promise.all(futures); 125 | } -------------------------------------------------------------------------------- /include/pyjs/pre_js/get_set_attr.js: -------------------------------------------------------------------------------- 1 | 2 | Module['_getattr_try_catch'] = function(obj, property_name) { 3 | try { 4 | let ret = obj[property_name] 5 | if (typeof ret === "function") { 6 | return _wrap_return_value(ret.bind(obj)) 7 | } else { 8 | return _wrap_return_value(ret) 9 | } 10 | } catch (e) { 11 | return _wrap_catched_error(e) 12 | } 13 | } 14 | Module['_setattr_try_catch'] = function(obj, property_name, value) { 15 | try { 16 | 17 | obj[property_name] = value 18 | return _wrap_void_result() 19 | } catch (e) { 20 | return _wrap_catched_error(e) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/get_type_string.js: -------------------------------------------------------------------------------- 1 | function _get_type_string(instance) { 2 | if (instance === null) { 3 | return _NULL 4 | } else if (instance === undefined) { 5 | return _UNDEFINED 6 | } else { 7 | const type = typeof instance; 8 | 9 | if (type === "object") { 10 | const constructor = instance.constructor; 11 | if (constructor !== undefined) { 12 | return constructor.name 13 | } 14 | return _OBJECT 15 | } else if (type === "string") { 16 | return _STR 17 | } else if (type === "number") { 18 | if (Number.isInteger(instance)) { 19 | return _INT 20 | } else { 21 | return _FLOAT 22 | } 23 | } else if (type === "boolean") { 24 | return _BOOL 25 | } else if (type === "function") { 26 | return _FUNCTION 27 | } else { 28 | console.log(instance, "is unhandled type") 29 | throw Error("internal error -- this should be unreachable") 30 | } 31 | } 32 | } 33 | 34 | Module['_get_type_string'] = _get_type_string 35 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/init.js: -------------------------------------------------------------------------------- 1 | Module._is_initialized = false 2 | 3 | 4 | 5 | Module['init_phase_1'] = async function(prefix, python_version, verbose) { 6 | 7 | if(verbose){console.log("in init phase 1");} 8 | let version_str = `${python_version[0]}.${python_version[1]}`; 9 | 10 | // list of python objects we need to delete when cleaning up 11 | let py_objects = [] 12 | Module._py_objects = py_objects 13 | 14 | // return empty promise when already initialized 15 | if(Module["_is_initialized"]) 16 | { 17 | return Promise.resolve(); 18 | } 19 | var p = await Module['_wait_run_dependencies'](); 20 | 21 | if(prefix == "/"){ 22 | Module.setenv("PYTHONHOME", `/`); 23 | Module.setenv("PYTHONPATH", `/lib/python${version_str}/site-packages:/usr/lib/python${version_str}`); 24 | 25 | var side_path = `/lib/python${version_str}/site-packages`; 26 | } 27 | else{ 28 | Module.setenv("PYTHONHOME", prefix); 29 | Module.setenv("PYTHONPATH", `${prefix}/lib/python${version_str}/site-packages:/usr/lib/python${version_str}`); 30 | var side_path = `${prefix}/lib/python${version_str}/site-packages`; 31 | } 32 | 33 | 34 | Module.create_directories(side_path); 35 | 36 | 37 | Module["_interpreter"] = new Module["_Interpreter"]() 38 | var default_scope = Module["main_scope"]() 39 | Module["default_scope"] = default_scope; 40 | 41 | Module['_py_objects'].push(Module["default_scope"]); 42 | Module['_py_objects'].push(Module["_interpreter"]); 43 | 44 | 45 | 46 | Module['exec'] = function(code, globals=default_scope, locals=default_scope) { 47 | let ret = Module._exec(code, globals, locals) 48 | if (ret.has_err) { 49 | throw ret 50 | } 51 | }; 52 | 53 | 54 | 55 | Module['eval'] = function(code, globals=default_scope, locals=default_scope) { 56 | let ret = Module._eval(code, globals, locals) 57 | if (ret.has_err) { 58 | throw ret 59 | } else { 60 | return ret['ret'] 61 | } 62 | }; 63 | 64 | Module['eval_file'] = function(file, globals=default_scope, locals=default_scope) { 65 | let ret = Module._eval_file(file, globals, locals) 66 | if (ret.has_err) { 67 | throw ret 68 | } 69 | }; 70 | 71 | Module['pyobject'].prototype._getattr = function(attr_name) { 72 | let ret = this._raw_getattr(attr_name) 73 | if (ret.has_err) { 74 | throw ret 75 | } else { 76 | return ret['ret'] 77 | } 78 | }; 79 | 80 | Module['pyobject'].prototype.py_call = function(...args) { 81 | return this.py_apply(args) 82 | }; 83 | 84 | Module['pyobject'].prototype.py_apply = function(args, kwargs) { 85 | 86 | if (args === undefined) { 87 | var args = [] 88 | var args_types = [] 89 | } 90 | else 91 | { 92 | var args_types = args.map(Module['_get_type_string']) 93 | } 94 | 95 | if (kwargs === undefined) { 96 | var kwargs = {} 97 | var kwargs_keys = [] 98 | var kwargs_values = [] 99 | var kwarg_values_types = [] 100 | } 101 | else 102 | { 103 | var kwargs_keys = Object.keys(kwargs) 104 | var kwargs_values = Object.values(kwargs) 105 | var kwarg_values_types = kwargs_values.map(Module['_get_type_string']) 106 | } 107 | 108 | let ret = this._raw_apply(args, args_types, args.length, 109 | kwargs_keys, kwargs_values, kwarg_values_types, kwargs_keys.length 110 | ) 111 | if (ret.has_err) { 112 | throw ret 113 | } else { 114 | return ret['ret'] 115 | } 116 | }; 117 | 118 | // [Symbol.toPrimitive](hint) for pyobject 119 | Module['pyobject'].prototype[Symbol.toPrimitive] = function(hint) { 120 | return this.__toPrimitive(); 121 | }; 122 | 123 | 124 | 125 | 126 | Module['pyobject'].prototype.get = function(...keys) { 127 | 128 | 129 | let types = keys.map(Module['_get_type_string']) 130 | let ret = this._raw_getitem(keys, types, keys.length) 131 | if (ret.has_err) { 132 | throw ret 133 | } else { 134 | return ret['ret'] 135 | } 136 | }; 137 | if(verbose){console.log("in init phase 2 done");} 138 | } 139 | 140 | Module['init_phase_2'] = function(prefix, python_version, verbose) { 141 | let default_scope = Module["default_scope"]; 142 | 143 | // make the python pyjs module easy available 144 | if(verbose){console.log("in init phase 2");} 145 | 146 | Module.exec(` 147 | import traceback 148 | try: 149 | import pyjs 150 | except Exception as e: 151 | print("ERROR",e) 152 | traceback.print_exc() 153 | raise e 154 | `) 155 | 156 | if(verbose){console.log("assign pyobjects I");} 157 | Module.py_pyjs = Module.eval("pyjs") 158 | Module._py_objects.push(Module.py_pyjs); 159 | 160 | 161 | // execute a script and return the value of the last expression 162 | if(verbose){console.log("assign pyobjects II");} 163 | Module._py_exec_eval = Module.eval("pyjs.exec_eval") 164 | Module._py_objects.push(Module._py_exec_eval) 165 | Module.exec_eval = function(script, globals=default_scope, locals=default_scope){ 166 | return Module._py_exec_eval.py_call(script, globals, locals) 167 | } 168 | 169 | // ansync execute a script and return the value of the last expression 170 | if(verbose){console.log("assign pyobjects III");} 171 | Module._py_async_exec_eval = Module.eval("pyjs.async_exec_eval") 172 | Module._py_objects.push(Module._py_async_exec_eval) 173 | Module.async_exec_eval = async function(script, globals=default_scope, locals=default_scope){ 174 | return await Module._py_async_exec_eval.py_call(script, globals, locals) 175 | } 176 | if(verbose){console.log("assign pyobjects IV");} 177 | Module._add_resolve_done_callback = Module.exec_eval(` 178 | import asyncio 179 | def _add_resolve_done_callback(future, resolve, reject): 180 | ensured_future = asyncio.ensure_future(future) 181 | def done(f): 182 | try: 183 | resolve(f.result()) 184 | except Exception as err: 185 | reject(repr(err)) 186 | 187 | ensured_future.add_done_callback(done) 188 | _add_resolve_done_callback 189 | `) 190 | Module._py_objects.push(Module._add_resolve_done_callback); 191 | 192 | 193 | 194 | Module._py_to_js = Module.eval("pyjs.to_js") 195 | Module._py_objects.push(Module._py_to_js); 196 | 197 | Module["to_js"] = function(obj){ 198 | return Module._py_to_js.py_call(obj) 199 | } 200 | 201 | Module._is_initialized = true; 202 | 203 | // Mock some system libraries 204 | Module.exec(` 205 | import sys 206 | import types 207 | import time 208 | 209 | sys.modules["fcntl"] = types.ModuleType("fcntl") 210 | sys.modules["pexpect"] = types.ModuleType("pexpect") 211 | sys.modules["resource"] = types.ModuleType("resource") 212 | 213 | def _mock_time_sleep(): 214 | def sleep(seconds): 215 | """Delay execution for a given number of seconds. The argument may be 216 | a floating point number for subsecond precision. 217 | """ 218 | start = now = time.time() 219 | while now - start < seconds: 220 | now = time.time() 221 | 222 | time.sleep = sleep 223 | _mock_time_sleep() 224 | del _mock_time_sleep 225 | 226 | def _mock_termios(): 227 | termios_mock = types.ModuleType("termios") 228 | termios_mock.TCSAFLUSH = 2 229 | sys.modules["termios"] = termios_mock 230 | _mock_termios() 231 | del _mock_termios 232 | 233 | def _mock_webbrowser(): 234 | def open(url, new=0, autoraise=True): 235 | pass 236 | def open_new(url): 237 | return open(url, 1) 238 | def open_new_tab(url): 239 | return open(url, 2) 240 | 241 | webbrowser_mock = types.ModuleType("webbrowser") 242 | webbrowser_mock.open = open 243 | webbrowser_mock.open_new = open_new 244 | webbrowser_mock.open_new_tab = open_new_tab 245 | 246 | sys.modules["webbrowser"] = webbrowser_mock 247 | _mock_webbrowser() 248 | del _mock_webbrowser 249 | `); 250 | if(verbose){console.log("init phase 2 done");} 251 | } 252 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/load_pkg.js: -------------------------------------------------------------------------------- 1 | Module["mkdirs"] = function (dirname) { 2 | // get all partent directories 3 | let parent_dirs = [] 4 | let parent_dir = dirname 5 | while (parent_dir != "") { 6 | parent_dirs.push(parent_dir) 7 | parent_dir = parent_dir.split("/").slice(0, -1).join("/") 8 | } 9 | console.log(parent_dirs) 10 | // make directories 11 | parent_dirs = parent_dirs.reverse() 12 | for (let parent_dir of parent_dirs) { 13 | if (!Module.FS.isDir(parent_dir)) { 14 | Module.FS.mkdir(parent_dir) 15 | } 16 | } 17 | } 18 | 19 | 20 | 21 | Module["_untar_from_python"] = function(tarball_path, target_dir = "") { 22 | Module.exec(` 23 | def _py_untar(tarball_path, target_dir): 24 | import tarfile 25 | import json 26 | from pathlib import Path 27 | import tempfile 28 | import shutil 29 | import os 30 | import sys 31 | 32 | 33 | def check_wasm_magic_number(file_path: Path) -> bool: 34 | WASM_BINARY_MAGIC = b"\\0asm" 35 | with file_path.open(mode="rb") as file: 36 | return file.read(4) == WASM_BINARY_MAGIC 37 | 38 | 39 | target_dir = target_dir 40 | if target_dir == "": 41 | target_dir = sys.prefix 42 | try: 43 | with tarfile.open(tarball_path) as tar: 44 | files = tar.getmembers() 45 | shared_libs = [] 46 | for file in files: 47 | if file.name.endswith(".so") or ".so." in file.name: 48 | if target_dir == "/": 49 | shared_libs.append(f"/{file.name}") 50 | else: 51 | shared_libs.append(f"{target_dir}/{file.name}") 52 | 53 | tar.extractall(target_dir) 54 | actual_shared_libs = [] 55 | for file in shared_libs: 56 | if not check_wasm_magic_number(Path(file)): 57 | print(f" {file} is not a wasm file") 58 | else: 59 | actual_shared_libs.append(file) 60 | s = json.dumps(actual_shared_libs) 61 | except Exception as e: 62 | print("ERROR",e) 63 | raise e 64 | return s 65 | `) 66 | let shared_libs = Module.eval(`_py_untar("${tarball_path}", "${target_dir}")`) 67 | 68 | return JSON.parse(shared_libs) 69 | } 70 | 71 | 72 | Module["_unzip_from_python"] = function(tarball_path, target_dir) { 73 | Module.exec(` 74 | def _py_unzip(tarball_path, target_dir): 75 | import json 76 | from pathlib import Path 77 | import zipfile 78 | 79 | target = Path(target_dir) 80 | target.mkdir(parents=True, exist_ok=True) 81 | pkg_file = {"name": "", "path": ""} 82 | with zipfile.ZipFile(tarball_path, mode="r") as archive: 83 | 84 | for filename in archive.namelist(): 85 | if filename.startswith("pkg-"): 86 | pkg_file["name"] = filename 87 | pkg_file["path"] = str(target / filename) 88 | archive.extract(filename, target_dir) 89 | break 90 | return json.dumps(pkg_file) 91 | 92 | `) 93 | let extracted_file = Module.eval(`_py_unzip("${tarball_path}", "${target_dir}")`) 94 | 95 | return JSON.parse(extracted_file) 96 | } 97 | 98 | Module["_install_conda_file_from_python"] = function(tarball_path, target_dir) { 99 | Module.exec(` 100 | def _py_unbz2(tarball_path, target_dir): 101 | import json 102 | from pathlib import Path 103 | import tarfile 104 | import shutil 105 | import os 106 | import sys 107 | 108 | target = Path(target_dir) 109 | prefix = Path(sys.prefix) 110 | try: 111 | with tarfile.open(tarball_path) as tar: 112 | tar.extractall(target_dir) 113 | 114 | src = target / "site-packages" 115 | dest = prefix / "lib/python3.11/site-packages" 116 | shutil.copytree(src, dest, dirs_exist_ok=True) 117 | for folder in ["etc", "share"]: 118 | src = target / folder 119 | dest = prefix / folder 120 | if src.exists(): 121 | shutil.copytree(src, dest, dirs_exist_ok=True) 122 | shutil.rmtree(target) 123 | except Exception as e: 124 | print("ERROR",e) 125 | raise e 126 | 127 | return json.dumps([]) 128 | 129 | `) 130 | let extracted_file = Module.eval(`_py_unbz2("${tarball_path}", "${target_dir}")`) 131 | 132 | return JSON.parse(extracted_file) 133 | } 134 | 135 | 136 | 137 | 138 | 139 | Module["bootstrap_from_empack_packed_environment"] = async function 140 | ( 141 | packages_json_url, 142 | package_tarballs_root_url, 143 | verbose = true, 144 | skip_loading_shared_libs = false 145 | ) 146 | { 147 | try{ 148 | 149 | function splitPackages(packages) { 150 | // find package with name "python" and remove it from the list 151 | let python_package = undefined 152 | for (let i = 0; i < packages.length; i++) { 153 | if (packages[i].name == "python") { 154 | python_package = packages[i] 155 | packages.splice(i, 1) 156 | break 157 | } 158 | } 159 | if (python_package == undefined) { 160 | throw new Error("no python package found in package.json") 161 | } 162 | return { python_package, packages } 163 | } 164 | 165 | 166 | 167 | async function fetchAndUntar 168 | ( 169 | package_tarballs_root_url, 170 | python_is_ready_promise, 171 | pkg, 172 | verbose 173 | ) { 174 | const package_url = 175 | pkg?.url ?? `${package_tarballs_root_url}/${pkg.filename}`; 176 | if (verbose) { 177 | console.log(`!!fetching pkg ${pkg.name} from ${package_url}`); 178 | } 179 | let byte_array = await fetchByteArray(package_url); 180 | const tarball_path = `/package_tarballs/${pkg.filename}`; 181 | Module.FS.writeFile(tarball_path, byte_array); 182 | if (verbose) { 183 | console.log( 184 | `!!extract ${tarball_path} (${byte_array.length} bytes)` 185 | ); 186 | } 187 | 188 | if (verbose) { 189 | console.log("await python_is_ready_promise"); 190 | } 191 | await python_is_ready_promise; 192 | 193 | if (package_url.toLowerCase().endsWith(".conda")) { 194 | // Conda v2 packages 195 | if (verbose) { 196 | console.log( 197 | `!!extract conda package ${package_url} (${byte_array.length} bytes)` 198 | ); 199 | } 200 | const dest = `/conda_packages/${pkg.name}`; 201 | const pkg_file = Module["_unzip_from_python"]( 202 | tarball_path, 203 | dest 204 | ); 205 | return Module._install_conda_file(pkg_file.path, dest, prefix); 206 | } else if (package_url.toLowerCase().endsWith(".tar.bz2")) { 207 | // Conda v1 packages 208 | if (verbose) { 209 | console.log( 210 | `!!extract conda package ${package_url} (${byte_array.length} bytes)` 211 | ); 212 | } 213 | const dest = `/conda_packages/${pkg.name}`; 214 | return Module["_install_conda_file_from_python"]( 215 | tarball_path, 216 | dest 217 | ); 218 | } else { 219 | // Pre-relocated packages 220 | return Module["_untar_from_python"](tarball_path); 221 | } 222 | } 223 | 224 | 225 | async function bootstrap_python(prefix, package_tarballs_root_url, python_package, verbose) { 226 | // fetch python package 227 | const python_package_url = python_package?.url ?? `${package_tarballs_root_url}/${python_package.filename}`; 228 | 229 | if (verbose) { 230 | console.log(`fetching python package from ${python_package_url}`) 231 | } 232 | let byte_array = await fetchByteArray(python_package_url) 233 | 234 | const python_tarball_path = `/package_tarballs/${python_package.filename}`; 235 | if(verbose){ 236 | console.log(`extract ${python_tarball_path} (${byte_array.length} bytes)`) 237 | } 238 | Module.FS.writeFile(python_tarball_path, byte_array); 239 | if(verbose){console.log("untar_from_python");} 240 | Module._untar(python_tarball_path, prefix); 241 | 242 | 243 | 244 | 245 | 246 | // split version string into major and minor and patch version 247 | let version = python_package.version.split(".").map(x => parseInt(x)); 248 | 249 | 250 | if(verbose){console.log("start init_phase_1");} 251 | await Module.init_phase_1(prefix, version, verbose); 252 | } 253 | 254 | 255 | 256 | if(verbose){ 257 | console.log("fetching packages.json from", packages_json_url) 258 | } 259 | 260 | // fetch json with list of all packages 261 | let empack_env_meta = await fetchJson(packages_json_url); 262 | let all_packages = empack_env_meta.packages; 263 | let prefix = empack_env_meta.prefix; 264 | 265 | if(verbose){ 266 | console.log("makeDirs"); 267 | } 268 | Module.create_directories("/package_tarballs"); 269 | 270 | // enusre there is python and split it from the rest 271 | if(verbose){console.log("splitPackages");} 272 | let splitted = splitPackages(all_packages); 273 | let packages = splitted.packages; 274 | let python_package = splitted.python_package; 275 | let python_version = python_package.version.split(".").map(x => parseInt(x)); 276 | 277 | // fetch init python itself 278 | console.log("--bootstrap_python"); 279 | if(verbose){ 280 | console.log("bootstrap_python"); 281 | } 282 | let python_is_ready_promise = bootstrap_python(prefix, package_tarballs_root_url, python_package, verbose); 283 | 284 | // create array with size 285 | if(verbose){ 286 | console.log("fetchAndUntarAll"); 287 | } 288 | let shared_libs = await Promise.all(packages.map(pkg => fetchAndUntar(package_tarballs_root_url, python_is_ready_promise, pkg, verbose))); 289 | 290 | if(verbose){ 291 | console.log("init_phase_2"); 292 | } 293 | Module.init_phase_2(prefix, python_version, verbose); 294 | 295 | if(verbose){ 296 | console.log("init shared"); 297 | } 298 | if(!skip_loading_shared_libs){ 299 | // instantiate all packages 300 | for (let i = 0; i < packages.length; i++) { 301 | 302 | // if we have any shared libraries, load them 303 | if (shared_libs[i].length > 0) { 304 | 305 | for (let j = 0; j < shared_libs[i].length; j++) { 306 | let sl = shared_libs[i][j]; 307 | } 308 | await Module._loadDynlibsFromPackage( 309 | prefix, 310 | python_version, 311 | packages[i].name, 312 | false, 313 | shared_libs[i] 314 | ) 315 | } 316 | } 317 | } 318 | if(verbose){ 319 | console.log("done bootstrapping");} 320 | } 321 | catch(e){ 322 | console.log("error in bootstrapping process") 323 | console.error(e); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/make_proxy.js: -------------------------------------------------------------------------------- 1 | Module['make_proxy'] = function(py_object) { 2 | const handler = { 3 | get(target, property, receiver) { 4 | var ret = target[property] 5 | if (ret !== undefined) { 6 | return ret 7 | } 8 | return target._getattr(property); 9 | } 10 | }; 11 | 12 | return new Proxy(py_object, handler); 13 | } 14 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/operators.js: -------------------------------------------------------------------------------- 1 | Module['_async_import_javascript'] = async function(import_str){ 2 | return import(import_str); 3 | } 4 | 5 | Module["__eq__"] = function(a, b) { 6 | return a === b; 7 | } 8 | 9 | Module['_new'] = function(cls, ...args) { 10 | return new cls(...args); 11 | } 12 | 13 | Module['_instanceof'] = function(instance, cls) { 14 | return (instance instanceof cls); 15 | } 16 | 17 | Module["_typeof"] = function(x) { 18 | return typeof x; 19 | } 20 | 21 | Module["_delete"] = function(x, key) { 22 | delete x[key]; 23 | } 24 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/platform.js: -------------------------------------------------------------------------------- 1 | 2 | Module['_IS_NODE'] = (typeof process === "object" && typeof require === "function") 3 | 4 | Module['_IS_BROWSER_WORKER_THREAD'] = (typeof importScripts === "function") 5 | 6 | Module['_IS_BROWSER_MAIN_THREAD'] = (typeof window === "object") 7 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/promise.js: -------------------------------------------------------------------------------- 1 | 2 | Module['_set_promise_then_catch'] = function(promise, py_object_then, py_object_catch) { 3 | 4 | let already_called = false; 5 | 6 | var callable_then = function(v) { 7 | 8 | py_object_then.__usafe_void_val__(v); 9 | 10 | // delete 11 | py_object_then.delete() 12 | py_object_catch.delete() 13 | } 14 | var callable_catch = function(err) { 15 | 16 | str_err = JSON.stringify(err, Object.getOwnPropertyNames(err)) 17 | py_object_catch.__usafe_void_val__(str_err); 18 | 19 | // delete 20 | py_object_then.delete() 21 | py_object_catch.delete() 22 | } 23 | promise.then(callable_then).catch(callable_catch) 24 | } 25 | 26 | Module['_future_to_promise'] = function(py_future){ 27 | let p = new Promise(function(resolve, reject) { 28 | Module._add_resolve_done_callback.py_call(py_future, resolve, reject) 29 | }); 30 | 31 | p.then(function(value) { 32 | py_future.delete() 33 | }, function(reason) { 34 | py_future.delete() 35 | }); 36 | return p; 37 | } -------------------------------------------------------------------------------- /include/pyjs/pre_js/shortcuts.js: -------------------------------------------------------------------------------- 1 | Module['_is_null'] = function(value) { 2 | return value === null; 3 | } 4 | 5 | Module['_is_undefined'] = function(value) { 6 | return value === undefined; 7 | } 8 | 9 | Module['_is_undefined_or_null'] = function(value) { 10 | return value === undefined || value === null; 11 | } 12 | 13 | Module["__len__"] = function(instance) { 14 | return instance.length || instance.size 15 | } 16 | 17 | Module["__contains__"] = function(instance, query) { 18 | let _has = false; 19 | let _includes = false; 20 | try { 21 | _has = instance.has(query); 22 | } catch (e) {} 23 | try { 24 | _has = instance.includes(query); 25 | } catch (e) {} 26 | return _has || _includes; 27 | } 28 | 29 | Module['_dir'] = function dir(x) { 30 | let result = []; 31 | do { 32 | result.push(...Object.getOwnPropertyNames(x)); 33 | } while ((x = Object.getPrototypeOf(x))); 34 | return result; 35 | } 36 | 37 | Module['_iter'] = function dir(x) { 38 | return x[Symbol.iterator]() 39 | } 40 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/wait_for_dependencies.js: -------------------------------------------------------------------------------- 1 | Module['_wait_run_dependencies'] = function() { 2 | const promise = new Promise((r) => { 3 | Module.monitorRunDependencies = (n) => { 4 | if (n === 0) { 5 | r(); 6 | } 7 | }; 8 | }); 9 | Module.addRunDependency("dummy"); 10 | Module.removeRunDependency("dummy"); 11 | return promise; 12 | } 13 | -------------------------------------------------------------------------------- /include/pyjs/pre_js/wrap_result.js: -------------------------------------------------------------------------------- 1 | 2 | function _wrap_void_result() { 3 | return { 4 | has_err: false, 5 | has_ret: false 6 | } 7 | } 8 | 9 | function _wrap_return_value(raw_ret) { 10 | const is_none = (raw_ret === undefined || raw_ret === null); 11 | let wret = { 12 | ret: raw_ret, 13 | has_err: false, 14 | has_ret: !is_none, 15 | type_string: _get_type_string(raw_ret) 16 | } 17 | return wret; 18 | } 19 | 20 | 21 | function _wrap_jreturn_value(raw_ret) { 22 | if (raw_ret === undefined) { 23 | return { 24 | ret: "", 25 | has_err: false 26 | } 27 | } else { 28 | return { 29 | ret: JSON.stringify(raw_ret), 30 | has_err: false 31 | } 32 | } 33 | } 34 | 35 | 36 | function _wrap_catched_error(err) { 37 | return { 38 | err: err, 39 | has_err: true, 40 | has_ret: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /include/pyjs/untar.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace em = emscripten; 7 | namespace pyjs{ 8 | em::val untar(const std::string &tar_path, const std::string &path); 9 | void untar_impl(FILE *a, const char *path, em::val & shared_libraraies); 10 | } -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pyjs docs 2 | theme: 3 | name: material 4 | # name: dracula 5 | 6 | palette: 7 | 8 | # Palette toggle for automatic mode 9 | - media: "(prefers-color-scheme)" 10 | toggle: 11 | icon: material/brightness-auto 12 | name: Switch to light mode 13 | 14 | # Palette toggle for light mode 15 | - media: "(prefers-color-scheme: light)" 16 | scheme: default 17 | toggle: 18 | icon: material/brightness-7 19 | name: Switch to dark mode 20 | 21 | # Palette toggle for dark mode 22 | - media: "(prefers-color-scheme: dark)" 23 | scheme: slate 24 | toggle: 25 | icon: material/brightness-4 26 | name: Switch to system preference 27 | 28 | 29 | plugins: 30 | - autorefs 31 | - mkdocstrings: 32 | handlers: 33 | python: 34 | options: 35 | allow_inspection: true 36 | show_root_heading: true 37 | 38 | 39 | markdown_extensions: 40 | - pymdownx.highlight: 41 | anchor_linenums: true 42 | line_spans: __span 43 | pygments_lang_class: true 44 | - pymdownx.inlinehilite 45 | - pymdownx.snippets 46 | - pymdownx.superfences 47 | -------------------------------------------------------------------------------- /module/pyjs/__init__.py: -------------------------------------------------------------------------------- 1 | import pyjs_core 2 | from pyjs_core import * 3 | from pyjs_core import JsValue 4 | 5 | from . core import * 6 | from . extend_js_val import * 7 | from . error_handling import * 8 | from . convert import * 9 | from . convert_py_to_js import * 10 | from . webloop import * 11 | from . pyodide_polyfill import * 12 | 13 | _in_browser_js = internal.module_property("_IS_NODE") 14 | IN_BROWSER = not to_py(_in_browser_js) 15 | 16 | 17 | js = sys.modules["pyjs_core.js"] 18 | _module = sys.modules["pyjs_core._module"] 19 | 20 | -------------------------------------------------------------------------------- /module/pyjs/convert.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import json 4 | from pyjs_core import internal, js,JsValue 5 | from .core import new 6 | from .error_handling import JsError, JsGenericError, JsInternalError, JsRangeError, JsReferenceError, JsSyntaxError, JsTypeError, JsURIError 7 | class _JsToPyConverterCache(object): 8 | def __init__(self): 9 | self._js_obj_to_int = js.WeakMap.new() 10 | self._int_to_py_obj = dict() 11 | self._counter = 0 12 | 13 | def __setitem__(self, js_val, py_val): 14 | c = self._counter 15 | self._js_obj_to_int.set(js_val, c) 16 | self._int_to_py_obj[c] = py_val 17 | self._counter = c + 1 18 | 19 | def __getitem__(self, js_val): 20 | if (key := self._js_obj_to_int.get(js_val)) is not None: 21 | return self._int_to_py_obj[key] 22 | else: 23 | return None 24 | 25 | def get(self, js_val, default_py): 26 | if (key := self._js_obj_to_int.get(js_val)) is not None: 27 | return self._int_to_py_obj[key], True 28 | else: 29 | self[js_val] = default_py 30 | return default_py, False 31 | 32 | def _array_converter(js_val, depth, cache, converter_options): 33 | py_list, found_in_cache = cache.get(js_val, []) 34 | if found_in_cache: 35 | return py_list 36 | 37 | size = internal.length(js_val) 38 | for i in range(size): 39 | # js_item = internal.__getitem__(js_val, i) 40 | js_item = js_val[i] 41 | py_item = to_py( 42 | js_item, depth=depth + 1, cache=cache, converter_options=converter_options 43 | ) 44 | py_list.append(py_item) 45 | return py_list 46 | 47 | def _object_converter(js_val, depth, cache, converter_options): 48 | 49 | ret_dict, found_in_cache = cache.get(js_val, {}) 50 | if found_in_cache: 51 | return ret_dict 52 | 53 | keys = internal.object_keys(js_val) 54 | values = internal.object_values(js_val) 55 | size = internal.length(keys) 56 | 57 | for i in range(size): 58 | 59 | # # todo, keys are always strings, this allows for optimization 60 | py_key = keys[i] 61 | js_val = values[i] 62 | 63 | py_val = to_py( 64 | js_val, depth=depth + 1, cache=cache, converter_options=converter_options 65 | ) 66 | 67 | ret_dict[py_key] = py_val 68 | 69 | return ret_dict 70 | 71 | def _map_converter(js_val, depth, cache, converter_options): 72 | 73 | ret_dict, found_in_cache = cache.get(js_val, {}) 74 | if found_in_cache: 75 | return ret_dict 76 | 77 | keys = js.Array["from"](js_val.keys()) 78 | values = js.Array["from"](js_val.values()) 79 | size = internal.length(keys) 80 | 81 | for i in range(size): 82 | 83 | js_key = keys[i] 84 | js_val = values[i] 85 | 86 | py_val = to_py( 87 | js_val, depth=depth + 1, cache=cache, converter_options=converter_options 88 | ) 89 | 90 | py_key = to_py( 91 | js_key, depth=depth + 1, cache=cache, converter_options=converter_options 92 | ) 93 | 94 | ret_dict[py_key] = py_val 95 | 96 | return ret_dict 97 | 98 | 99 | def _set_converter(js_val, depth, cache, converter_options): 100 | pyset, found_in_cache = cache.get(js_val, set()) 101 | if found_in_cache: 102 | return pyset 103 | 104 | for v in js_val: 105 | pyset.add( 106 | to_py(v, depth=depth + 1, cache=cache, converter_options=converter_options) 107 | ) 108 | return pyset 109 | 110 | 111 | def _error_converter(js_val, depth, cache, converter_options, error_cls): 112 | return error_cls(err=js_val) 113 | 114 | _error_to_py_converters = dict( 115 | Error=functools.partial(_error_converter, error_cls=JsError), 116 | InternalError=functools.partial(_error_converter, error_cls=JsInternalError), 117 | RangeError=functools.partial(_error_converter, error_cls=JsRangeError), 118 | ReferenceError=functools.partial(_error_converter, error_cls=JsReferenceError), 119 | SyntaxError=functools.partial(_error_converter, error_cls=JsSyntaxError), 120 | TypeError=functools.partial(_error_converter, error_cls=JsTypeError), 121 | URIError=functools.partial(_error_converter, error_cls=JsURIError), 122 | ) 123 | # register converters 124 | _basic_to_py_converters = { 125 | "0": lambda x: None, 126 | "1": lambda x, d, c, opts: None, 127 | "3": lambda x, d, c, opts: internal.as_string(x), 128 | "6": lambda x, d, c, opts: internal.as_boolean(x), 129 | "4": lambda x, d, c, opts: internal.as_int(x), 130 | "5": lambda x, d, c, opts: internal.as_float(x), 131 | "pyobject": lambda x, d, c, opts: internal.as_py_object(x), 132 | "2": _object_converter, 133 | "Object": _object_converter, 134 | "Array": _array_converter, 135 | "Set": _set_converter, 136 | "Map": _map_converter, 137 | "7": lambda x, d, c, opts: x, 138 | "Promise": lambda x, d, c, opts: x._to_future(), 139 | "ArrayBuffer": lambda x, d, c, opts: to_py(new(js.Uint8Array, x), d, c, opts), 140 | "Uint8Array": lambda x, d, c, opts: internal.as_buffer(x), 141 | "Int8Array": lambda x, d, c, opts: internal.as_buffer(x), 142 | "Uint16Array": lambda x, d, c, opts: internal.as_buffer(x), 143 | "Int16Array": lambda x, d, c, opts: internal.as_buffer(x), 144 | "Uint32Array": lambda x, d, c, opts: internal.as_buffer(x), 145 | "Int32Array": lambda x, d, c, opts: internal.as_buffer(x), 146 | "Float32Array": lambda x, d, c, opts: internal.as_buffer(x), 147 | "Float64Array": lambda x, d, c, opts: internal.as_buffer(x), 148 | "BigInt64Array": lambda x, d, c, opts: internal.as_buffer(x), 149 | "BigUint64Array": lambda x, d, c, opts: internal.as_buffer(x), 150 | "Uint8ClampedArray": lambda x, d, c, opts: internal.as_buffer(x), 151 | } 152 | _basic_to_py_converters = {**_basic_to_py_converters, **_error_to_py_converters} 153 | 154 | def register_converter(cls_name : str, converter : callable): 155 | ''' 156 | Register a custom JavaScript to Python converter. 157 | 158 | Args: 159 | cls_name: The name of the JavaScript class to convert. 160 | converter: A function that takes a JavaScript object and returns a Python object. 161 | 162 | Example: 163 | For this example we define the JavaScript class Rectangle on the fly 164 | and create an instance of it. We then register a custom converter for the 165 | Rectangle class and convert the instance to a Python object. 166 | 167 | ```python 168 | 169 | 170 | # Define JavaScript Rectangle class 171 | # and create an instance of it 172 | rectangle = pyjs.js.Function(""" 173 | class Rectangle { 174 | constructor(height, width) { 175 | this.height = height; 176 | this.width = width; 177 | } 178 | } 179 | return new Rectangle(10,20) 180 | """)() 181 | 182 | # A Python Rectangle class 183 | class Rectangle(object): 184 | def __init__(self, height, width): 185 | self.height = height 186 | self.width = width 187 | 188 | # the custom converter 189 | def rectangle_converter(js_val, depth, cache, converter_options): 190 | return Rectangle(js_val.height, js_val.width) 191 | 192 | # Register the custom converter 193 | pyjs.register_converter("Rectangle", rectangle_converter) 194 | 195 | # Convert the JavaScript Rectangle to a Python Rectangle 196 | r = pyjs.to_py(rectangle) 197 | assert isinstance(r, Rectangle) 198 | assert r.height == 10 199 | assert r.width == 20 200 | 201 | ``` 202 | 203 | ''' 204 | 205 | 206 | _basic_to_py_converters[cls_name] = converter 207 | 208 | 209 | def to_py_json(js_val : JsValue): 210 | """ Convert a JavaScript object to a Python object using JSON serialization.""" 211 | return json.loads(JSON.stringify(js_val)) 212 | 213 | class JsToPyConverterOptions(object): 214 | def __init__(self, json=False, converters=None, default_converter=None): 215 | self.json = json 216 | 217 | if converters is None: 218 | converters = _basic_to_py_converters 219 | if default_converter is None: 220 | default_converter = _basic_to_py_converters["Object"] 221 | 222 | self.converters = converters 223 | self.default_converter = default_converter 224 | 225 | 226 | def to_py(js_val, depth=0, cache=None, converter_options=None): 227 | if not isinstance(js_val, JsValue): 228 | return js_val 229 | if converter_options is None: 230 | converter_options = JsToPyConverterOptions() 231 | if cache is None: 232 | cache = _JsToPyConverterCache() 233 | converters = converter_options.converters 234 | default_converter = converter_options.default_converter 235 | ts = internal.get_type_string(js_val) 236 | return converters.get(ts, default_converter)( 237 | js_val, depth, cache, converter_options 238 | ) 239 | 240 | 241 | def error_to_py(err): 242 | default_converter = functools.partial(_error_converter, error_cls=JsGenericError) 243 | converter_options = JsToPyConverterOptions( 244 | converters=_error_to_py_converters, default_converter=default_converter 245 | ) 246 | return to_py(err, converter_options=converter_options) 247 | 248 | 249 | def error_to_py_and_raise(err): 250 | raise error_to_py(err) 251 | 252 | 253 | def buffer_to_js_typed_array(buffer, view=False): 254 | return internal.py_1d_buffer_to_typed_array(buffer, bool(view)) 255 | -------------------------------------------------------------------------------- /module/pyjs/convert_py_to_js.py: -------------------------------------------------------------------------------- 1 | 2 | from .core import new 3 | from pyjs_core import JsValue, js_array, js, js_undefined,internal 4 | 5 | def _py_list_like_to_js(value, cache, depth, max_depth): 6 | vid = id(value) 7 | if vid in cache: 8 | return cache[vid] 9 | j_value = js_array() 10 | cache[vid] = j_value 11 | for v in value: 12 | js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth) 13 | j_value.push(js_v) 14 | return j_value 15 | 16 | 17 | def _py_dict_like_to_js(value, cache, depth, max_depth): 18 | vid = id(value) 19 | if vid in cache: 20 | return cache[vid] 21 | j_value = new(js.Map) 22 | cache[vid] = j_value 23 | for k, v in value.items(): 24 | js_k = to_js(k, cache=cache, depth=depth + 1, max_depth=max_depth) 25 | js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth) 26 | j_value.set(js_k, js_v) 27 | return j_value 28 | 29 | 30 | def _py_set_like_to_js(value, cache, depth, max_depth): 31 | vid = id(value) 32 | if vid in cache: 33 | return cache[vid] 34 | j_value = new(js.Set) 35 | cache[vid] = j_value 36 | for v in value: 37 | js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth) 38 | j_value.add(js_v) 39 | return j_value 40 | 41 | 42 | def to_js(value, cache=None, depth=0, max_depth=None): 43 | """ 44 | Convert a Python object to a JavaScript object. 45 | 46 | Args: 47 | value: The Python object to convert. 48 | cache: A dictionary to use as a cache for already converted objects. 49 | depth: The current depth of of nested object conversion. 50 | max_depth: The maximum depth of nested object conversion. 51 | 52 | Returns: 53 | 54 | a JavaScript stored in a pyjs_core.JsValue 55 | """ 56 | if cache is None: 57 | cache = dict() 58 | 59 | if max_depth is not None and depth >= max_depth: 60 | return value 61 | 62 | if isinstance(value, JsValue): 63 | return value 64 | elif isinstance(value, (list, tuple)): 65 | return _py_list_like_to_js( 66 | value=value, cache=cache, depth=depth, max_depth=max_depth 67 | ) 68 | elif isinstance(value, dict): 69 | return _py_dict_like_to_js( 70 | value=value, cache=cache, depth=depth, max_depth=max_depth 71 | ) 72 | elif isinstance(value, set): 73 | return _py_set_like_to_js( 74 | value=value, cache=cache, depth=depth, max_depth=max_depth 75 | ) 76 | elif value is None: 77 | return js_undefined() 78 | elif isinstance(value, (int, float, str, bool)): 79 | return JsValue(value) 80 | 81 | # # bytestring 82 | elif isinstance(value, bytes): 83 | return internal.bytes_to_typed_array(value).buffer 84 | 85 | 86 | else: 87 | raise RuntimeError(f"no registerd converted for {value} of type {type(value)}") 88 | -------------------------------------------------------------------------------- /module/pyjs/core.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | import sys 4 | import types 5 | from typing import Any 6 | import ast 7 | import pyjs_core 8 | from pyjs_core import JsValue, js_array, js_py_object 9 | 10 | def install_submodules(): 11 | def _js_mod__getattr__(name: str) -> Any: 12 | ret = pyjs_core.internal.global_property(name) 13 | if ret is None: 14 | raise AttributeError(f"has no attribute {name}") 15 | return ret 16 | 17 | js = sys.modules["pyjs.js"] = sys.modules["js"] = sys.modules["pyjs_core.js"] = types.ModuleType("js") 18 | js.__getattr__ = _js_mod__getattr__ 19 | pyjs_core.js = js 20 | 21 | def _module_mod__getattr__(name: str) -> Any: 22 | ret = pyjs_core.internal.module_property(name) 23 | if pyjs_core.internal.is_undefined_or_null(ret): 24 | raise AttributeError(f"has no attribute {name}") 25 | return ret 26 | 27 | _module = sys.modules["pyjs_core._module"] = types.ModuleType("_module") 28 | _module.__getattr__ = _module_mod__getattr__ 29 | pyjs_core._module = _module 30 | 31 | 32 | install_submodules() 33 | del install_submodules 34 | 35 | 36 | 37 | 38 | def new(cls_, *args): 39 | """ Create a new instance of a JavaScript class. 40 | 41 | This function is a wrapper around the `new` operator in JavaScript. 42 | 43 | Args: 44 | cls_ (JsValue): The JavaScript class to create an instance of 45 | *args (Any): The arguments to pass to the constructor of the JavaScript class 46 | """ 47 | return pyjs_core._module._new(cls_, *args) 48 | 49 | # todo deprecate 50 | def async_import_javascript(path): 51 | return pyjs_core._module._async_import_javascript(path) 52 | 53 | 54 | # TODO make private 55 | def type_str(x): 56 | return pyjs_core.internal.type_str(x) 57 | 58 | 59 | def create_callable(py_function): 60 | '''Create a JavaScript callable from a Python function. 61 | 62 | Args: 63 | py_function (Callable): The Python function to create a JavaScript callable from. 64 | 65 | Example: 66 | ```python 67 | def py_function(x, y): 68 | return x + y 69 | 70 | js_callable, js_py_object = create_callable(py_function) 71 | 72 | # this function can be passed to JavaScript. 73 | # lets create some JavaScript code to test it 74 | higher_order_function = pyjs.js.Function("f", "x", "y", "z", """ 75 | return z * f(x, y); 76 | """) 77 | 78 | # call the higher order JavaScript function with py_function wrapped as a JavaScript callable 79 | result = higher_order_function(js_callable, 1, 2, 3) 80 | assert result == 9 81 | 82 | js_py_object.delete() 83 | ``` 84 | 85 | Returns: 86 | callable: The JavaScript callable 87 | js_py_object: this object needs to be deleted after the callable is no longer needed 88 | ''' 89 | _js_py_object = js_py_object(py_function) 90 | return _js_py_object["py_call"].bind(_js_py_object), _js_py_object 91 | 92 | 93 | @contextlib.contextmanager 94 | def callable_context(py_function): 95 | ''' Create a JavaScript callable from a Python function and delete it when the context is exited. 96 | 97 | See `create_callable` for more information. 98 | 99 | Args: 100 | py_function (Callable): The Python function to create a JavaScript callable from. 101 | 102 | Example: 103 | 104 | ```python 105 | def py_function(x, y): 106 | return x + y 107 | 108 | with pyjs.callable_context(py_function) as js_function: 109 | # js_function is a JavaScript callable and could be passed and called from JavaScript 110 | # here we just call it from Python 111 | print(js_function(1,2)) 112 | ``` 113 | ''' 114 | 115 | 116 | cb, handle = create_callable(py_function) 117 | yield cb 118 | handle.delete() 119 | 120 | 121 | # todo, deprecate 122 | class AsOnceCallableMixin(object): 123 | def __init__(self): 124 | self._once_callable = create_once_callable(self) 125 | 126 | def as_once_callable(self): 127 | return self._once_callable 128 | 129 | 130 | def promise(py_resolve_reject): 131 | """ Create a new JavaScript promise with a python callback to resolve or reject the promise. 132 | 133 | Args: 134 | py_resolve_reject (Callable): A Python function that takes two arguments, resolve and reject, which are both functions. 135 | The resolve function should be called with the result of the promise and the reject function should be called with an error. 136 | 137 | Example: 138 | ```python 139 | import asyncio 140 | import pyjs 141 | def f(resolve, reject): 142 | async def task(): 143 | try: 144 | print("start task") 145 | await asyncio.sleep(1) 146 | print("end task") 147 | # resolve when everything is done 148 | resolve() 149 | except: 150 | # reject the promise in case of an error 151 | reject() 152 | asyncio.create_task(task()) 153 | 154 | js_promise = pyjs.promise(f) 155 | print("await the js promise from python") 156 | await js_promise 157 | print("the wait has an end") 158 | print(js_promise) 159 | ``` 160 | """ 161 | 162 | return pyjs_core.js.Promise.new(create_once_callable(py_resolve_reject)) 163 | 164 | 165 | def create_once_callable(py_function): 166 | """Create a JavaScript callable from a Python function that can only be called once. 167 | 168 | Since this function can only be called once, it will be deleted after the first call. 169 | Therefore no manual deletion is necessary. 170 | See `create_callable` for more information. 171 | 172 | Args: 173 | py_function (Callable): The Python function to create a JavaScript callable from. 174 | 175 | Returns: 176 | callable: The JavaScript callable 177 | 178 | Example: 179 | ```python 180 | 181 | def py_function(x, y): 182 | return x + y 183 | 184 | js_function = pyjs.create_once_callable(py_function) 185 | print(js_function(1,2)) # this will print 3 186 | 187 | # the following will raise an error 188 | try: 189 | print(js_function(1,2)) 190 | except Exception as e: 191 | print(e) 192 | ``` 193 | """ 194 | 195 | js_py_function = JsValue(py_function) 196 | once_callable = pyjs_core._module._create_once_callable(js_py_function) 197 | return once_callable 198 | 199 | 200 | def _make_js_args(args): 201 | js_array_args = js_array() 202 | is_generated_proxy = js_array() 203 | for arg in args: 204 | js_arg, is_proxy = pyjs_core.internal.implicit_py_to_js(arg) 205 | pyjs_core.internal.val_call(js_array_args, "push", js_arg) 206 | pyjs_core.internal.val_call(is_generated_proxy, "push", JsValue(is_proxy)) 207 | return (js_array_args, is_generated_proxy) 208 | 209 | 210 | def apply(js_function, args): 211 | '''Call a JavaScript function with the given arguments. 212 | 213 | Args: 214 | js_function (JsValue): The JavaScript function to call 215 | args (List): The arguments to pass to the JavaScript function 216 | 217 | Returns: 218 | Any: The result of the JavaScript function 219 | 220 | Example: 221 | ```python 222 | 223 | # create a JavaScript function on the fly 224 | js_function = pyjs.js.Function("x", "y", """ 225 | return x + y; 226 | """) 227 | result = pyjs.apply(js_function, [1, 2]) 228 | assert result == 3 229 | ``` 230 | ''' 231 | js_array_args, is_generated_proxy = _make_js_args(args) 232 | ret, meta = pyjs_core.internal.apply_try_catch(js_function, js_array_args, is_generated_proxy) 233 | return ret 234 | 235 | 236 | # deprecated 237 | def japply(js_function, args): 238 | sargs = json.dumps(args) 239 | ret, meta = pyjs_core.internal.japply_try_catch(js_function, sargs) 240 | return ret 241 | 242 | # deprecated 243 | def gapply(js_function, args, jin=True, jout=True): 244 | if jin: 245 | args = json.dumps(args) 246 | is_generated_proxy = [False] * len(args) 247 | else: 248 | args, is_generated_proxy = _make_js_args(args) 249 | ret = pyjs_core.internal.gapply_try_catch(js_function, args, is_generated_proxy, jin, jout) 250 | if jout: 251 | if ret == "": 252 | return None 253 | else: 254 | return json.loads(ret) 255 | else: 256 | return ret 257 | 258 | 259 | # move to internal 260 | def exec_eval(script, globals=None, locals=None): 261 | """Execute a script and return the value of the last expression""" 262 | stmts = list(ast.iter_child_nodes(ast.parse(script))) 263 | if not stmts: 264 | return None 265 | if isinstance(stmts[-1], ast.Expr): 266 | # the last one is an expression and we will try to return the results 267 | # so we first execute the previous statements 268 | if len(stmts) > 1: 269 | exec( 270 | compile( 271 | ast.Module(body=stmts[:-1], type_ignores=[]), 272 | filename="", 273 | mode="exec", 274 | ), 275 | globals, 276 | locals, 277 | ) 278 | # then we eval the last one 279 | return eval( 280 | compile( 281 | ast.Expression(body=stmts[-1].value), filename="", mode="eval" 282 | ), 283 | globals, 284 | locals, 285 | ) 286 | else: 287 | # otherwise we just execute the entire code 288 | return exec(script, globals, locals) 289 | 290 | # move to internal 291 | async def async_exec_eval(stmts, globals=None, locals=None): 292 | parsed_stmts = ast.parse(stmts) 293 | if parsed_stmts.body: 294 | 295 | last_node = parsed_stmts.body[-1] 296 | if isinstance(last_node, ast.Expr): 297 | last_node = ast.Return(value=last_node.value) 298 | parsed_stmts.body.append(last_node) 299 | ast.fix_missing_locations(parsed_stmts) 300 | 301 | fn_name = "_pyjs_async_exec_f" 302 | fn = f"async def {fn_name}(): pass" 303 | parsed_fn = ast.parse(fn) 304 | for node in parsed_stmts.body: 305 | ast.increment_lineno(node) 306 | 307 | parsed_fn.body[0].body = parsed_stmts.body 308 | exec(compile(parsed_fn, filename="", mode="exec"), globals, locals) 309 | return await eval(f'{fn_name}()', globals, locals) # fmt: skip 310 | -------------------------------------------------------------------------------- /module/pyjs/error_handling.py: -------------------------------------------------------------------------------- 1 | from pyjs_core import js 2 | 3 | # and object holding a javascript 4 | class JsHolder(object): 5 | def __init__(self, js_proxy): 6 | self._js_proxy = js_proxy 7 | 8 | def get_js_proxy(self): 9 | return self._js_proxy 10 | 11 | 12 | class JsException(JsHolder, Exception): 13 | def __init__(self, err, message=None): 14 | 15 | # default message 16 | if message is None: 17 | message = js.JSON.stringify(err, js.Object.getOwnPropertyNames(err)) 18 | 19 | self.name = "UnknownError" 20 | try: 21 | self.name = err.name 22 | except AttributeError: 23 | pass 24 | 25 | self.message = message 26 | 27 | # i 28 | Exception.__init__(self, self.message) 29 | JsHolder.__init__(self, js_proxy=err) 30 | 31 | 32 | class JsGenericError(JsException): 33 | def __init__(self, err): 34 | super().__init__(err=err) 35 | self.value = err 36 | 37 | 38 | class JsError(JsException): 39 | def __init__(self, err): 40 | super().__init__(err=err) 41 | 42 | 43 | class JsInternalError(JsError): 44 | def __init__(self, err): 45 | super().__init__(err=err) 46 | 47 | 48 | class JsRangeError(JsError): 49 | def __init__(self, err): 50 | super().__init__(err=err) 51 | 52 | 53 | class JsReferenceError(JsError): 54 | def __init__(self, err): 55 | super().__init__(err=err) 56 | 57 | 58 | class JsSyntaxError(JsError): 59 | def __init__(self, err): 60 | super().__init__(err=err) 61 | 62 | 63 | class JsTypeError(JsError): 64 | def __init__(self, err): 65 | super().__init__(err=err) 66 | 67 | 68 | class JsURIError(JsError): 69 | def __init__(self, err): 70 | super().__init__(err=err) 71 | -------------------------------------------------------------------------------- /module/pyjs/extend_js_val.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import pyjs_core 4 | from pyjs_core import _module, JsValue 5 | from .convert import to_py 6 | from .core import apply, japply 7 | 8 | 9 | def extend_val(): 10 | def _to_string(val): 11 | if _module._is_undefined(val): 12 | return "undefined" 13 | elif _module._is_null(val): 14 | return "null" 15 | else: 16 | return val.toString() 17 | 18 | def val_next(self): 19 | res = self.next() 20 | if res.done: 21 | raise StopIteration 22 | else: 23 | return res.value 24 | 25 | @property 26 | def val_typeof(s): 27 | return pyjs_core._module._typeof(s) 28 | 29 | def val_to_future(self, callback=None): 30 | future = asyncio.Future() 31 | 32 | def _then(val, cb): 33 | if cb is not None: 34 | val = cb(val) 35 | future.set_result(val) 36 | 37 | def _catch(str_err): 38 | str_err = to_py(str_err) 39 | future.set_exception(RuntimeError(str_err)) 40 | 41 | binded_then = functools.partial(_then, cb=callback) 42 | pyjs_core._module._set_promise_then_catch(self, JsValue(binded_then), JsValue(_catch)) 43 | return future 44 | JsValue.__call__ = lambda self, *args: apply(self, args=args) 45 | JsValue._asstr_unsafe = lambda self: pyjs_core.internal.to_string(self) 46 | JsValue.__str__ = lambda self: _to_string(self) 47 | JsValue.__repr__ = lambda self: _to_string(self) 48 | JsValue.__len__ = lambda self: pyjs_core.internal.module_property("__len__")(self) 49 | JsValue.__contains__ = lambda self, q: pyjs_core.internal.module_property("__contains__")( 50 | self, q 51 | ) 52 | JsValue.__eq__ = lambda self, q: pyjs_core.internal.module_property("__eq__")(self, q) 53 | 54 | JsValue.new = lambda self, *args: pyjs_core.internal.module_property("_new")(self, *args) 55 | JsValue.to_py = lambda self, converter_options=None: to_py( 56 | js_val=self, converter_options=converter_options 57 | ) 58 | JsValue.typeof = val_typeof 59 | JsValue.__iter__ = lambda self: pyjs_core._module._iter(self) 60 | JsValue.__next__ = val_next 61 | 62 | JsValue.__delattr__ = lambda s, k: pyjs_core._module._delete(s, k) 63 | JsValue.__delitem__ = lambda s, k: s.delete(k) 64 | JsValue._to_future = val_to_future 65 | JsValue.__await__ = lambda s: s._to_future().__await__() 66 | JsValue.jcall = lambda s, *args: japply(s, args) 67 | 68 | 69 | 70 | try: 71 | extend_val() 72 | del extend_val 73 | except Exception as e: 74 | print(e) 75 | raise e 76 | -------------------------------------------------------------------------------- /module/pyjs/pyodide_polyfill.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from .convert_py_to_js import to_js 3 | from .error_handling import JsException 4 | import sys 5 | import types 6 | 7 | def install_pyodide_polyfill(): 8 | # Expose a small pyodide polyfill 9 | def _pyodide__getattr__(name: str) -> Any: 10 | if name == "to_js": 11 | return to_js 12 | 13 | raise AttributeError( 14 | "This is not the real Pyodide. We are providing a small Pyodide polyfill for conveniance." 15 | "If you are missing an important Pyodide feature, please open an issue in https://github.com/emscripten-forge/pyjs/issues" 16 | ) 17 | 18 | pyodide = sys.modules["pyodide"] = types.ModuleType("pyodide") 19 | pyodide.ffi = sys.modules["pyodide.ffi"] = types.ModuleType("ffi") 20 | pyodide.ffi.JsException = JsException 21 | pyodide.ffi.JsArray = object 22 | pyodide.ffi.JsProxy = object 23 | pyodide.__getattr__ = _pyodide__getattr__ 24 | pyodide.ffi.__getattr__ = _pyodide__getattr__ 25 | 26 | 27 | 28 | install_pyodide_polyfill() 29 | del install_pyodide_polyfill -------------------------------------------------------------------------------- /module/pyjs/webloop_README.md: -------------------------------------------------------------------------------- 1 | the webloop implementation has been taken from pyodide (https://github.com/pyodide/pyodide/blob/main/src/py/pyodide/webloop.py) 2 | and has been modified to fit pyjs -------------------------------------------------------------------------------- /pyjsConfig.cmake.in: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright (c) 2022, Dr. Thorsten Beier # 3 | # # 4 | # Distributed under the terms of the BSD 3-Clause License. # 5 | # # 6 | # The full license is in the file LICENSE, distributed with this software. # 7 | ############################################################################ 8 | 9 | # pyjs cmake module 10 | # This module sets the following variables in your project:: 11 | # 12 | # pyjs_FOUND - true if pyjs found on the system 13 | # pyjs_INCLUDE_DIRS - the directory containing pyjs headers 14 | # pyjs_PRE_JS_PATH - the pre-js path 15 | # pyjs_PRE_JS_PATH - the post-js path 16 | @PACKAGE_INIT@ 17 | 18 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR};${CMAKE_MODULE_PATH}") 19 | 20 | @PYJS_CONFIG_CODE@ 21 | 22 | include(CMakeFindDependencyMacro) 23 | find_dependency(pybind11 @pybind11_REQUIRED_VERSION@) 24 | 25 | 26 | if (NOT TARGET pyjs) 27 | include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") 28 | get_target_property(@PROJECT_NAME@_INCLUDE_DIR pyjs INTERFACE_INCLUDE_DIRECTORIES) 29 | SET(@PROJECT_NAME@_PRE_JS_PATH ${@PROJECT_NAME@_INCLUDE_DIR}/pyjs/pyjs_pre.js) 30 | SET(@PROJECT_NAME@_POST_JS_PATH ${@PROJECT_NAME@_INCLUDE_DIR}/pyjs/pyjs_post.js) 31 | endif () 32 | -------------------------------------------------------------------------------- /src/convert.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace py = pybind11; 10 | namespace em = emscripten; 11 | 12 | 13 | namespace pyjs 14 | { 15 | std::pair implicit_py_to_js(py::object& py_ret) 16 | { 17 | // py::module_ pyjs = py::module_::import("pyjs_utils"); 18 | // const std::string info = pyjs.attr("implicit_convert_info")(py_ret).cast(); 19 | 20 | const std::string info = py_ret.get_type().attr("__name__").str(); 21 | 22 | if (info == "int") 23 | { 24 | return std::make_pair(em::val(py_ret.cast()),false); 25 | } 26 | else if (info == "str") 27 | { 28 | return std::make_pair(em::val(py_ret.cast()),false); 29 | } 30 | else if (info == "bool") 31 | { 32 | return std::make_pair(em::val(py_ret.cast()),false); 33 | } 34 | else if (info == "double" || info == "float") 35 | { 36 | return std::make_pair(em::val(py_ret.cast()),false); 37 | } 38 | else if (info == "NoneType") 39 | { 40 | return std::make_pair(em::val::undefined(),false); 41 | } 42 | else if (info == "JsValue") 43 | { 44 | return std::make_pair(py_ret.cast(),false); 45 | } 46 | else if(info == "Task" || info == "coroutine") 47 | { 48 | return std::make_pair(em::val::module_property("_future_to_promise")(em::val(py_ret)),false); 49 | } 50 | else 51 | { 52 | return std::make_pair(em::val::module_property("make_proxy")(em::val(py_ret)),true); 53 | } 54 | } 55 | 56 | 57 | py::object implicit_js_to_py(em::val val, const std::string& type_string) 58 | { 59 | if (type_string.size() == 1) 60 | { 61 | const char s = type_string[0]; 62 | switch (s) 63 | { 64 | case static_cast(JsType::JS_NULL): 65 | { 66 | return py::none(); 67 | } 68 | case static_cast(JsType::JS_UNDEFINED): 69 | { 70 | return py::none(); 71 | } 72 | // 2 is object 73 | case static_cast(JsType::JS_STR): 74 | { 75 | return py::cast(val.as()); 76 | } 77 | case static_cast(JsType::JS_INT): 78 | { 79 | const auto double_number = val.as(); 80 | const auto rounded_double_number = std::round(double_number); 81 | return py::cast(int(rounded_double_number)); 82 | } 83 | case static_cast(JsType::JS_FLOAT): 84 | { 85 | return py::cast(val.as()); 86 | } 87 | case static_cast(JsType::JS_BOOL): 88 | { 89 | return py::cast(val.as()); 90 | } 91 | default: 92 | { 93 | return py::cast(val); 94 | } 95 | } 96 | } 97 | else if (type_string == "pyobject") 98 | { 99 | return val.as(); 100 | } 101 | else 102 | { 103 | return py::cast(val); 104 | } 105 | } 106 | 107 | py::object implicit_js_to_py(em::val val) 108 | { 109 | const auto type_string = em::val::module_property("_get_type_string")(val).as(); 110 | return implicit_js_to_py(val, type_string); 111 | } 112 | 113 | 114 | bool instanceof (em::val instance, const std::string& cls_name) 115 | { 116 | return em::val::module_property("_instanceof")(instance, em::val::global(cls_name.c_str())) 117 | .as(); 118 | }; 119 | 120 | 121 | TypedArrayBuffer::TypedArrayBuffer( 122 | em::val js_array, const std::string & format_descriptor 123 | ) 124 | : m_size(js_array["length"].as()), 125 | m_bytes_per_element(js_array["BYTES_PER_ELEMENT"].as()), 126 | m_format_descriptor(format_descriptor), 127 | m_data( new uint8_t[m_size * m_bytes_per_element] ) 128 | { 129 | em::val js_array_buffer = js_array["buffer"].as(); 130 | 131 | const unsigned byte_offset = js_array["byteOffset"].as().as(); 132 | 133 | // this is a uint8 view of the array 134 | em::val js_uint8array = em::val::global("Uint8Array") 135 | .new_(js_array_buffer, byte_offset, m_size * m_bytes_per_element); 136 | 137 | em::val wasm_heap_allocated = js_uint8array["constructor"].new_( 138 | em::val::module_property("HEAPU8")["buffer"], 139 | reinterpret_cast(m_data), 140 | m_size * m_bytes_per_element 141 | ); 142 | wasm_heap_allocated.call("set", js_uint8array); 143 | } 144 | 145 | TypedArrayBuffer::~TypedArrayBuffer(){ 146 | delete[] m_data; 147 | } 148 | 149 | TypedArrayBuffer* typed_array_to_buffer(em::val js_array) 150 | { 151 | if (instanceof (js_array, "Int8Array")) 152 | { 153 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 154 | } 155 | else if (instanceof (js_array, "Uint8Array")) 156 | { 157 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 158 | } 159 | else if (instanceof (js_array, "Uint8ClampedArray")) 160 | { 161 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 162 | } 163 | else if (instanceof (js_array, "Int16Array")) 164 | { 165 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 166 | } 167 | else if (instanceof (js_array, "Uint16Array")) 168 | { 169 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 170 | } 171 | else if (instanceof (js_array, "Int32Array")) 172 | { 173 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 174 | } 175 | else if (instanceof (js_array, "Uint32Array")) 176 | { 177 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 178 | } 179 | else if (instanceof (js_array, "Float32Array")) 180 | { 181 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 182 | } 183 | else if (instanceof (js_array, "Float64Array")) 184 | { 185 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 186 | } 187 | else if (instanceof (js_array, "BigInt64Array")) 188 | { 189 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 190 | } 191 | else if (instanceof (js_array, "BigUint64Array")) 192 | { 193 | return new TypedArrayBuffer(js_array, py::format_descriptor::format()); 194 | } 195 | else 196 | { 197 | throw pybind11::type_error("unknown array type"); 198 | } 199 | } 200 | 201 | 202 | em::val bytes_to_js(char * binary_string) 203 | { 204 | // get the length of the string 205 | std::size_t length = std::strlen(binary_string); 206 | em::val mem_view = em::val(em::typed_memory_view(length, binary_string)); 207 | em::val mem_copy = em::val::global("Uint8Array").new_(mem_view); 208 | return mem_copy; 209 | } 210 | 211 | template 212 | em::val py_1d_buffer_to_typed_array_t(const std::size_t size, 213 | void* void_ptr, 214 | bool view, 215 | const std::string& js_cls_name) 216 | { 217 | T* ptr = static_cast(void_ptr); 218 | em::val mem_view = em::val(em::typed_memory_view(size, ptr)); 219 | if (!view) 220 | { 221 | em::val mem_copy = em::val::global(js_cls_name.c_str()).new_(mem_view); 222 | return mem_copy; 223 | } 224 | return mem_view; 225 | } 226 | 227 | 228 | 229 | 230 | em::val py_1d_buffer_to_typed_array(py::buffer buffer, bool view) 231 | { 232 | /* Request a buffer descriptor from Python */ 233 | py::buffer_info info = buffer.request(); 234 | 235 | const auto format = info.format; 236 | 237 | 238 | if (info.ndim != 1) 239 | { 240 | throw std::runtime_error("Incompatible buffer dimension!"); 241 | } 242 | 243 | 244 | 245 | // sizeof one element in bytes 246 | const auto itemsize = info.itemsize; 247 | const auto stride = (info.strides[0] / itemsize); 248 | if (stride != 1) 249 | { 250 | std::stringstream s; 251 | s << "only continous arrays are allowed but stride is " << stride << " raw stride " 252 | << info.strides[0] << " itemsize " << itemsize << " shape " << info.shape[0]; 253 | throw std::runtime_error(s.str().c_str()); 254 | } 255 | 256 | // shape 257 | const std::size_t size = info.shape[0]; 258 | 259 | 260 | if (format == py::format_descriptor::format()) 261 | { 262 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Float32Array"); 263 | } 264 | else if (format == py::format_descriptor::format()) 265 | { 266 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Float64Array"); 267 | } 268 | else if (format == py::format_descriptor::format()) 269 | { 270 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Uint8Array"); 271 | } 272 | else if (format == py::format_descriptor::format()) 273 | { 274 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Uint16Array"); 275 | } 276 | else if (format == py::format_descriptor::format() || format == "L") 277 | { 278 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Uint32Array"); 279 | } 280 | else if (format == py::format_descriptor::format()) 281 | { 282 | throw std::runtime_error( 283 | "uint64_t is not yet supported in pyjs since the stack is not yet compiled with WasmBigInt support"); 284 | } 285 | else if (format == py::format_descriptor::format()) 286 | { 287 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Int8Array"); 288 | } 289 | else if (format == py::format_descriptor::format()) 290 | { 291 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Int16Array"); 292 | } 293 | else if (format == py::format_descriptor::format() || format == "l") 294 | { 295 | return py_1d_buffer_to_typed_array_t(size, info.ptr, view, "Int32Array"); 296 | } 297 | else if (format == py::format_descriptor::format()) 298 | { 299 | throw std::runtime_error( 300 | "int64_t is not yet supported in pyjs since the stack is not yet compiled with WasmBigInt support"); 301 | } 302 | else 303 | { 304 | std::stringstream s; 305 | s << "pyjs error: an unknown format: occurred when converting np.ndarray to a JavaScript TypedArray: format=" 306 | << format; 307 | throw std::runtime_error(s.str()); 308 | } 309 | } 310 | 311 | } 312 | -------------------------------------------------------------------------------- /src/export_js_module.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | namespace pyjs 17 | { 18 | namespace em = emscripten; 19 | namespace py = pybind11; 20 | 21 | 22 | em::val eval(const std::string & code, py::object & globals, py::object & locals) 23 | { em::val ret = em::val::object(); 24 | try 25 | { 26 | py::object py_ret = py::eval(code, globals, locals); 27 | ret.set("has_err",em::val(false)); 28 | auto [jsval, is_proxy] = implicit_py_to_js(py_ret); 29 | ret.set("ret",jsval); 30 | ret.set("is_proxy",is_proxy); 31 | return ret; 32 | } 33 | catch (py::error_already_set& e) 34 | { 35 | ret.set("has_err",em::val(true)); 36 | ret.set("message",em::val(std::string(e.what()))); 37 | ret.set("error",em::val(std::string(e.what()))); 38 | return ret; 39 | } 40 | } 41 | 42 | 43 | 44 | em::val exec(const std::string & code, py::object & globals, py::object & locals) 45 | { 46 | em::val ret = em::val::object(); 47 | try 48 | { 49 | py::exec(code, globals, locals); 50 | ret.set("has_err",em::val(false)); 51 | return ret; 52 | } 53 | catch (py::error_already_set& e) 54 | { 55 | ret.set("has_err",em::val(true)); 56 | ret.set("message",em::val(std::string(e.what()))); 57 | ret.set("error",em::val(std::string(e.what()))); 58 | return ret; 59 | } 60 | } 61 | 62 | 63 | em::val eval_file(const std::string & filename, py::object & globals, py::object & locals) 64 | { 65 | em::val ret = em::val::object(); 66 | try 67 | { 68 | py::eval_file(filename, globals, locals); 69 | ret.set("has_err",em::val(false)); 70 | return ret; 71 | } 72 | catch (py::error_already_set& e) 73 | { 74 | ret.set("has_err",em::val(true)); 75 | ret.set("message",em::val(std::string(e.what()))); 76 | ret.set("error",em::val(std::string(e.what()))); 77 | return ret; 78 | } 79 | } 80 | 81 | void set_env(const std::string & key, const std::string & value) 82 | { 83 | setenv(key.c_str(), value.c_str(), 1); 84 | } 85 | 86 | void create_directories(const std::string & p) 87 | { 88 | std::filesystem::create_directories(std::filesystem::path(p)); 89 | } 90 | bool is_directory(const std::string & p) 91 | { 92 | return std::filesystem::is_directory(std::filesystem::path(p)); 93 | } 94 | 95 | 96 | std::string extract_exception_message(int exceptionPtr) 97 | { 98 | auto ptr = reinterpret_cast(exceptionPtr); 99 | 100 | // get traceback 101 | std::vector traceback; 102 | 103 | return std::string(ptr->what()); 104 | } 105 | 106 | 107 | void export_js_module() 108 | { 109 | // interpreter itself, 110 | em::class_("_Interpreter") 111 | .constructor<>() 112 | ; 113 | 114 | em::function("_eval", &eval); 115 | em::function("_exec", &exec); 116 | em::function("_eval_file", &eval_file); 117 | 118 | em::function("create_directories", &create_directories); 119 | em::function("is_directory", &is_directory); 120 | 121 | 122 | em::function("_untar", &untar); 123 | em::function("_install_conda_file", &install_conda_file); 124 | em::function("setenv", &set_env); 125 | 126 | // py-object (proxy) 127 | export_py_object(); 128 | 129 | // main scope 130 | em::function("main_scope",em::select_overload( 131 | []()->py::object{ 132 | auto scope = py::module_::import("__main__").attr("__dict__"); 133 | //py::exec("import pyjs;import asyncio", scope); 134 | return scope; 135 | } 136 | )); 137 | 138 | em::function("cout", 139 | em::select_overload([](const std::string& val) 140 | { std::cout << val; })); 141 | 142 | 143 | em::function("extract_exception_message", &extract_exception_message); 144 | 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/export_js_proxy.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | namespace py = pybind11; 16 | namespace em = emscripten; 17 | 18 | 19 | namespace pyjs 20 | { 21 | inline py::object wrap_result(const py::module_& pyjs_core, em::val wrapped_return_value, const bool has_err) 22 | { 23 | if (has_err) 24 | { 25 | py::module::import("pyjs").attr("error_to_py_and_raise")(wrapped_return_value["err"]); 26 | //pyjs.attr("error_to_py_and_raise")(wrapped_return_value["err"]); 27 | } 28 | const bool has_ret = wrapped_return_value["has_ret"].as(); 29 | const auto type_string = wrapped_return_value["type_string"].as(); 30 | if (has_ret) 31 | { 32 | py::tuple ret_tuple = py::make_tuple( 33 | implicit_js_to_py(wrapped_return_value["ret"], type_string), type_string); 34 | return ret_tuple; 35 | } 36 | else 37 | { 38 | py::tuple ret_tuple = py::make_tuple(py::none(), type_string); 39 | return ret_tuple; 40 | } 41 | } 42 | 43 | inline py::object wrap_result(const py::module_& m, em::val wrapped_return_value) 44 | { 45 | const bool has_err = wrapped_return_value["has_err"].as(); 46 | return wrap_result(m, wrapped_return_value, has_err); 47 | } 48 | 49 | 50 | inline void wrap_void(const py::module_& pyjs, em::val wrapped_return_value) 51 | { 52 | const bool has_err = wrapped_return_value["has_err"].as(); 53 | 54 | if (has_err) 55 | { 56 | //py::module pyjs = py::module::import("pyjs"); 57 | pyjs.attr("error_to_py_and_raise")(wrapped_return_value["err"]); 58 | } 59 | } 60 | 61 | 62 | inline py::object getattr(em::val* self, em::val* key) 63 | { 64 | em::val wrapped_return_value = em::val::module_property("_getattr_try_catch")(*self, *key); 65 | 66 | const bool has_ret = wrapped_return_value["has_ret"].as(); 67 | const bool has_err = wrapped_return_value["has_err"].as(); 68 | 69 | if (has_err) 70 | { 71 | py::module pyjs = py::module::import("pyjs"); 72 | pyjs.attr("error_to_py_and_raise")(wrapped_return_value["err"]); 73 | } 74 | 75 | const auto type_string = wrapped_return_value["type_string"].as(); 76 | 77 | 78 | 79 | if(type_string.size() == 1 && type_string[0] == static_cast::type>(JsType::JS_NULL)) 80 | { 81 | return py::none(); 82 | } 83 | else if(type_string.size() == 1 && type_string[0] == static_cast::type>(JsType::JS_UNDEFINED)) 84 | { 85 | std::stringstream ss; 86 | ss << "has no attribute/key "; 87 | 88 | // check if key is a string 89 | if (key->typeOf().as() == "string") 90 | { 91 | ss << key->as(); 92 | } 93 | else if (key->typeOf().as() == "number") 94 | { 95 | ss << key->as(); 96 | } 97 | else 98 | { 99 | ss << "[unknown key type]"; 100 | } 101 | 102 | throw pybind11::attribute_error(ss.str()); 103 | } 104 | return implicit_js_to_py(wrapped_return_value["ret"], type_string); 105 | } 106 | 107 | void export_js_proxy(py::module_& m) 108 | { 109 | py::module_ m_internal = m.def_submodule("internal", "implementation details of of pyjs"); 110 | 111 | m_internal.def("global_property", 112 | [](const std::string& arg) 113 | { 114 | em::val v = em::val::global(arg.c_str()); 115 | const std::string type_string 116 | = em::val::module_property("_get_type_string")(v).as(); 117 | return implicit_js_to_py(v, type_string); 118 | }); 119 | 120 | m_internal.def("module_property", 121 | [](const std::string& arg) 122 | { 123 | em::val v = em::val::module_property(arg.c_str()); 124 | return v; 125 | }); 126 | 127 | 128 | m_internal.def("apply_try_catch", 129 | [m](em::val* js_function, em::val* args, em::val* is_generated_proxy) -> py::object { 130 | return wrap_result(m, 131 | em::val::module_property("_apply_try_catch")(*js_function, *args, *is_generated_proxy)); 132 | }); 133 | 134 | m_internal.def("japply_try_catch", 135 | [m](em::val* js_function, em::val* jargs, em::val* is_generated_proxy) -> py::object { 136 | return wrap_result(m, 137 | em::val::module_property("_japply_try_catch")(*js_function, *jargs,*is_generated_proxy)); 138 | }); 139 | 140 | m_internal.def("gapply_try_catch", 141 | [m](em::val* js_function, em::val* jargs, bool jin, bool jout) -> py::object 142 | { 143 | em::val wrapped_return_value = em::val::module_property( 144 | "_gapply_try_catch")(*js_function, *jargs, jin, jout); 145 | const bool has_err = wrapped_return_value["has_err"].as(); 146 | if (has_err) 147 | { 148 | py::module pyjs = py::module::import("pyjs"); 149 | pyjs.attr("error_to_py_and_raise")(wrapped_return_value["err"]); 150 | } 151 | if (jout) 152 | { 153 | em::val out = wrapped_return_value["ret"]; 154 | return py::cast(out.as()); 155 | } 156 | else 157 | { 158 | return wrap_result(m, wrapped_return_value, has_err); 159 | } 160 | }); 161 | 162 | m_internal.def( 163 | "getattr_try_catch", 164 | [m](em::val* obj, em::val* key) -> py::object 165 | { return wrap_result(m, em::val::module_property("_getattr_try_catch")(*obj, *key)); }); 166 | 167 | 168 | m_internal.def( 169 | "setattr_try_catch", 170 | [m](em::val* obj, em::val* key, em::val* value) 171 | { wrap_void(m, em::val::module_property("_setattr_try_catch")(*obj, *key, *value)); }); 172 | 173 | m.def("js_int", [](const int v) { return em::val(v); }); 174 | 175 | m.def("js_array", []() { return em::val::array(); }); 176 | m.def("js_object", []() { return em::val::object(); }); 177 | m.def("js_undefined", []() { return em::val::undefined(); }); 178 | m.def("js_null", []() { return em::val::null(); }); 179 | m.def("js_py_object", 180 | [](const py::object& py_object) 181 | { 182 | py::object cp(py_object); 183 | py_object.inc_ref(); 184 | return em::val(std::move(cp)); 185 | }); 186 | 187 | m.def("instanceof", 188 | [](em::val* instance, em::val* cls) 189 | { return em::val::module_property("_instanceof")(*instance, *cls).as(); }); 190 | 191 | 192 | m_internal.def("val_call", 193 | [](em::val* v, const std::string& key, em::val& arg1) 194 | { return v->call(key.c_str(), arg1); }); 195 | 196 | m_internal.def("val_call", 197 | [](em::val* v, const std::string& key, em::val& arg1, em::val& arg2) 198 | { return v->call(key.c_str(), arg1, arg2); }); 199 | 200 | m_internal.def("val_function_call", [](em::val* v) { return v->operator()(); }); 201 | m_internal.def("val_function_call", 202 | [](em::val* v, em::val arg1) { return v->operator()(arg1); }); 203 | m_internal.def("val_function_call", 204 | [](em::val* v, em::val arg1, em::val arg2) 205 | { return v->operator()(arg1, arg2); }); 206 | m_internal.def("val_function_call", 207 | [](em::val* v, em::val arg1, em::val arg2, em::val arg3) 208 | { return v->operator()(arg1, arg2, arg3); }); 209 | m_internal.def("val_function_call", 210 | [](em::val* v, em::val arg1, em::val arg2, em::val arg3, em::val arg4) 211 | { return v->operator()(arg1, arg2, arg3, arg4); }); 212 | 213 | m_internal.def("val_bind", 214 | [](em::val* v, em::val arg1) { return v->call("bind", arg1); }); 215 | 216 | 217 | // m_internal.def("val_new",[](em::val v){ 218 | // return v.new_(); 219 | // }); 220 | // m_internal.def("val_new",[](em::val v, em::val arg1){ 221 | // return v.new_(arg1); 222 | // }); 223 | 224 | m_internal.def("as_int", [](em::val* v) -> int { return v->as(); }); 225 | m_internal.def("as_double", [](em::val* v) -> double { return v->as(); }); 226 | m_internal.def("as_float", [](em::val* v) -> float { return v->as(); }); 227 | m_internal.def("as_boolean", [](em::val* v) -> bool { return v->as(); }); 228 | m_internal.def("as_string", [](em::val* v) -> std::wstring { return v->as(); }); 229 | 230 | // this function returns a new value so the return value policy needs to manage a new object 231 | m_internal.def("as_buffer", 232 | [](em::val* v) -> TypedArrayBuffer* { return typed_array_to_buffer(*v); }, py::return_value_policy::take_ownership); 233 | 234 | m_internal.def("as_py_object", 235 | [](em::val* v) -> py::object { return v->as(); }); 236 | 237 | 238 | m_internal.def( 239 | "get_type_string", 240 | [](em::val* val) -> std::string 241 | { return em::val::module_property("_get_type_string")(*val).as(); }); 242 | 243 | 244 | m_internal.def("is_null", 245 | [](em::val* val) -> bool 246 | { return em::val::module_property("_is_null")(*val).as(); }); 247 | 248 | m_internal.def("is_undefined", 249 | [](em::val* val) -> bool 250 | { return em::val::module_property("_is_undefined")(*val).as(); }); 251 | 252 | m_internal.def( 253 | "is_undefined_or_null", 254 | [](em::val* val) -> bool 255 | { return em::val::module_property("_is_undefined_or_null")(*val).as(); }); 256 | 257 | 258 | m_internal.def("is_error", 259 | [](em::val* val) 260 | { 261 | const std::string ts = val->typeOf().as(); 262 | if (ts == "object") 263 | { 264 | if (val->hasOwnProperty("__pyjs__error__")) 265 | { 266 | return true; 267 | } 268 | } 269 | return false; 270 | }); 271 | 272 | m_internal.def("get_error", 273 | [](em::val* val) { return val->operator[]("__pyjs__error__"); }); 274 | 275 | 276 | m_internal.def("__getitem__", 277 | [](em::val* v, const std::string& key) { return v->operator[](key); }); 278 | 279 | m_internal.def("__getitem__", [](em::val* v, int index) { return v->operator[](index); }); 280 | 281 | 282 | m_internal.def("object_keys", 283 | [](em::val* v) 284 | { return em::val::global("Object").call("keys", *v); }); 285 | m_internal.def("object_values", 286 | [](em::val* v) 287 | { return em::val::global("Object").call("values", *v); }); 288 | 289 | 290 | m_internal.def("length", 291 | [](em::val* v) -> int { return v->operator[]("length").as(); }); 292 | 293 | m_internal.def("type_str", [](em::val* v) { return v->typeOf().as(); }); 294 | 295 | 296 | m_internal.def("to_string", 297 | [](em::val* v) -> std::wstring { return v->call("toString"); }); 298 | 299 | 300 | py::class_(m, "TypedArrayBuffer", py::buffer_protocol()) 301 | .def_buffer([](TypedArrayBuffer& self) -> py::buffer_info 302 | { 303 | return py::buffer_info(self.m_data, self.m_bytes_per_element, self.m_format_descriptor, 304 | 1, {self.m_size}, {self.m_bytes_per_element}); 305 | }) 306 | .def("tobytes", [](TypedArrayBuffer& self) { 307 | return py::reinterpret_steal( 308 | PYBIND11_BYTES_FROM_STRING_AND_SIZE(reinterpret_cast(self.m_data), self.m_size) 309 | ).release(); 310 | }); 311 | 312 | // this class is heavy extended on the python side 313 | // py::class_(m, "JsValue", py::dynamic_attr()) 314 | py::class_(m, "JsValue") //, py::dynamic_attr()) 315 | 316 | .def(py::init([](std::wstring arg) 317 | { return std::unique_ptr(new em::val(arg)); })) 318 | .def(py::init([](bool arg) { return std::unique_ptr(new em::val(arg)); })) 319 | 320 | .def(py::init([](int arg) { return std::unique_ptr(new em::val(arg)); })) 321 | .def(py::init([](float arg) { return std::unique_ptr(new em::val(arg)); })) 322 | .def(py::init([](double arg) { return std::unique_ptr(new em::val(arg)); })) 323 | 324 | .def(py::init([](py::object obj) 325 | { return std::unique_ptr(new em::val(std::move(obj))); })) 326 | 327 | .def("__getattr__", &getattr) 328 | .def("__getitem__", &getattr) 329 | .def("__setattr__", 330 | [m](em::val* obj, em::val* key, em::val* value) 331 | { wrap_void(m, em::val::module_property("_setattr_try_catch")(*obj, *key, *value)); }) 332 | .def("__setitem__", 333 | [m](em::val* obj, em::val* key, em::val* value) { 334 | wrap_void(m, em::val::module_property("_setattr_try_catch")(*obj, *key, *value)); 335 | }); 336 | 337 | m_internal.def("implicit_py_to_js",&implicit_py_to_js); 338 | 339 | py::implicitly_convertible(); 340 | py::implicitly_convertible(); 341 | py::implicitly_convertible(); 342 | py::implicitly_convertible(); 343 | py::implicitly_convertible(); 344 | 345 | 346 | 347 | m_internal.def("py_1d_buffer_to_typed_array", &py_1d_buffer_to_typed_array); 348 | m_internal.def("bytes_to_typed_array", &bytes_to_js); 349 | 350 | } 351 | 352 | } 353 | -------------------------------------------------------------------------------- /src/export_py_object.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | namespace em = emscripten; 10 | 11 | namespace pyjs 12 | { 13 | template 14 | em::val wrap_py_err(E& e) 15 | { 16 | em::val ret = em::val::object(); 17 | ret.set("has_err", em::val(true)); 18 | ret.set("message", em::val(std::string(e.what()))); 19 | ret.set("error", em::val(std::string(e.what()))); 20 | return ret; 21 | } 22 | 23 | 24 | em::val raw_apply(py::object& self, 25 | em::val args, 26 | em::val args_types, 27 | std::size_t n_args, 28 | em::val kwargs_keys, 29 | em::val kwargs_values, 30 | em::val kwargs_values_types, 31 | std::size_t n_kwargs) 32 | { 33 | py::gil_scoped_acquire acquire; 34 | py::list py_args; 35 | py::dict py_kwargs; 36 | for (std::size_t i = 0; i < n_args; ++i) 37 | { 38 | py::object py_arg = implicit_js_to_py(args[i], args_types[i].as()); 39 | py_args.append(py_arg); 40 | } 41 | 42 | for (std::size_t i = 0; i < n_kwargs; ++i) 43 | { 44 | py::object py_kwarg_val 45 | = implicit_js_to_py(kwargs_values[i], kwargs_values_types[i].as()); 46 | const std::string key = kwargs_keys[i].as(); 47 | py_kwargs[py::cast(key)] = py_kwarg_val; 48 | } 49 | 50 | try 51 | { 52 | em::val ret = em::val::object(); 53 | py::object py_ret = self(*py_args, **py_kwargs); 54 | ret.set("has_err", em::val(false)); 55 | 56 | auto [jsval, is_proxy] = implicit_py_to_js(py_ret); 57 | 58 | ret.set("ret", jsval); 59 | ret.set("is_proxy", is_proxy); 60 | return ret; 61 | } 62 | catch (py::error_already_set& e) 63 | { 64 | return wrap_py_err(e); 65 | } 66 | catch (std::exception& e) 67 | { 68 | return wrap_py_err(e); 69 | } 70 | } 71 | 72 | 73 | void export_py_object() 74 | { 75 | em::class_("pyobject") 76 | 77 | 78 | .function("_raw_apply", &raw_apply) 79 | 80 | 81 | .function( 82 | "_raw_getitem", 83 | em::select_overload( 84 | [](py::object& pyobject, em::val args, em::val arg_types, std::size_t n_keys) 85 | -> em::val 86 | { 87 | py::gil_scoped_acquire acquire; 88 | 89 | 90 | // implicit to py 91 | 92 | py::list py_args; 93 | for (std::size_t i = 0; i < n_keys; ++i) 94 | { 95 | py::object py_arg 96 | = implicit_js_to_py(args[i], arg_types[i].as()); 97 | py_args.append(py_arg); 98 | } 99 | 100 | try 101 | { 102 | em::val ret = em::val::object(); 103 | ret.set("has_err", em::val(false)); 104 | if (n_keys == 0) 105 | { 106 | py::object py_ret = pyobject.attr("__getitem__")(); 107 | auto [jsval, is_proxy] = implicit_py_to_js(py_ret); 108 | ret.set("ret", jsval); 109 | ret.set("is_proxy", is_proxy); 110 | } 111 | if (n_keys == 1) 112 | { 113 | py::object py_ret = pyobject.attr("__getitem__")(py_args[0]); 114 | auto [jsval, is_proxy] = implicit_py_to_js(py_ret); 115 | ret.set("ret", jsval); 116 | ret.set("is_proxy", is_proxy); 117 | } 118 | else 119 | { 120 | py::tuple tuple_args(py_args); 121 | py::object py_ret = pyobject.attr("__getitem__")(tuple_args); 122 | auto [jsval, is_proxy] = implicit_py_to_js(py_ret); 123 | ret.set("ret", jsval); 124 | ret.set("is_proxy", is_proxy); 125 | } 126 | return ret; 127 | } 128 | catch (py::error_already_set& e) 129 | { 130 | return wrap_py_err(e); 131 | } 132 | catch (std::exception& e) 133 | { 134 | return wrap_py_err(e); 135 | } 136 | })) 137 | 138 | 139 | // 0-ary 140 | .function("__call__", 141 | em::select_overload( 142 | [](py::object& pyobject) -> em::val 143 | { 144 | try 145 | { 146 | py::gil_scoped_acquire acquire; 147 | py::object ret = pyobject(); 148 | return em::val(std::move(ret)); 149 | } 150 | catch (py::error_already_set& e) 151 | { 152 | std::cout << "error: " << e.what() << "\n"; 153 | return em::val::null(); 154 | } 155 | })) 156 | // 1-ary 157 | .function("__call__", 158 | em::select_overload( 159 | [](py::object& pyobject, em::val arg1) -> em::val 160 | { 161 | try 162 | { 163 | py::gil_scoped_acquire acquire; 164 | py::object ret = pyobject(arg1); 165 | return em::val(std::move(ret)); 166 | } 167 | catch (py::error_already_set& e) 168 | { 169 | std::cout << "error: " << e.what() << "\n"; 170 | return em::val::null(); 171 | } 172 | })) 173 | 174 | .function("__usafe_void_void__", 175 | em::select_overload( 176 | [](py::object& pyobject) 177 | { 178 | try 179 | { 180 | py::gil_scoped_acquire acquire; 181 | pyobject(); 182 | } 183 | catch (py::error_already_set& e) 184 | { 185 | std::cout << "unhandled error: " << e.what() << "\n"; 186 | } 187 | catch( const std::exception &e) { 188 | std::cout << "unhandled error: " << e.what() << "\n"; 189 | } 190 | catch(...){ 191 | std::cout<<"catched unhandled something.\n"; 192 | } 193 | })) 194 | 195 | .function("__usafe_void_val__", 196 | em::select_overload( 197 | [](py::object& pyobject, em::val val) 198 | { 199 | { 200 | py::gil_scoped_acquire acquire; 201 | pyobject(implicit_js_to_py(val)); 202 | } 203 | })) 204 | 205 | .function("_raw_getattr", 206 | em::select_overload( 207 | [](py::object& pyobject, const std::string& attr_name) -> em::val 208 | { 209 | em::val ret = em::val::object(); 210 | try 211 | { 212 | py::object py_ret = pyobject.attr(attr_name.c_str()); 213 | ret.set("has_err", em::val(false)); 214 | auto [jsval, is_proxy] = implicit_py_to_js(py_ret); 215 | ret.set("ret", jsval); 216 | ret.set("is_proxy",is_proxy); 217 | return ret; 218 | } 219 | catch (py::error_already_set& e) 220 | { 221 | ret.set("has_err", em::val(true)); 222 | ret.set("message", em::val(std::string(e.what()))); 223 | ret.set("error", em::val(std::string(e.what()))); 224 | return ret; 225 | } 226 | })) 227 | .function("__toPrimitive", 228 | em::select_overload( 229 | [](py::object& pyobject) -> em::val 230 | { 231 | auto numbers = py::module::import("numbers"); 232 | if(py::isinstance(pyobject, numbers.attr("Number"))) 233 | { 234 | py::float_ pyf = pyobject.cast(); 235 | const auto d = pyf.cast(); 236 | return em::val(d); 237 | } 238 | else{ 239 | const auto py_str = py::str(pyobject); 240 | const std::string str = py_str.cast(); 241 | return em::val(str); 242 | } 243 | }) 244 | ) 245 | 246 | .function("toString", 247 | em::select_overload( 248 | [](py::object& pyobject) -> em::val 249 | { 250 | const std::string str = py::str(pyobject); 251 | return em::val(str); 252 | }) 253 | ) 254 | .function("toJSON", 255 | em::select_overload( 256 | [](py::object& pyobject, em::val val) -> em::val 257 | { 258 | auto json_module = py::module::import("json"); 259 | auto json_dumps = json_module.attr("dumps"); 260 | try{ 261 | auto json_str = em::val(json_dumps(pyobject).cast()); 262 | return json_str; 263 | } 264 | catch (py::error_already_set& e) 265 | { 266 | const auto py_str = py::str(pyobject); 267 | const std::string str = py_str.cast(); 268 | return em::val(str); 269 | } 270 | }) 271 | ) 272 | ; 273 | } 274 | 275 | } 276 | -------------------------------------------------------------------------------- /src/export_pyjs_module.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | // python 14 | 15 | namespace py = pybind11; 16 | namespace em = emscripten; 17 | 18 | namespace pyjs 19 | { 20 | void export_pyjs_module(py::module_& pyjs_module) 21 | { 22 | export_js_proxy(pyjs_module); 23 | try 24 | { 25 | // pyjs_core_pseudo_init(pyjs_module); 26 | // pyjs_extend_js_val_pseudo_init(pyjs_module); 27 | // pyjs_error_handling_pseudo_init(pyjs_module); 28 | // pyjs_convert_pseudo_init(pyjs_module); 29 | // pyjs_convert_py_to_js_pseudo_init(pyjs_module); 30 | // pyjs_webloop_pseudo_init(pyjs_module); 31 | // pyjs_pyodide_polyfill_pseudo_init(pyjs_module); 32 | } 33 | catch (py::error_already_set& e) 34 | { 35 | std::cout << "error: " << e.what() << "\n"; 36 | throw e; 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/inflate.cpp: -------------------------------------------------------------------------------- 1 | // from https://zlib.net/zlib_how.html 2 | // and https://windrealm.org/tutorials/decompress-gzip-stream.php 3 | // and https://stackoverflow.com/questions/10195343/copy-a-file-in-a-sane-safe-and-efficient-way 4 | 5 | #include 6 | #include 7 | #include 8 | #include "zlib.h" 9 | 10 | #define CHUNK 16384 11 | 12 | namespace pyjs{ 13 | 14 | 15 | 16 | /* Decompress from file source to file dest until stream ends or EOF. */ 17 | void inflate(gzFile_s *source, FILE *dest) { 18 | 19 | unsigned char buf[CHUNK]; 20 | 21 | for ( 22 | int size = gzread(source, buf, CHUNK); 23 | size > 0; 24 | size = gzread(source, buf, CHUNK) 25 | ) { 26 | fwrite(buf, 1, CHUNK, dest); 27 | } 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/install_conda_file.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | namespace em = emscripten; 19 | namespace fs = std::filesystem; 20 | namespace pyjs 21 | { 22 | 23 | bool decompress_zstd(const fs::path& inputFile, const fs::path& outputFile) 24 | { 25 | const int CHUNK_SIZE = 16384; 26 | // Open input and output files 27 | std::ifstream fin(inputFile, std::ios::binary); 28 | std::ofstream fout(outputFile, std::ios::binary); 29 | 30 | if (!fin.is_open() || !fout.is_open()) 31 | { 32 | std::cerr << "Failed to open input or output file!" << std::endl; 33 | return false; 34 | } 35 | 36 | // Create a Zstd decompression context 37 | ZSTD_DCtx* dctx = ZSTD_createDCtx(); 38 | if (!dctx) 39 | { 40 | std::cerr << "Failed to create ZSTD decompression context!" << std::endl; 41 | return false; 42 | } 43 | 44 | // Allocate buffers for input and output 45 | std::vector in_buffer(CHUNK_SIZE); 46 | std::vector out_buffer(CHUNK_SIZE); 47 | 48 | size_t read_bytes = 0; 49 | size_t result = 0; 50 | 51 | // Decompress the file chunk by chunk 52 | while (fin.read(in_buffer.data(), CHUNK_SIZE) || fin.gcount() > 0) 53 | { 54 | read_bytes = fin.gcount(); 55 | const char* src = in_buffer.data(); 56 | 57 | // Stream decompression 58 | while (read_bytes > 0) 59 | { 60 | ZSTD_inBuffer input = { src, read_bytes, 0 }; 61 | ZSTD_outBuffer output = { out_buffer.data(), out_buffer.size(), 0 }; 62 | 63 | result = ZSTD_decompressStream(dctx, &output, &input); 64 | 65 | if (ZSTD_isError(result)) 66 | { 67 | std::cerr << "Decompression error: " << ZSTD_getErrorName(result) << std::endl; 68 | ZSTD_freeDCtx(dctx); 69 | return false; 70 | } 71 | 72 | fout.write(out_buffer.data(), output.pos); 73 | read_bytes -= input.pos; 74 | src += input.pos; 75 | } 76 | } 77 | 78 | // Clean up 79 | ZSTD_freeDCtx(dctx); 80 | return true; 81 | } 82 | 83 | bool merge_directories(fs::path& source_path, fs::path& destination_path) 84 | { 85 | try 86 | { 87 | if (!fs::exists(source_path) || !fs::is_directory(source_path)) 88 | { 89 | return false; 90 | } 91 | 92 | // Create the destination directory if it doesn't exist 93 | if (!fs::exists(destination_path)) 94 | { 95 | fs::create_directories(destination_path); 96 | } 97 | 98 | // Iterate through the source directory recursively 99 | for (const auto& entry : fs::recursive_directory_iterator(source_path)) 100 | { 101 | const fs::path& source_entry_path = entry.path(); 102 | fs::path destination_entry_path 103 | = destination_path / fs::relative(source_entry_path, source_path); 104 | 105 | if (fs::is_directory(source_entry_path)) 106 | { 107 | // Create directories in the destination if they don't exist 108 | if (!fs::exists(destination_entry_path)) 109 | { 110 | fs::create_directory(destination_entry_path); 111 | } 112 | } 113 | else if (fs::is_regular_file(source_entry_path)) 114 | { 115 | // Copy/replace files from source to destination 116 | fs::copy_file(source_entry_path, 117 | destination_entry_path, 118 | fs::copy_options::overwrite_existing); 119 | } 120 | } 121 | 122 | fs::remove_all(source_path); 123 | 124 | return true; 125 | } 126 | catch (const fs::filesystem_error& e) 127 | { 128 | std::cerr << "Filesystem error: " << e.what() << std::endl; 129 | return false; 130 | } 131 | } 132 | 133 | 134 | em::val install_conda_file(const std::string& zstd_file_path, 135 | const std::string& working_dir, 136 | const std::string& prefix) 137 | { 138 | auto output = em::val::array(); 139 | fs::path output_dir(working_dir); 140 | fs::path zstd_path(zstd_file_path); 141 | fs::path output_file = output_dir / "pkg.tar"; 142 | 143 | bool success = decompress_zstd(zstd_path, output_file); 144 | if (!success) 145 | { 146 | return output; 147 | } 148 | FILE* output_file_ptr = fopen(output_file.c_str(), "r"); 149 | 150 | untar_impl(output_file_ptr, output_dir.c_str(), output); 151 | 152 | std::vector dir_names = { "etc", "share" }; 153 | for (size_t i = 0; i < dir_names.size(); i++) 154 | { 155 | auto source_dir_path = output_dir / fs::path(dir_names[i]); 156 | auto dest_dir_path = fs::path(dir_names[i]); 157 | merge_directories(source_dir_path, dest_dir_path); 158 | } 159 | 160 | auto site_packages_dir_path = output_dir / "site-packages"; 161 | if (fs::exists(site_packages_dir_path)) 162 | { 163 | auto site_packages_dest = fs::path(prefix) / "lib/python3.11/site-packages"; 164 | bool check = merge_directories(site_packages_dir_path, site_packages_dest); 165 | if (!check) 166 | { 167 | std::cerr << " Failed to copy package to site-packages directory: " 168 | << site_packages_dir_path << std::endl; 169 | } 170 | } 171 | std::fclose(output_file_ptr); 172 | fs::remove_all(output_dir); 173 | return output; 174 | } 175 | } -------------------------------------------------------------------------------- /src/js_timestamp.cpp: -------------------------------------------------------------------------------- 1 | #define PYJS_JS_UTC_TIMESTAMP "2024-10-18 09:14:27.486802" -------------------------------------------------------------------------------- /src/runtime.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | PYBIND11_EMBEDDED_MODULE(pyjs_core, m) 7 | { 8 | pyjs::export_pyjs_module(m); 9 | } 10 | 11 | EMSCRIPTEN_BINDINGS(my_module) 12 | { 13 | pyjs::export_js_module(); 14 | } 15 | -------------------------------------------------------------------------------- /src/untar.cpp: -------------------------------------------------------------------------------- 1 | // from https://github.com/libarchive/libarchive/blob/master/contrib/untar.c 2 | /* 3 | * This file is in the public domain. Use it as you see fit. 4 | */ 5 | 6 | /* 7 | * "untar" is an extremely simple tar extractor: 8 | * * A single C source file, so it should be easy to compile 9 | * and run on any system with a C compiler. 10 | * * Extremely portable standard C. The only non-ANSI function 11 | * used is mkdir(). 12 | * * Reads basic ustar tar archives. 13 | * * Does not require libarchive or any other special library. 14 | * 15 | * To compile: cc -o untar untar.c 16 | * 17 | * Usage: untar 18 | * 19 | * In particular, this program should be sufficient to extract the 20 | * distribution for libarchive, allowing people to bootstrap 21 | * libarchive on systems that do not already have a tar program. 22 | * 23 | * To unpack libarchive-x.y.z.tar.gz: 24 | * * gunzip libarchive-x.y.z.tar.gz 25 | * * untar libarchive-x.y.z.tar 26 | * 27 | * Written by Tim Kientzle, March 2009. 28 | * 29 | * Released into the public domain. 30 | */ 31 | 32 | /* These are all highly standard and portable headers. */ 33 | #include 34 | #include 35 | #include 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | /* This is for mkdir(); this may need to be changed for some platforms. */ 43 | #include /* For mkdir() */ 44 | #include 45 | 46 | #include 47 | #include 48 | 49 | #include 50 | 51 | namespace em = emscripten; 52 | 53 | namespace pyjs 54 | { 55 | 56 | 57 | 58 | // function to check if cstring ends with ".so" 59 | bool is_so_file(const char * cstr){ 60 | std::string str(cstr); 61 | return str.size() > 3 && str.substr(str.size() - 3) == ".so"; 62 | } 63 | 64 | 65 | 66 | /* Parse an octal number, ignoring leading and trailing nonsense. */ 67 | static int 68 | parseoct(const char *p, size_t n) 69 | { 70 | int i = 0; 71 | 72 | while ((*p < '0' || *p > '7') && n > 0) { 73 | ++p; 74 | --n; 75 | } 76 | while (*p >= '0' && *p <= '7' && n > 0) { 77 | i *= 8; 78 | i += *p - '0'; 79 | ++p; 80 | --n; 81 | } 82 | return (i); 83 | } 84 | 85 | /* Returns true if this is 512 zero bytes. */ 86 | static int 87 | is_end_of_archive(const char *p) 88 | { 89 | int n; 90 | for (n = 511; n >= 0; --n) 91 | if (p[n] != '\0') 92 | return (0); 93 | return (1); 94 | } 95 | 96 | /* Create a directory, including parent directories as necessary. */ 97 | static void 98 | create_dir(const char *pathname, int mode, const char * root_dir) 99 | { 100 | std::error_code ec; 101 | if(root_dir != nullptr){ 102 | 103 | std::filesystem::path full_path =std::filesystem::path(root_dir) / std::filesystem::path(pathname); 104 | std::filesystem::create_directories(full_path, ec); 105 | } 106 | else{ 107 | std::filesystem::create_directories(pathname, ec); 108 | } 109 | 110 | if (ec){ 111 | fprintf(stderr, "Could not create directory %s\n", pathname); 112 | throw std::runtime_error("could not create directory"); 113 | } 114 | } 115 | 116 | /* Create a file, including parent directory as necessary. */ 117 | static FILE * 118 | create_file( 119 | char *pathname_in, int mode, const char * root_dir 120 | ) 121 | { 122 | //std::cout << "Creating file " << pathname_in << " with mode " << mode << std::endl; 123 | const auto longlink_path = std::filesystem::path("@LongLink"); 124 | 125 | std::string pathname(pathname_in); 126 | std::filesystem::path full_path =std::filesystem::path(root_dir) / std::filesystem::path(pathname); 127 | pathname = full_path.string(); 128 | 129 | if ( 130 | // if this pathname_in isn't @LongLink... 131 | longlink_path != std::filesystem::path(pathname_in) 132 | // ... and there exists an @LongLink file 133 | && std::filesystem::exists(longlink_path) 134 | ) { 135 | 136 | std::cout << " @LongLink detected" << std::endl; 137 | std::cout << " Renaming " << pathname_in << " to @LongLink contents"; 138 | std::cout << std::endl; 139 | 140 | // then set the pathname to the contents of @LongLink... 141 | std::ifstream longlink_stream(longlink_path); 142 | pathname = std::string( 143 | std::istreambuf_iterator(longlink_stream), 144 | std::istreambuf_iterator() 145 | ); 146 | // ... and delete the @LongLink file 147 | std::filesystem::remove(longlink_path); 148 | } 149 | //std::cout << "Creating file " << pathname << std::endl; 150 | FILE *f; 151 | f = fopen(pathname.c_str(), "wb+"); 152 | if (f == NULL) { 153 | /* Try creating parent dir and then creating file. */ 154 | create_dir( 155 | std::filesystem::path(pathname).parent_path().c_str(), 156 | 0755, 157 | nullptr 158 | ); 159 | f = fopen(pathname.c_str(), "wb+"); 160 | } 161 | 162 | return (f); 163 | } 164 | 165 | /* Verify the tar checksum. */ 166 | static int 167 | verify_checksum(const char *p) 168 | { 169 | int n, u = 0; 170 | for (n = 0; n < 512; ++n) { 171 | if (n < 148 || n > 155) 172 | /* Standard tar checksum adds unsigned bytes. */ 173 | u += ((unsigned char *)p)[n]; 174 | else 175 | u += 0x20; 176 | 177 | } 178 | return (u == parseoct(p + 148, 8)); 179 | } 180 | 181 | /* Extract a tar archive. */ 182 | void untar_impl(FILE *a, const char *path, em::val & shared_libraraies) 183 | { 184 | 185 | 186 | std::filesystem::path root_dir(path); 187 | 188 | 189 | char buff[512]; 190 | FILE *f = NULL; 191 | size_t bytes_read; 192 | int filesize; 193 | 194 | //printf("Extracting from %s\n", path); 195 | 196 | for (std::size_t iter=0; ;++iter) { 197 | bytes_read = fread(buff, 1, 512, a); 198 | if (bytes_read < 512) { 199 | fprintf(stderr, 200 | "Short read on %s: expected 512, got %d\n", 201 | path, (int)bytes_read); 202 | throw std::runtime_error("untar error: short read error: expected 512"); 203 | } 204 | if (is_end_of_archive(buff)) { 205 | return ; 206 | } 207 | if (!verify_checksum(buff)) { 208 | fprintf(stderr, "Checksum failure\n"); 209 | throw std::runtime_error("checksum failure"); 210 | } 211 | filesize = parseoct(buff + 124, 12); 212 | switch (buff[156]) { 213 | case '1': 214 | printf(" Ignoring hardlink %s\n", buff); 215 | break; 216 | case '2': 217 | printf(" Ignoring symlink %s\n", buff); 218 | break; 219 | case '3': 220 | printf(" Ignoring character device %s\n", buff); 221 | break; 222 | case '4': 223 | printf(" Ignoring block device %s\n", buff); 224 | break; 225 | case '5': 226 | //printf(" Extracting dir %s\n", buff); 227 | create_dir(buff, parseoct(buff + 100, 8), path); 228 | filesize = 0; 229 | break; 230 | case '6': 231 | printf(" Ignoring FIFO %s\n", buff); 232 | break; 233 | default: 234 | 235 | std::filesystem::path so_path = root_dir / std::filesystem::path(buff); 236 | 237 | 238 | 239 | //std::cout<<"considering file: "<("push", em::val(so_path.string())); 248 | } 249 | 250 | f = create_file(buff, parseoct(buff + 100, 8), path); 251 | break; 252 | } 253 | while (filesize > 0) { 254 | bytes_read = fread(buff, 1, 512, a); 255 | if (bytes_read < 512) { 256 | fprintf(stderr, 257 | "Short read on %s: Expected 512, got %d\n", 258 | path, (int)bytes_read); 259 | throw std::runtime_error("untar error: short read error: expected 512"); 260 | } 261 | if (filesize < 512) 262 | bytes_read = filesize; 263 | if (f != NULL) { 264 | if (fwrite(buff, 1, bytes_read, f) 265 | != bytes_read) 266 | { 267 | fprintf(stderr, "Failed write\n"); 268 | fclose(f); 269 | f = NULL; 270 | throw std::runtime_error("failed write"); 271 | } 272 | } 273 | filesize -= bytes_read; 274 | } 275 | if (f != NULL) { 276 | fclose(f); 277 | f = NULL; 278 | } 279 | } 280 | } 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | em::val untar(const std::string &tar_path, const std::string &path){ 292 | 293 | auto shared_libraraies = em::val::array(); 294 | 295 | 296 | auto file = gzopen(tar_path.c_str(), "rb"); 297 | auto decompressed_tmp_file = std::tmpfile(); 298 | 299 | 300 | // unzip into temporary file 301 | //std::cout<<"inflate "< None: 13 | """ 14 | Create a new JsValue from a python value. 15 | Args: 16 | value: The value to convert to a JsValue. 17 | If the value is a primitive type (int, float, str, bool) it will be converted to the corresponding javascript type. 18 | For any other python object, it will be converted to the javascript class `pyjs.pyobject` which is a wrapper around the python object on the javascript side. 19 | """ 20 | ... 21 | 22 | # def __getitem__(self, key: Union[str, int]) -> Any: 23 | 24 | def __call__(self, *args: Any) -> Any: 25 | """ 26 | Call the javascript object as a function. 27 | 28 | Args: 29 | *args: The arguments to pass to the function. 30 | 31 | Returns: 32 | The result of the function call. 33 | """ 34 | ... 35 | 36 | def __str__(self) -> str: 37 | """ 38 | Convert the javascript object to a string. 39 | """ 40 | ... 41 | 42 | def __repr__(self) -> str: 43 | """ 44 | Convert the javascript object to a string. 45 | """ 46 | ... 47 | 48 | def __len__(self) -> int: 49 | """ 50 | Get the length of the javascript object. 51 | """ 52 | ... 53 | 54 | def __contains__(self, q: Any) -> bool: 55 | """ 56 | Check if the javascript object contains a value. 57 | """ 58 | ... 59 | 60 | def __eq__(self, q: Any) -> bool: 61 | """ 62 | Check if the javascript object is equal to a value. 63 | """ 64 | ... 65 | 66 | def new(self, *args: Any) -> Any: 67 | """ 68 | Create a new instance of a JavaScript class. 69 | """ 70 | ... 71 | 72 | def __iter__(self) -> Any: 73 | """ 74 | Get an iterator for the javascript object. 75 | """ 76 | ... 77 | 78 | def __next__(self) -> Any: 79 | """ 80 | Get the next value from the iterator. 81 | """ 82 | ... 83 | 84 | def __delattr__(self, __name: str) -> None: 85 | """ 86 | Delete an attribute from the javascript object. 87 | """ 88 | ... 89 | 90 | def __delitem__(self, __name: str) -> None: 91 | """ 92 | Delete an item from the javascript object. 93 | """ 94 | ... 95 | 96 | def __await__(self) -> Any: 97 | """ 98 | Wait for the javascript object to resolve. 99 | """ 100 | ... -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emscripten-forge/pyjs/b7f3a08768a1116ca856a512bca231247e3a4d25/tests/__init__.py -------------------------------------------------------------------------------- /tests/atests/__init__.py: -------------------------------------------------------------------------------- 1 | from .async_tests import * 2 | -------------------------------------------------------------------------------- /tests/atests/async_tests.py: -------------------------------------------------------------------------------- 1 | import pyjs 2 | from pathlib import Path 3 | import json 4 | import io 5 | import tarfile 6 | 7 | async def test_stuff(): 8 | assert 2 == 2 9 | 10 | 11 | async def test_stuff_2(): 12 | assert 2 == 2 13 | 14 | 15 | async def test_callbacks_in_async(): 16 | def py_func(arg1, arg2): 17 | return arg1 * 2 + arg2 18 | 19 | js_func, cleanup = pyjs.create_callable(py_func) 20 | 21 | async_js_function = pyjs.js.Function( 22 | """ 23 | return async function(callback, arg){ 24 | let res = callback(arg * 2, 42); 25 | return res 26 | } 27 | """ 28 | )() 29 | 30 | result = await async_js_function(js_func, 42) 31 | assert result == 42 * 2 * 2 + 42 32 | cleanup.delete() 33 | 34 | 35 | async def test_callbacks_in_async(): 36 | 37 | async_js_function = pyjs.js.Function( 38 | """ 39 | return async function(py_dict){ 40 | return py_dict.get('value') 41 | } 42 | """ 43 | )() 44 | 45 | pyval = {"value": 42} 46 | result = await async_js_function(pyval) 47 | assert result == 42 48 | 49 | 50 | async def trigger_js_tests(): 51 | 52 | js_test_path = Path(__file__).parents[1] / "jtests" / "test_main.js" 53 | with open(js_test_path) as f: 54 | content = f.read() 55 | js_tests_async_main = pyjs.js.Function(content)() 56 | 57 | js_tests_return_code = await js_tests_async_main() 58 | assert js_tests_return_code == 0 59 | 60 | -------------------------------------------------------------------------------- /tests/js_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pyjs 3 | 4 | 5 | async def main(): 6 | 7 | js_test_path = Path(__file__).parents[1] / "js_tests" / "test_main.js" 8 | with open(js_test_path) as f: 9 | content = f.read() 10 | js_tests_async_main = pyjs.js.Function(content)() 11 | 12 | js_pyjs = pyjs.js.pyjs 13 | js_tests_return_code = await js_tests_async_main(js_pyjs) 14 | return js_tests_return_code 15 | -------------------------------------------------------------------------------- /tests/js_tests/test_main.js: -------------------------------------------------------------------------------- 1 | 2 | let pyjs = null 3 | tests = {} 4 | ntests = {} 5 | 6 | function assert_eq(a,b){ 7 | if (a !== b){ 8 | throw Error(`assert_eq failed: "${a} === ${b}" is violated`) 9 | } 10 | } 11 | 12 | function assert_deep_json_eq(a,b){ 13 | const replacer = (key, value) => 14 | value instanceof Object && !(value instanceof Array) ? 15 | Object.keys(value) 16 | .sort() 17 | .reduce((sorted, key) => { 18 | sorted[key] = value[key]; 19 | return sorted 20 | }, {}) : 21 | value; 22 | 23 | let jsa = JSON.stringify(a, replacer); 24 | let jsb = JSON.stringify(b, replacer); 25 | if(jsa !== jsb) 26 | { 27 | throw Error(`assert_eq failed: "${a}" and "${b}" are not deeply equal`) 28 | } 29 | } 30 | 31 | function assert_feq(v1, v2, epsilon) { 32 | if (epsilon == null) { 33 | epsilon = 0.000001; 34 | } 35 | if(! Math.abs(v1 - v2) < epsilon) 36 | { 37 | throw Error(`assert_feq failed: "${a} === ${b}" is violated`) 38 | } 39 | }; 40 | 41 | function assert_undefined(a){ 42 | if (a !== undefined){ 43 | throw Error(`assert_undefined failed: "${a} !== undefined" is violated`) 44 | } 45 | } 46 | 47 | tests.test_eval_fundamentals = async function(){ 48 | 49 | // string 50 | let hello_world = pyjs.eval("'hello world'"); 51 | assert_eq(hello_world, "hello world") 52 | let string_one = pyjs.eval("'1'"); 53 | assert_eq(string_one, "1") 54 | 55 | // int 56 | let one = pyjs.eval("1"); 57 | assert_eq(one, 1); 58 | let neg_one = pyjs.eval("-1"); 59 | assert_eq(neg_one, -1); 60 | 61 | // bool 62 | let trueval = pyjs.eval("True"); 63 | assert_eq(trueval, true); 64 | let falseval = pyjs.eval("False"); 65 | assert_eq(falseval, false); 66 | 67 | 68 | // float 69 | let fone = pyjs.eval("1.0"); 70 | assert_eq(fone, 1.0); 71 | 72 | // none 73 | let none = pyjs.eval("None"); 74 | assert_undefined(none); 75 | } 76 | 77 | tests.test_eval_nested = async function(){ 78 | assert_deep_json_eq(pyjs.to_js(pyjs.eval("[False,1,[1,2,'three']]")),[false,1,[1,2,'three']]) 79 | assert_deep_json_eq(Object.fromEntries(pyjs.to_js(pyjs.eval("{'k':[1,2,3]}"))),{'k':[1,2,3]}) 80 | } 81 | 82 | tests.test_import_pyjs = async function(){ 83 | pyjs.exec("import pyjs"); 84 | } 85 | 86 | tests.test_eval_exec = async function(){ 87 | let res = pyjs.exec_eval( 88 | `import pyjs 89 | def fubar(): 90 | return 42 91 | fubar() 92 | `); 93 | assert_eq(res, 42); 94 | } 95 | 96 | 97 | tests.test_async_call_simple = async function(){ 98 | 99 | let asleep = pyjs.exec_eval(` 100 | import asyncio 101 | async def fubar(arg): 102 | await asyncio.sleep(0.5) 103 | return arg * 2 104 | fubar 105 | `) 106 | let res = await asleep.py_call(21) 107 | assert_eq(res, 42); 108 | } 109 | 110 | 111 | tests.test_async_eval_exec = async function(){ 112 | let res = await pyjs.async_exec_eval(` 113 | await asyncio.sleep(0.5) 114 | 42 115 | `); 116 | assert_eq(res, 42); 117 | } 118 | async function async_main(pyjs_module){ 119 | pyjs = pyjs_module 120 | var name; 121 | for (name in tests) { 122 | 123 | console.log("tests `" + name + "`:"); 124 | try{ 125 | await tests[name]() 126 | } 127 | catch(error){ 128 | console.error("tests `" + name + "` failed with",error.message); 129 | return 1 130 | } 131 | } 132 | return 0 133 | } 134 | return async_main -------------------------------------------------------------------------------- /tests/main.py: -------------------------------------------------------------------------------- 1 | import pyjs 2 | import time 3 | from atests import * 4 | from js_tests import main as js_main 5 | 6 | 7 | async def main(): 8 | run_sync_pytest_tests = True 9 | run_async_js_tests = True 10 | run_async_py_tests = True 11 | 12 | if run_async_js_tests: 13 | # run js tests 14 | retcode = await js_main() 15 | if retcode != 0: 16 | return retcode 17 | 18 | if run_async_py_tests: 19 | import inspect 20 | import traceback 21 | 22 | bar = "".join(["="] * 40) 23 | print(f"{bar}\nRUN ASYNC TESTS:\n{bar}") 24 | n_failed, n_tests = 0, 0 25 | t0 = time.time() 26 | for k, v in globals().items(): 27 | if k.startswith("test_") and inspect.iscoroutinefunction(v): 28 | try: 29 | print(f"RUN: {k}") 30 | await v() 31 | except Exception as e: 32 | traceback.print_exc() 33 | n_failed += 1 34 | n_tests += 1 35 | t1 = time.time() 36 | d = t1 - t0 37 | print( 38 | f"============================== {n_tests-n_failed} passed in {d:.01}s ==============================" 39 | ) 40 | 41 | if n_failed > 0: 42 | return 1 43 | 44 | if run_sync_pytest_tests: 45 | import pyjs 46 | import pytest 47 | 48 | import os 49 | 50 | dir_path = os.path.dirname(os.path.realpath(__file__)) 51 | os.chdir(dir_path) 52 | 53 | # import pytest_asyncio 54 | 55 | # start the tests 56 | os.environ["NO_COLOR"] = "1" 57 | 58 | specific_test = None 59 | args = [] 60 | args = ["-s", f"{dir_path}/tests"] 61 | if specific_test is not None: 62 | args += ["-k", str(specific_test)] 63 | 64 | # args += ["-p", "pytest_asyncio"] 65 | 66 | retcode = pytest.main(args, plugins=[]) 67 | if retcode != pytest.ExitCode.OK: 68 | raise RuntimeError(f"pytest failed with return code: {retcode}") 69 | 70 | return 0 71 | -------------------------------------------------------------------------------- /tests/test.js.in: -------------------------------------------------------------------------------- 1 | // import test from 'ava'; 2 | test = require('ava') 3 | 4 | 5 | var interpreter = null 6 | var pyjs = null 7 | var main_scope = null 8 | 9 | // This runs before all tests 10 | test.before(async function (t) { 11 | var createModule = require('./pyjs_runtime_node.js') 12 | pyjs = await createModule() 13 | global.Module = pyjs 14 | await import('./python_data.js'); 15 | await pyjs.init() 16 | return pyjs 17 | }); 18 | 19 | test.after.always('guaranteed cleanup', t => { 20 | pyjs.cleanup() 21 | }); 22 | 23 | 24 | test.beforeEach(t => { 25 | // This runs before each test 26 | main_scope = pyjs.main_scope() 27 | }); 28 | 29 | test.afterEach(t => { 30 | // This runs after each test 31 | main_scope.delete() 32 | }); 33 | 34 | 35 | test.serial('test-basics',async function (t) { 36 | 37 | pyjs.exec("import numpy\nimport pyjs", main_scope) 38 | var s0 = pyjs.eval("numpy.ones([640, 480]).shape[0]", main_scope) 39 | t.is(s0,640) 40 | s1 = pyjs.eval("numpy.ones([640, 480]).shape[1]", main_scope) 41 | t.is(s1,480) 42 | }); 43 | 44 | 45 | test.serial('test-exceptions',async function (t) { 46 | var err = false; 47 | try{ 48 | pyjs.exec("raise RuntimeError(\"this is intended\")", main_scope) 49 | } 50 | catch(e) 51 | { 52 | err = true 53 | t.is(e.message.startsWith("RuntimeError: this is intended"),true) 54 | } 55 | t.is(err, true) 56 | }); 57 | 58 | 59 | 60 | test.serial('test-getattr',async function (t) { 61 | 62 | pyjs.exec("import numpy\nimport pyjs", main_scope) 63 | var arr = pyjs.eval("numpy.ones([640, 480])", main_scope) 64 | var shape = arr.shape 65 | t.is(shape !== undefined, true) 66 | shape.delete() 67 | arr.delete() 68 | 69 | }); 70 | 71 | 72 | test.serial('test-call',async function (t) { 73 | var square = pyjs.eval("lambda a : a * a", main_scope) 74 | res = square.py_call(10) 75 | t.is(res,100) 76 | }); 77 | 78 | 79 | 80 | 81 | 82 | test.serial('test-call-with-py-objects',async function (t) { 83 | pyjs.exec(`def mysum(l): 84 | if not isinstance(l, list): 85 | raise RuntimeError(f'{l} is not list') 86 | return sum(l) 87 | `, main_scope) 88 | var list = pyjs.eval("[1,2,3]", main_scope) 89 | var mysum = pyjs.eval("mysum", main_scope) 90 | 91 | res = mysum.py_call(list) 92 | list.delete() 93 | mysum.delete() 94 | t.is(res, 6) 95 | }); 96 | 97 | 98 | test.serial('test-call-with-kwargs',async function (t) { 99 | pyjs.exec(`def myfunc(arg0, arg1, arg2, arg3): 100 | assert arg0==10 101 | assert arg1==2 102 | assert arg2==3 103 | assert arg3=='four' 104 | return arg0 + arg1 + arg2 105 | `, main_scope) 106 | var myfunc = pyjs.eval("myfunc", main_scope) 107 | 108 | var args = [10,2] 109 | var kwargs = {"arg2":3,"arg3":"four"} 110 | 111 | res = myfunc.py_apply(args, kwargs) 112 | 113 | myfunc.delete() 114 | t.is(res, 15) 115 | }); 116 | 117 | 118 | 119 | test.serial('test-getitem',async function (t) { 120 | 121 | pyjs.exec("import numpy\nimport pyjs", main_scope) 122 | var arr = pyjs.eval("numpy.ones([64, 48])", main_scope) 123 | var shape = arr.shape 124 | var s0 = shape.get(0) 125 | t.is(s0,64) 126 | var s1 = shape.get(1) 127 | t.is(s1,48) 128 | shape.delete() 129 | arr.delete() 130 | }); 131 | 132 | 133 | test.serial('test-getitem-multi-key',async function (t) { 134 | 135 | var py_code = ` 136 | import numpy 137 | class Foo(object): 138 | def __init__(self): 139 | self.a = numpy.identity(3) 140 | def __getitem__(self, key): 141 | return int(self.a[key]) 142 | ` 143 | ; 144 | 145 | 146 | pyjs.exec(py_code, main_scope) 147 | var foo = pyjs.eval("Foo()", main_scope) 148 | t.is(foo.get(0,1),0) 149 | t.is(foo.get(1,0),0) 150 | t.is(foo.get(0,0),1) 151 | t.is(foo.get(1,1),1) 152 | foo.delete() 153 | }); 154 | 155 | 156 | test.serial('test-main-scope',async function (t) { 157 | 158 | var py_code = ` 159 | assert pyjs is not None 160 | assert asyncio is not None 161 | `; 162 | pyjs.exec(py_code, main_scope) 163 | 164 | t.pass() 165 | }); 166 | 167 | 168 | 169 | test.serial('test-async',async function (t) { 170 | 171 | var py_code = ` 172 | async def muladd_inner(*args): 173 | return args[0]*args[1] + args[2] 174 | 175 | async def muladd(a, b, c): 176 | await asyncio.sleep(0.1) 177 | return await muladd_inner(a,b,c) 178 | `; 179 | 180 | 181 | 182 | pyjs.exec(py_code, main_scope) 183 | var muladd = pyjs.eval("muladd", main_scope) 184 | 185 | var r42 = await muladd.py_call_async(4,10,2) 186 | t.is(r42, 42) 187 | 188 | var p43 = muladd.py_apply_async([4,10], {c:3}) 189 | t.is(await p43, 43) 190 | 191 | 192 | }); 193 | 194 | 195 | test.serial('test-async-errors',async function (t) { 196 | 197 | var py_code = ` 198 | async def muladd_inner(*args): 199 | return args[0]*args[1] + args[2] + c 200 | 201 | async def muladd(a, b, c): 202 | await asyncio.sleep(0.1) 203 | return await muladd_inner(a,b,c) 204 | `; 205 | 206 | pyjs.exec(py_code, main_scope) 207 | var muladd = pyjs.eval("muladd", main_scope) 208 | 209 | var err = false 210 | try{ 211 | await muladd.py_call_async(4,10,2) 212 | } 213 | catch(e){ 214 | err = true 215 | t.is(e, 'NameError("name \'c\' is not defined")') 216 | } 217 | t.is(err, true) 218 | 219 | 220 | }); 221 | 222 | 223 | 224 | 225 | test.serial('test-numpy-to-typed-array',async function (t) { 226 | 227 | 228 | 229 | pyjs.exec(` 230 | import numpy 231 | arr = numpy.arange(5) 232 | print(arr.dtype) 233 | typed_array = pyjs.buffer_to_js_typed_array(arr) 234 | 235 | `, main_scope) 236 | 237 | var typed_array = pyjs.eval("typed_array", main_scope); 238 | 239 | t.is(typed_array.constructor.name, "Int32Array"); 240 | 241 | var shouldArray = new Int32Array([0,1,2,3,4]); 242 | 243 | t.deepEqual(typed_array, shouldArray); 244 | }); 245 | 246 | 247 | 248 | test.serial('test-numpy-to-typed-array-types',async function (t) { 249 | 250 | type_mapping = { 251 | "uint8": "Uint8Array", 252 | "uint16": "Uint16Array", 253 | "uint32": "Uint32Array", 254 | // "uint64": "BigUint64Array", 255 | "int8": "Int8Array", 256 | "int16": "Int16Array", 257 | "int32": "Int32Array", 258 | // "int64": "BigInt64Array", 259 | "float32": "Float32Array", 260 | "float64": "Float64Array" 261 | } 262 | 263 | for (const [np_dtype, js_cls] of Object.entries(type_mapping)) { 264 | 265 | pyjs.exec(` 266 | import numpy 267 | arr = numpy.arange(5, dtype='${np_dtype}') 268 | typed_array = pyjs.buffer_to_js_typed_array(arr) 269 | 270 | `, main_scope) 271 | 272 | var typed_array = pyjs.eval("typed_array", main_scope); 273 | 274 | t.is(typed_array.constructor.name, js_cls); 275 | 276 | 277 | } 278 | }); 279 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pyjs 2 | import json 3 | try: 4 | import numpy 5 | has_numpy = True 6 | except ImportError: 7 | has_numpy = False 8 | 9 | 10 | js_assert_eq = pyjs.js.Function( 11 | "output", 12 | "expected_output", 13 | """ 14 | let eq = output === expected_output; 15 | if(!eq){ 16 | console.log("assertion failed:", output,"!=",expected_output) 17 | } 18 | return eq 19 | """, 20 | ) 21 | 22 | 23 | def to_js_to_py(x): 24 | return pyjs.to_py(pyjs.to_js(x)) 25 | 26 | 27 | def nullary(body): 28 | return pyjs.js.Function(body)() 29 | 30 | 31 | def eval_jsfunc(body): 32 | return pyjs.js.Function("return " + body)() 33 | 34 | 35 | def ensure_js(val): 36 | if not isinstance(val, pyjs.JsValue): 37 | if not isinstance(val, str): 38 | return pyjs.JsValue(val) 39 | else: 40 | return eval_jsfunc(val) 41 | else: 42 | return val 43 | 44 | if has_numpy: 45 | 46 | def converting_array_eq(x, should): 47 | return array_eq(numpy.array(x), should) 48 | 49 | def converting_array_feq(x, should): 50 | return array_feq(numpy.array(x), should) 51 | 52 | def array_eq(x, should): 53 | return x.dtype == should.dtype and numpy.array_equal(x, should) 54 | 55 | 56 | def array_feq(x, should): 57 | return x.dtype == should.dtype and numpy.allclose(x, should) 58 | 59 | 60 | def nested_eq(x, should): 61 | # stupid low effort impls 62 | 63 | x_string = json.dumps(x, sort_keys=True, indent=2) 64 | should_string = json.dumps(should, sort_keys=True, indent=2) 65 | return x_string == should_string 66 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emscripten-forge/pyjs/b7f3a08768a1116ca856a512bca231247e3a4d25/tests/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from ..test_utils import * 2 | -------------------------------------------------------------------------------- /tests/tests/test_conversion.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pyjs 3 | 4 | from .conftest import * 5 | 6 | 7 | class TestPyToJs: 8 | @pytest.mark.parametrize( 9 | "test_input", 10 | [-1, 1, 1.0, 0, True, False, "0"], 11 | ) 12 | def test_fundamentals(self, test_input): 13 | output = pyjs.to_js(test_input) 14 | assert js_assert_eq(output, pyjs.JsValue(test_input)) 15 | 16 | def test_none(self): 17 | t = pyjs.to_js(None) 18 | assert pyjs.pyjs_core._module._is_undefined(t) == True 19 | --------------------------------------------------------------------------------