├── .circleci └── config.yml ├── .clang-format ├── .github ├── actions │ ├── setup-mingw │ │ └── action.yml │ ├── setup-poetry │ │ └── action.yml │ └── upload-artifacts │ │ └── action.yml └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .style.yapf ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── check_memory.py ├── module.c ├── poetry.lock ├── pyproject.toml ├── quickjs └── __init__.py ├── setup.py └── test_quickjs.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/python:3.11 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | - run: 13 | name: Init git submodules 14 | command: | 15 | git submodule update --init --recursive 16 | 17 | - restore_cache: 18 | keys: 19 | - v1-3.11-dependencies-{{ checksum "poetry.lock" }} 20 | 21 | - run: 22 | name: Install dependencies 23 | command: | 24 | poetry config --local virtualenvs.in-project true 25 | poetry install 26 | 27 | - save_cache: 28 | paths: 29 | - ./.venv 30 | - ~/.cache/pypoetry 31 | key: v1-3.11-dependencies-{{ checksum "poetry.lock" }} 32 | 33 | - run: 34 | name: Tests 35 | command: | 36 | make test 37 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | # For calculating line lengths. 4 | IndentWidth: 4 5 | TabWidth: 4 6 | 7 | UseTab: ForIndentation 8 | ColumnLimit: 100 9 | 10 | AlignOperands: true 11 | AllowShortFunctionsOnASingleLine: false 12 | BinPackArguments: false 13 | BinPackParameters: false 14 | BreakBeforeBinaryOperators: NonAssignment 15 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 16 | Standard: Cpp11 17 | -------------------------------------------------------------------------------- /.github/actions/setup-mingw/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup MinGW" 2 | 3 | inputs: 4 | key-base: 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - uses: actions/cache@v3 11 | id: cache-mingw 12 | with: 13 | path: mingw 14 | key: ${{ inputs.key-base }}-mingw${{ hashFiles('mingw') }} 15 | - run: New-Item 'mingw' -ItemType Directory -Force | Out-Null 16 | shell: pwsh 17 | if: steps.cache-mingw.outputs.cache-hit != 'true' 18 | - run: (New-Object Net.WebClient).DownloadFile('https://github.com/brechtsanders/winlibs_mingw/releases/download/11.3.0-14.0.3-10.0.0-msvcrt-r3/winlibs-x86_64-posix-seh-gcc-11.3.0-mingw-w64msvcrt-10.0.0-r3.7z', 'mingw.7z') 19 | shell: pwsh 20 | if: steps.cache-mingw.outputs.cache-hit != 'true' 21 | - run: 7z x 'mingw.7z' -o'mingw' -aoa 22 | shell: pwsh 23 | if: steps.cache-mingw.outputs.cache-hit != 'true' 24 | - run: $env:PATH = 'mingw\\mingw64\\bin;' + $env:PATH 25 | shell: pwsh 26 | -------------------------------------------------------------------------------- /.github/actions/setup-poetry/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Poetry" 2 | 3 | inputs: 4 | key-base: 5 | required: true 6 | use-pipx: 7 | required: true 8 | use-specific-python-version: 9 | required: true 10 | specific-python-version: 11 | required: false 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - run: pipx install poetry 17 | shell: bash 18 | if: inputs.use-pipx == 'true' 19 | - run: pip install poetry 20 | shell: bash 21 | if: inputs.use-pipx == 'false' 22 | - run: poetry config --local virtualenvs.in-project true 23 | shell: bash 24 | - run: echo "v=$(poetry --version | cut -d' ' -f3 | cut -d')' -f1)" >> "$GITHUB_OUTPUT" 25 | shell: bash 26 | id: poetry-version 27 | - uses: actions/cache@v3 28 | id: cache-poetry 29 | with: 30 | path: .venv 31 | key: ${{ inputs.key-base }}-po${{ steps.poetry-version.outputs.v }}-lock${{ hashFiles('poetry.lock') }} 32 | - run: poetry env use python${{ inputs.specific-python-version }} 33 | shell: bash 34 | if: inputs.use-specific-python-version == 'true' && steps.cache-poetry.outputs.cache-hit != 'true' 35 | - run: poetry install 36 | shell: bash 37 | if: steps.cache-poetry.outputs.cache-hit != 'true' 38 | -------------------------------------------------------------------------------- /.github/actions/upload-artifacts/action.yml: -------------------------------------------------------------------------------- 1 | name: "Make and upload artifacts" 2 | 3 | inputs: 4 | make-binary: 5 | required: true 6 | repair-manylinux: 7 | required: false 8 | default: false 9 | manylinux-target: 10 | required: false 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - run: make distribute-binary 16 | shell: bash 17 | if: inputs.make-binary == 'true' 18 | - run: make distribute-source 19 | shell: bash 20 | if: inputs.make-binary == 'false' 21 | - run: find dist/*.whl | xargs auditwheel repair --plat=${{ inputs.manylinux-target }} 22 | shell: bash 23 | if: inputs.make-binary == 'true' && inputs.repair-manylinux == 'true' 24 | - uses: actions/upload-artifact@v2 25 | with: 26 | path: | 27 | wheelhouse/*.whl 28 | if: inputs.make-binary == 'true' && inputs.repair-manylinux == 'true' 29 | - uses: actions/upload-artifact@v2 30 | with: 31 | path: | 32 | dist/*.tar.gz 33 | dist/*.whl 34 | if: inputs.make-binary == 'false' || inputs.repair-manylinux == 'false' 35 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build, test, release 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags: 8 | - '[0-9]+.[0-9]+.[0-9]+' 9 | - '[0-9]+.[0-9]+.[0-9]+.dev[0-9]+' 10 | 11 | env: 12 | PY_VERSIONS_STR: >- 13 | ["3.8", "3.9", "3.10", "3.11", "3.12"] 14 | VERSIONS_LINUX_STR: >- 15 | ["manylinux2014", "manylinux_2_28"] 16 | VERSIONS_MACOS_STR: >- 17 | ["macos-11", "macos-12"] 18 | VERSIONS_WINDOWS_STR: >- 19 | ["windows-2022"] 20 | 21 | 22 | jobs: 23 | pass-env: 24 | runs-on: ubuntu-latest 25 | outputs: 26 | py-versions-str: ${{ steps.set-env.outputs.py_versions_str }} 27 | versions-linux-str: ${{ steps.set-env.outputs.versions_linux_str }} 28 | versions-macos-str: ${{ steps.set-env.outputs.versions_macos_str }} 29 | versions-windows-str: ${{ steps.set-env.outputs.versions_windows_str }} 30 | is-from-tag-str: ${{ steps.set-env.outputs.is_from_tag_str }} 31 | is-for-release-str: ${{ steps.set-env.outputs.is_for_release_str }} 32 | steps: 33 | - id: set-env 34 | run: | 35 | echo "py_versions_str=${{ toJSON(env.PY_VERSIONS_STR) }}" >> "$GITHUB_OUTPUT" 36 | echo "versions_linux_str=${{ toJSON(env.VERSIONS_LINUX_STR) }}" >> "$GITHUB_OUTPUT" 37 | echo "versions_macos_str=${{ toJSON(env.VERSIONS_MACOS_STR) }}" >> "$GITHUB_OUTPUT" 38 | echo "versions_windows_str=${{ toJSON(env.VERSIONS_WINDOWS_STR) }}" >> "$GITHUB_OUTPUT" 39 | echo "is_from_tag_str=${{ toJSON(github.ref_type == 'tag') }}" >> "$GITHUB_OUTPUT" 40 | echo "is_for_release_str=${{ toJSON(!contains(github.ref_name, 'dev')) }}" >> "$GITHUB_OUTPUT" 41 | build-linux: 42 | needs: pass-env 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | image-name: ${{ fromJSON(needs.pass-env.outputs.versions-linux-str) }} 47 | image-arch: 48 | - x86_64 49 | py-version: ${{ fromJSON(needs.pass-env.outputs.py-versions-str) }} 50 | runs-on: ubuntu-latest 51 | container: 52 | image: quay.io/pypa/${{ matrix.image-name }}_${{ matrix.image-arch }} 53 | steps: 54 | - uses: actions/checkout@v3 55 | with: 56 | submodules: true 57 | - uses: ./.github/actions/setup-poetry 58 | with: 59 | key-base: ${{ matrix.image-name }}_${{ matrix.image-arch }}-py${{ matrix.py-version }} 60 | use-pipx: true 61 | use-specific-python-version: true 62 | specific-python-version: ${{ matrix.py-version }} 63 | - run: make test 64 | - uses: ./.github/actions/upload-artifacts 65 | with: 66 | make-binary: true 67 | repair-manylinux: true 68 | manylinux-target: ${{ matrix.image-name}}_${{ matrix.image-arch }} 69 | if: ${{ fromJSON(needs.pass-env.outputs.is-from-tag-str) }} 70 | build-macos: 71 | needs: pass-env 72 | strategy: 73 | fail-fast: false 74 | matrix: 75 | arch: ${{ fromJSON(needs.pass-env.outputs.versions-macos-str) }} 76 | py-version: ${{ fromJSON(needs.pass-env.outputs.py-versions-str) }} 77 | runs-on: ${{ matrix.arch }} 78 | steps: 79 | - uses: actions/checkout@v3 80 | with: 81 | submodules: true 82 | - uses: actions/setup-python@v3 83 | with: 84 | python-version: ${{ matrix.py-version }} 85 | - uses: ./.github/actions/setup-poetry 86 | with: 87 | key-base: ${{ matrix.arch }}-py${{ matrix.py-version }} 88 | use-pipx: false 89 | use-specific-python-version: false 90 | - run: make test 91 | - uses: ./.github/actions/upload-artifacts 92 | with: 93 | make-binary: true 94 | if: ${{ fromJSON(needs.pass-env.outputs.is-from-tag-str) }} 95 | build-windows: 96 | needs: pass-env 97 | strategy: 98 | fail-fast: false 99 | matrix: 100 | arch: ${{ fromJSON(needs.pass-env.outputs.versions-windows-str) }} 101 | py-version: ${{ fromJSON(needs.pass-env.outputs.py-versions-str) }} 102 | runs-on: ${{ matrix.arch }} 103 | steps: 104 | - uses: actions/checkout@v3 105 | with: 106 | submodules: true 107 | - uses: ./.github/actions/setup-mingw 108 | with: 109 | key-base: ${{ matrix.arch }}-py${{ matrix.py-version }} 110 | - uses: actions/setup-python@v3 111 | with: 112 | python-version: ${{ matrix.py-version }} 113 | - uses: ./.github/actions/setup-poetry 114 | with: 115 | key-base: ${{ matrix.arch }}-py${{ matrix.py-version }} 116 | use-pipx: false 117 | use-specific-python-version: false 118 | - run: make test 119 | - uses: ./.github/actions/upload-artifacts 120 | with: 121 | make-binary: true 122 | if: ${{ fromJSON(needs.pass-env.outputs.is-from-tag-str) }} 123 | package-source: 124 | needs: 125 | - pass-env 126 | - build-linux 127 | - build-macos 128 | - build-windows 129 | runs-on: ubuntu-latest 130 | if: ${{ fromJSON(needs.pass-env.outputs.is-from-tag-str) }} 131 | steps: 132 | - uses: actions/checkout@v3 133 | with: 134 | submodules: true 135 | - uses: actions/setup-python@v3 136 | with: 137 | python-version: ${{ fromJSON(needs.pass-env.outputs.py-versions-str)[0] }} 138 | - uses: ./.github/actions/setup-poetry 139 | with: 140 | key-base: source_linux-py${{ fromJSON(needs.pass-env.outputs.py-versions-str)[0] }} 141 | use-pipx: false 142 | use-specific-python-version: false 143 | - uses: ./.github/actions/upload-artifacts 144 | with: 145 | make-binary: false 146 | upload-pypi: 147 | needs: 148 | - pass-env 149 | - build-linux 150 | - build-macos 151 | - build-windows 152 | - package-source 153 | runs-on: ubuntu-latest 154 | if: ${{ fromJSON(needs.pass-env.outputs.is-from-tag-str) }} 155 | steps: 156 | - uses: actions/checkout@v3 157 | with: 158 | submodules: true 159 | - uses: actions/setup-python@v3 160 | with: 161 | python-version: ${{ fromJSON(needs.pass-env.outputs.py-versions-str)[0] }} 162 | - uses: actions/download-artifact@v2 163 | with: 164 | path: dist 165 | - uses: ./.github/actions/setup-poetry 166 | with: 167 | key-base: source_linux-py${{ fromJSON(needs.pass-env.outputs.py-versions-str)[0] }} 168 | use-pipx: false 169 | use-specific-python-version: false 170 | - uses: pypa/gh-action-pypi-publish@release/v1 171 | with: 172 | user: __token__ 173 | password: ${{ secrets.PYPI_API_TOKEN }} 174 | packages_dir: dist/artifact/ 175 | if: ${{ fromJSON(needs.pass-env.outputs.is-for-release-str) }} 176 | - uses: pypa/gh-action-pypi-publish@release/v1 177 | with: 178 | user: __token__ 179 | password: ${{ secrets.TESTPYPI_API_TOKEN }} 180 | packages_dir: dist/artifact/ 181 | repository_url: https://test.pypi.org/legacy/ 182 | if: ${{ !fromJSON(needs.pass-env.outputs.is-for-release-str) }} 183 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | quickjs.egg-info 5 | *.so 6 | *.pyd 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "upstream-quickjs"] 2 | path = upstream-quickjs 3 | url = https://github.com/bellard/quickjs.git 4 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | # YAPF can be customized. 2 | # https://github.com/google/yapf#knobs 3 | 4 | [style] 5 | based_on_style = pep8 6 | column_limit = 100 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2019 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude pyproject.toml poetry.lock -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: install 3 | poetry run python -X dev -m unittest 4 | 5 | install: build 6 | poetry run python setup.py develop 7 | 8 | build: Makefile module.c upstream-quickjs/quickjs.c upstream-quickjs/quickjs.h 9 | ifeq ($(shell uname | head -c5), MINGW) 10 | poetry run python setup.py build -c mingw32 11 | else 12 | poetry run python setup.py build 13 | endif 14 | 15 | format: 16 | poetry run python -m yapf -i -r --style .style.yapf *.py 17 | clang-format-7 -i module.c 18 | 19 | distribute-source: test 20 | rm -rf dist/ 21 | poetry run python setup.py sdist 22 | 23 | distribute-binary: test 24 | poetry run python setup.py bdist_wheel --skip-build 25 | 26 | upload-test: 27 | poetry run python -m twine upload -r testpypi dist/* 28 | 29 | upload: 30 | poetry run python -m twine upload dist/* 31 | 32 | clean: 33 | rm -rf build/ dist/ 34 | rm -f *.so 35 | rm -f *.pyd 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/PetterS/quickjs.svg?style=svg)](https://circleci.com/gh/PetterS/quickjs) [![PyPI version fury.io](https://badge.fury.io/py/quickjs.svg)](https://pypi.python.org/pypi/quickjs/) 2 | 3 | Just install with 4 | 5 | pip install quickjs 6 | 7 | Binaries are provided for: 8 | - 1.19.2 and later: Python 3.7-3.10, 64-bit for Windows, macOS and GNU/Linux. 9 | - 1.18.0-1.19.1: None. 10 | - 1.5.1–1.17.0: Python 3.9, 64-bit for Windows. 11 | - 1.5.0 and earlier: Python 3.7, 64-bit for Windows. 12 | 13 | # Usage 14 | 15 | ```python 16 | from quickjs import Function 17 | 18 | f = Function("f", """ 19 | function adder(a, b) { 20 | return a + b; 21 | } 22 | 23 | function f(a, b) { 24 | return adder(a, b); 25 | } 26 | """) 27 | 28 | assert f(1, 2) == 3 29 | ``` 30 | 31 | Simple types like int, floats and strings are converted directly. Other types (dicts, lists) are converted via JSON by the `Function` class. 32 | The library is thread-safe if `Function` is used. If the `Context` class is used directly, it can only ever be accessed by the same thread. 33 | This is true even if the accesses are not concurrent. 34 | 35 | Both `Function` and `Context` expose `set_memory_limit` and `set_time_limit` functions that allow limits for code running in production. 36 | 37 | ## API 38 | The `Function` class has, apart from being a callable, additional methods: 39 | - `set_memory_limit` 40 | - `set_time_limit` 41 | - `set_max_stack_size` 42 | - `memory` – returns a dict with information about memory usage. 43 | - `add_callable` – adds a Python function and makes it callable from JS. 44 | - `execute_pending_job` – executes a pending job (such as a async function or Promise). 45 | 46 | ## Documentation 47 | For full functionality, please see `test_quickjs.py` 48 | 49 | # Developing 50 | This project uses a git submodule for the upstream code, so clone it with the `--recurse-submodules` option or run `git submodule update --init --recursive` afterwards. 51 | 52 | Use a `poetry shell` and `make test` should work from inside its virtual environment. 53 | -------------------------------------------------------------------------------- /check_memory.py: -------------------------------------------------------------------------------- 1 | # Script that tries to prove that the quickjs wrapper does not leak memory. 2 | # 3 | # It finds the leak if a Py_DECREF is commented out in module.c. 4 | 5 | import gc 6 | import tracemalloc 7 | import unittest 8 | 9 | import quickjs 10 | import test_quickjs 11 | 12 | def run(): 13 | loader = unittest.TestLoader() 14 | suite = loader.discover(".") 15 | runner = unittest.TextTestRunner() 16 | runner.run(suite) 17 | 18 | filters = [ 19 | tracemalloc.Filter(True, quickjs.__file__), 20 | tracemalloc.Filter(True, test_quickjs.__file__), 21 | ] 22 | 23 | def main(): 24 | print("Warming up (to discount regex cache etc.)") 25 | run() 26 | 27 | tracemalloc.start(25) 28 | gc.collect() 29 | snapshot1 = tracemalloc.take_snapshot().filter_traces(filters) 30 | run() 31 | gc.collect() 32 | snapshot2 = tracemalloc.take_snapshot().filter_traces(filters) 33 | 34 | top_stats = snapshot2.compare_to(snapshot1, 'traceback') 35 | 36 | print("Objects not released") 37 | print("====================") 38 | for stat in top_stats: 39 | if stat.size_diff == 0: 40 | continue 41 | print(stat) 42 | for line in stat.traceback.format(): 43 | print(" ", line) 44 | 45 | print("\nquickjs should not show up above.") 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "upstream-quickjs/quickjs.h" 5 | 6 | // Node of Python callable that the context needs to keep available. 7 | typedef struct PythonCallableNode PythonCallableNode; 8 | struct PythonCallableNode { 9 | PyObject *obj; 10 | PythonCallableNode *prev; 11 | PythonCallableNode *next; 12 | }; 13 | 14 | // Keeps track of the time if we are using a time limit. 15 | typedef struct { 16 | clock_t start; 17 | clock_t limit; 18 | } InterruptData; 19 | 20 | // The data of the type _quickjs.Context. 21 | typedef struct { 22 | PyObject_HEAD JSRuntime *runtime; 23 | JSContext *context; 24 | int has_time_limit; 25 | clock_t time_limit; 26 | // Used when releasing the GIL. 27 | PyThreadState *thread_state; 28 | InterruptData interrupt_data; 29 | // NULL-terminated doubly linked list of callable Python objects that we need to keep track of. 30 | // We need to store references to callables in a place where we can access them when running 31 | // Python's GC. Having them stored only in QuickJS' function opaques would create a dependency 32 | // cycle across Python and QuickJS that neither GC can notice. 33 | PythonCallableNode *python_callables; 34 | } RuntimeData; 35 | 36 | // The data of the type _quickjs.Object. 37 | typedef struct { 38 | PyObject_HEAD; 39 | RuntimeData *runtime_data; 40 | JSValue object; 41 | } ObjectData; 42 | 43 | // The exception raised by this module. 44 | static PyObject *JSException = NULL; 45 | static PyObject *StackOverflow = NULL; 46 | // Converts the current Javascript exception to a Python exception via a C string. 47 | static void quickjs_exception_to_python(JSContext *context); 48 | // Converts a JSValue to a Python object. 49 | // 50 | // Takes ownership of the JSValue and will deallocate it (refcount reduced by 1). 51 | static PyObject *quickjs_to_python(RuntimeData *runtime_data, JSValue value); 52 | 53 | // Returns nonzero if we should stop due to a time limit. 54 | static int js_interrupt_handler(JSRuntime *rt, void *opaque) { 55 | InterruptData *data = opaque; 56 | if (clock() - data->start >= data->limit) { 57 | return 1; 58 | } else { 59 | return 0; 60 | } 61 | } 62 | 63 | // Sets up a context and an InterruptData struct if the context has a time limit. 64 | static void setup_time_limit(RuntimeData *runtime_data, InterruptData *interrupt_data) { 65 | if (runtime_data->has_time_limit) { 66 | JS_SetInterruptHandler(runtime_data->runtime, js_interrupt_handler, interrupt_data); 67 | interrupt_data->limit = runtime_data->time_limit; 68 | interrupt_data->start = clock(); 69 | } 70 | } 71 | 72 | // Restores the context if the context has a time limit. 73 | static void teardown_time_limit(RuntimeData *runtime_data) { 74 | if (runtime_data->has_time_limit) { 75 | JS_SetInterruptHandler(runtime_data->runtime, NULL, NULL); 76 | } 77 | } 78 | 79 | // This method is always called in a context before running JS code in QuickJS. It sets up time 80 | // limites, releases the GIL etc. 81 | static void prepare_call_js(RuntimeData *runtime_data) { 82 | // We release the GIL in order to speed things up for certain use cases. 83 | assert(!runtime_data->thread_state); 84 | runtime_data->thread_state = PyEval_SaveThread(); 85 | JS_UpdateStackTop(runtime_data->runtime); 86 | setup_time_limit(runtime_data, &runtime_data->interrupt_data); 87 | } 88 | 89 | // This method is called right after returning from running JS code. Aquires the GIL etc. 90 | static void end_call_js(RuntimeData *runtime_data) { 91 | teardown_time_limit(runtime_data); 92 | assert(runtime_data->thread_state); 93 | PyEval_RestoreThread(runtime_data->thread_state); 94 | runtime_data->thread_state = NULL; 95 | } 96 | 97 | // Called when Python is called again from inside QuickJS. 98 | static void prepare_call_python(RuntimeData *runtime_data) { 99 | assert(runtime_data->thread_state); 100 | PyEval_RestoreThread(runtime_data->thread_state); 101 | runtime_data->thread_state = NULL; 102 | } 103 | 104 | // Called when the operation started by prepare_call_python is done. 105 | static void end_call_python(RuntimeData *runtime_data) { 106 | assert(!runtime_data->thread_state); 107 | runtime_data->thread_state = PyEval_SaveThread(); 108 | } 109 | 110 | // GC traversal. 111 | static int object_traverse(ObjectData *self, visitproc visit, void *arg) { 112 | Py_VISIT(self->runtime_data); 113 | return 0; 114 | } 115 | 116 | // Creates an instance of the Object class. 117 | static PyObject *object_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { 118 | ObjectData *self = PyObject_GC_New(ObjectData, type); 119 | if (self != NULL) { 120 | self->runtime_data = NULL; 121 | } 122 | return (PyObject *)self; 123 | } 124 | 125 | // Deallocates an instance of the Object class. 126 | static void object_dealloc(ObjectData *self) { 127 | if (self->runtime_data) { 128 | PyObject_GC_UnTrack(self); 129 | JS_FreeValue(self->runtime_data->context, self->object); 130 | // We incremented the refcount of the runtime data when we created this object, so we should 131 | // decrease it now so we don't leak memory. 132 | Py_CLEAR(self->runtime_data); 133 | } 134 | PyObject_GC_Del(self); 135 | } 136 | 137 | // _quickjs.Object.__call__ 138 | static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds); 139 | 140 | // _quickjs.Object.json 141 | // 142 | // Returns the JSON representation of the object as a Python string. 143 | static PyObject *object_json(ObjectData *self) { 144 | JSContext *context = self->runtime_data->context; 145 | JSValue json_string = JS_JSONStringify(context, self->object, JS_UNDEFINED, JS_UNDEFINED); 146 | return quickjs_to_python(self->runtime_data, json_string); 147 | } 148 | 149 | // All methods of the _quickjs.Object class. 150 | static PyMethodDef object_methods[] = { 151 | {"json", (PyCFunction)object_json, METH_NOARGS, "Converts to a JSON string."}, 152 | {NULL} /* Sentinel */ 153 | }; 154 | 155 | // Define the quickjs.Object type. 156 | static PyTypeObject Object = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Object", 157 | .tp_doc = "Quickjs object", 158 | .tp_basicsize = sizeof(ObjectData), 159 | .tp_itemsize = 0, 160 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, 161 | .tp_traverse = (traverseproc)object_traverse, 162 | .tp_new = object_new, 163 | .tp_dealloc = (destructor)object_dealloc, 164 | .tp_call = (ternaryfunc)object_call, 165 | .tp_methods = object_methods}; 166 | 167 | // Whether converting item to QuickJS would be possible. 168 | static int python_to_quickjs_possible(RuntimeData *runtime_data, PyObject *item) { 169 | if (PyBool_Check(item)) { 170 | return 1; 171 | } else if (PyLong_Check(item)) { 172 | return 1; 173 | } else if (PyFloat_Check(item)) { 174 | return 1; 175 | } else if (item == Py_None) { 176 | return 1; 177 | } else if (PyUnicode_Check(item)) { 178 | return 1; 179 | } else if (PyObject_IsInstance(item, (PyObject *)&Object)) { 180 | ObjectData *object = (ObjectData *)item; 181 | if (object->runtime_data != runtime_data) { 182 | PyErr_Format(PyExc_ValueError, "Can not mix JS objects from different contexts."); 183 | return 0; 184 | } 185 | return 1; 186 | } else { 187 | PyErr_Format(PyExc_TypeError, 188 | "Unsupported type when converting a Python object to quickjs: %s.", 189 | Py_TYPE(item)->tp_name); 190 | return 0; 191 | } 192 | } 193 | 194 | // Converts item to QuickJS. 195 | // 196 | // If the Python object is not possible to convert to JS, undefined will be returned. This fallback 197 | // will not be used if python_to_quickjs_possible returns 1. 198 | static JSValueConst python_to_quickjs(RuntimeData *runtime_data, PyObject *item) { 199 | if (PyBool_Check(item)) { 200 | return JS_MKVAL(JS_TAG_BOOL, item == Py_True ? 1 : 0); 201 | } else if (PyLong_Check(item)) { 202 | int overflow; 203 | long value = PyLong_AsLongAndOverflow(item, &overflow); 204 | if (overflow) { 205 | PyObject *float_value = PyNumber_Float(item); 206 | double double_value = PyFloat_AsDouble(float_value); 207 | Py_DECREF(float_value); 208 | return JS_NewFloat64(runtime_data->context, double_value); 209 | } else { 210 | return JS_MKVAL(JS_TAG_INT, value); 211 | } 212 | } else if (PyFloat_Check(item)) { 213 | return JS_NewFloat64(runtime_data->context, PyFloat_AsDouble(item)); 214 | } else if (item == Py_None) { 215 | return JS_NULL; 216 | } else if (PyUnicode_Check(item)) { 217 | return JS_NewString(runtime_data->context, PyUnicode_AsUTF8(item)); 218 | } else if (PyObject_IsInstance(item, (PyObject *)&Object)) { 219 | return JS_DupValue(runtime_data->context, ((ObjectData *)item)->object); 220 | } else { 221 | // Can not happen if python_to_quickjs_possible passes. 222 | return JS_UNDEFINED; 223 | } 224 | } 225 | 226 | // _quickjs.Object.__call__ 227 | static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) { 228 | if (self->runtime_data == NULL) { 229 | // This object does not have a context and has not been created by this module. 230 | Py_RETURN_NONE; 231 | } 232 | 233 | // We first loop through all arguments and check that they are supported without doing anything. 234 | // This makes the cleanup code simpler for the case where we have to raise an error. 235 | const int nargs = PyTuple_Size(args); 236 | for (int i = 0; i < nargs; ++i) { 237 | PyObject *item = PyTuple_GetItem(args, i); 238 | if (!python_to_quickjs_possible(self->runtime_data, item)) { 239 | return NULL; 240 | } 241 | } 242 | 243 | // Now we know that all arguments are supported and we can convert them. 244 | JSValueConst *jsargs; 245 | if (nargs) { 246 | jsargs = js_malloc(self->runtime_data->context, nargs * sizeof(JSValueConst)); 247 | if (jsargs == NULL) { 248 | quickjs_exception_to_python(self->runtime_data->context); 249 | return NULL; 250 | } 251 | } 252 | for (int i = 0; i < nargs; ++i) { 253 | PyObject *item = PyTuple_GetItem(args, i); 254 | jsargs[i] = python_to_quickjs(self->runtime_data, item); 255 | } 256 | 257 | prepare_call_js(self->runtime_data); 258 | JSValue value; 259 | value = JS_Call(self->runtime_data->context, self->object, JS_NULL, nargs, jsargs); 260 | for (int i = 0; i < nargs; ++i) { 261 | JS_FreeValue(self->runtime_data->context, jsargs[i]); 262 | } 263 | if (nargs) { 264 | js_free(self->runtime_data->context, jsargs); 265 | } 266 | end_call_js(self->runtime_data); 267 | return quickjs_to_python(self->runtime_data, value); 268 | } 269 | 270 | // Converts the current Javascript exception to a Python exception via a C string. 271 | static void quickjs_exception_to_python(JSContext *context) { 272 | JSValue exception = JS_GetException(context); 273 | const char *cstring = JS_ToCString(context, exception); 274 | const char *stack_cstring = NULL; 275 | if (!JS_IsNull(exception) && !JS_IsUndefined(exception)) { 276 | JSValue stack = JS_GetPropertyStr(context, exception, "stack"); 277 | if (!JS_IsException(stack)) { 278 | stack_cstring = JS_ToCString(context, stack); 279 | JS_FreeValue(context, stack); 280 | } 281 | } 282 | if (cstring != NULL) { 283 | const char *safe_stack_cstring = stack_cstring ? stack_cstring : ""; 284 | if (strstr(cstring, "stack overflow") != NULL) { 285 | PyErr_Format(StackOverflow, "%s\n%s", cstring, safe_stack_cstring); 286 | } else { 287 | PyErr_Format(JSException, "%s\n%s", cstring, safe_stack_cstring); 288 | } 289 | } else { 290 | // This has been observed to happen when different threads have used the same QuickJS 291 | // runtime, but not at the same time. 292 | // Could potentially be another problem though, since JS_ToCString may return NULL. 293 | PyErr_Format(JSException, 294 | "(Failed obtaining QuickJS error string. Concurrency issue?)"); 295 | } 296 | JS_FreeCString(context, cstring); 297 | JS_FreeCString(context, stack_cstring); 298 | JS_FreeValue(context, exception); 299 | } 300 | 301 | // Converts a JSValue to a Python object. 302 | // 303 | // Takes ownership of the JSValue and will deallocate it (refcount reduced by 1). 304 | static PyObject *quickjs_to_python(RuntimeData *runtime_data, JSValue value) { 305 | JSContext *context = runtime_data->context; 306 | int tag = JS_VALUE_GET_TAG(value); 307 | // A return value of NULL means an exception. 308 | PyObject *return_value = NULL; 309 | 310 | if (tag == JS_TAG_INT) { 311 | return_value = Py_BuildValue("i", JS_VALUE_GET_INT(value)); 312 | } else if (tag == JS_TAG_BIG_INT) { 313 | const char *cstring = JS_ToCString(context, value); 314 | return_value = PyLong_FromString(cstring, NULL, 10); 315 | JS_FreeCString(context, cstring); 316 | } else if (tag == JS_TAG_BOOL) { 317 | return_value = Py_BuildValue("O", JS_VALUE_GET_BOOL(value) ? Py_True : Py_False); 318 | } else if (tag == JS_TAG_NULL) { 319 | return_value = Py_None; 320 | } else if (tag == JS_TAG_UNDEFINED) { 321 | return_value = Py_None; 322 | } else if (tag == JS_TAG_EXCEPTION) { 323 | quickjs_exception_to_python(context); 324 | } else if (tag == JS_TAG_FLOAT64) { 325 | return_value = Py_BuildValue("d", JS_VALUE_GET_FLOAT64(value)); 326 | } else if (tag == JS_TAG_STRING) { 327 | const char *cstring = JS_ToCString(context, value); 328 | return_value = Py_BuildValue("s", cstring); 329 | JS_FreeCString(context, cstring); 330 | } else if (tag == JS_TAG_OBJECT || tag == JS_TAG_MODULE || tag == JS_TAG_SYMBOL) { 331 | // This is a Javascript object or function. We wrap it in a _quickjs.Object. 332 | return_value = PyObject_CallObject((PyObject *)&Object, NULL); 333 | ObjectData *object = (ObjectData *)return_value; 334 | // This is important. Otherwise, the context may be deallocated before the object, which 335 | // will result in a segfault with high probability. 336 | Py_INCREF(runtime_data); 337 | object->runtime_data = runtime_data; 338 | PyObject_GC_Track(object); 339 | object->object = JS_DupValue(context, value); 340 | } else { 341 | PyErr_Format(PyExc_TypeError, "Unknown quickjs tag: %d", tag); 342 | } 343 | 344 | JS_FreeValue(context, value); 345 | if (return_value == Py_None) { 346 | // Can not simply return PyNone for refcounting reasons. 347 | Py_RETURN_NONE; 348 | } 349 | return return_value; 350 | } 351 | 352 | static PyObject *test(PyObject *self, PyObject *args) { 353 | return Py_BuildValue("i", 42); 354 | } 355 | 356 | // Global state of the module. Currently none. 357 | struct module_state {}; 358 | 359 | // GC traversal. 360 | static int runtime_traverse(RuntimeData *self, visitproc visit, void *arg) { 361 | PythonCallableNode *node = self->python_callables; 362 | while (node) { 363 | Py_VISIT(node->obj); 364 | node = node->next; 365 | } 366 | return 0; 367 | } 368 | 369 | // GC clearing. Object does not have a clearing method, therefore dependency cycles 370 | // between Context and Object will always be cleared starting here. 371 | static int runtime_clear(RuntimeData *self) { 372 | PythonCallableNode *node = self->python_callables; 373 | while (node) { 374 | Py_CLEAR(node->obj); 375 | node = node->next; 376 | } 377 | return 0; 378 | } 379 | 380 | static JSClassID js_python_function_class_id; 381 | 382 | static void js_python_function_finalizer(JSRuntime *rt, JSValue val) { 383 | PythonCallableNode *node = JS_GetOpaque(val, js_python_function_class_id); 384 | RuntimeData *runtime_data = JS_GetRuntimeOpaque(rt); 385 | if (node) { 386 | // fail safe 387 | JS_SetOpaque(val, NULL); 388 | // NOTE: This may be called from e.g. runtime_dealloc, but also from 389 | // e.g. JS_Eval, so we need to ensure that we are in the correct state. 390 | // TODO: integrate better with (prepare|end)_call_(python|js). 391 | if (runtime_data->thread_state) { 392 | PyEval_RestoreThread(runtime_data->thread_state); 393 | } 394 | if (node->prev) { 395 | node->prev->next = node->next; 396 | } else { 397 | runtime_data->python_callables = node->next; 398 | } 399 | if (node->next) { 400 | node->next->prev = node->prev; 401 | } 402 | // May have just been cleared in runtime_clear. 403 | Py_XDECREF(node->obj); 404 | PyMem_Free(node); 405 | if (runtime_data->thread_state) { 406 | runtime_data->thread_state = PyEval_SaveThread(); 407 | } 408 | }; 409 | } 410 | 411 | static JSValue js_python_function_call(JSContext *ctx, JSValueConst func_obj, 412 | JSValueConst this_val, int argc, JSValueConst *argv, 413 | int flags) { 414 | RuntimeData *runtime_data = (RuntimeData *)JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); 415 | PythonCallableNode *node = JS_GetOpaque(func_obj, js_python_function_class_id); 416 | if (runtime_data->has_time_limit) { 417 | return JS_ThrowInternalError(ctx, "Can not call into Python with a time limit set."); 418 | } 419 | prepare_call_python(runtime_data); 420 | 421 | PyObject *args = PyTuple_New(argc); 422 | if (!args) { 423 | end_call_python(runtime_data); 424 | return JS_ThrowOutOfMemory(ctx); 425 | } 426 | int tuple_success = 1; 427 | for (int i = 0; i < argc; ++i) { 428 | PyObject *arg = quickjs_to_python(runtime_data, JS_DupValue(ctx, argv[i])); 429 | if (!arg) { 430 | tuple_success = 0; 431 | break; 432 | } 433 | PyTuple_SET_ITEM(args, i, arg); 434 | } 435 | if (!tuple_success) { 436 | Py_DECREF(args); 437 | end_call_python(runtime_data); 438 | return JS_ThrowInternalError(ctx, "Internal error: could not convert args."); 439 | } 440 | 441 | PyObject *result = PyObject_CallObject(node->obj, args); 442 | Py_DECREF(args); 443 | if (!result) { 444 | end_call_python(runtime_data); 445 | return JS_ThrowInternalError(ctx, "Python call failed."); 446 | } 447 | JSValue js_result = JS_NULL; 448 | if (python_to_quickjs_possible(runtime_data, result)) { 449 | js_result = python_to_quickjs(runtime_data, result); 450 | } else { 451 | PyErr_Clear(); 452 | js_result = JS_ThrowInternalError(ctx, "Can not convert Python result to JS."); 453 | } 454 | Py_DECREF(result); 455 | 456 | end_call_python(runtime_data); 457 | return js_result; 458 | } 459 | 460 | static JSClassDef js_python_function_class = { 461 | "PythonFunction", 462 | .finalizer = js_python_function_finalizer, 463 | .call = js_python_function_call, 464 | }; 465 | 466 | // Creates an instance of the _quickjs.Context class. 467 | static PyObject *runtime_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { 468 | RuntimeData *self = PyObject_GC_New(RuntimeData, type); 469 | if (self != NULL) { 470 | // We never have different contexts for the same runtime. This way, different 471 | // _quickjs.Context can be used concurrently. 472 | self->runtime = JS_NewRuntime(); 473 | self->context = JS_NewContext(self->runtime); 474 | JS_NewClass(self->runtime, js_python_function_class_id, 475 | &js_python_function_class); 476 | JSValue global = JS_GetGlobalObject(self->context); 477 | JSValue fct_cls = JS_GetPropertyStr(self->context, global, "Function"); 478 | JSValue fct_proto = JS_GetPropertyStr(self->context, fct_cls, "prototype"); 479 | JS_FreeValue(self->context, fct_cls); 480 | JS_SetClassProto(self->context, js_python_function_class_id, fct_proto); 481 | JS_FreeValue(self->context, global); 482 | self->has_time_limit = 0; 483 | self->time_limit = 0; 484 | self->thread_state = NULL; 485 | self->python_callables = NULL; 486 | JS_SetRuntimeOpaque(self->runtime, self); 487 | PyObject_GC_Track(self); 488 | } 489 | return (PyObject *)self; 490 | } 491 | 492 | // Deallocates an instance of the _quickjs.Context class. 493 | static void runtime_dealloc(RuntimeData *self) { 494 | JS_FreeContext(self->context); 495 | JS_FreeRuntime(self->runtime); 496 | PyObject_GC_UnTrack(self); 497 | PyObject_GC_Del(self); 498 | } 499 | 500 | // Evaluates a Python string as JS and returns the result as a Python object. Will return 501 | // _quickjs.Object for complex types (other than e.g. str, int). 502 | static PyObject *runtime_eval_internal(RuntimeData *self, PyObject *args, int eval_type) { 503 | const char *code; 504 | if (!PyArg_ParseTuple(args, "s", &code)) { 505 | return NULL; 506 | } 507 | prepare_call_js(self); 508 | JSValue value; 509 | value = JS_Eval(self->context, code, strlen(code), "", eval_type); 510 | end_call_js(self); 511 | return quickjs_to_python(self, value); 512 | } 513 | 514 | // _quickjs.Context.eval 515 | // 516 | // Evaluates a Python string as JS and returns the result as a Python object. Will return 517 | // _quickjs.Object for complex types (other than e.g. str, int). 518 | static PyObject *runtime_eval(RuntimeData *self, PyObject *args) { 519 | return runtime_eval_internal(self, args, JS_EVAL_TYPE_GLOBAL); 520 | } 521 | 522 | // _quickjs.Context.module 523 | // 524 | // Evaluates a Python string as JS module. Otherwise identical to eval. 525 | static PyObject *runtime_module(RuntimeData *self, PyObject *args) { 526 | return runtime_eval_internal(self, args, JS_EVAL_TYPE_MODULE); 527 | } 528 | 529 | // _quickjs.Context.execute_pending_job 530 | // 531 | // If there are pending jobs, executes one and returns True. Else returns False. 532 | static PyObject *runtime_execute_pending_job(RuntimeData *self) { 533 | prepare_call_js(self); 534 | JSContext *ctx; 535 | int ret = JS_ExecutePendingJob(self->runtime, &ctx); 536 | end_call_js(self); 537 | if (ret > 0) { 538 | Py_RETURN_TRUE; 539 | } else if (ret == 0) { 540 | Py_RETURN_FALSE; 541 | } else { 542 | quickjs_exception_to_python(ctx); 543 | return NULL; 544 | } 545 | } 546 | 547 | // _quickjs.Context.parse_json 548 | // 549 | // Evaluates a Python string as JSON and returns the result as a Python object. Will 550 | // return _quickjs.Object for complex types (other than e.g. str, int). 551 | static PyObject *runtime_parse_json(RuntimeData *self, PyObject *args) { 552 | const char *data; 553 | if (!PyArg_ParseTuple(args, "s", &data)) { 554 | return NULL; 555 | } 556 | JSValue value; 557 | Py_BEGIN_ALLOW_THREADS; 558 | value = JS_ParseJSON(self->context, data, strlen(data), "runtime_parse_json.json"); 559 | Py_END_ALLOW_THREADS; 560 | return quickjs_to_python(self, value); 561 | } 562 | 563 | // _quickjs.Context.get 564 | // 565 | // Retrieves a global variable from the JS context. 566 | static PyObject *runtime_get(RuntimeData *self, PyObject *args) { 567 | const char *name; 568 | if (!PyArg_ParseTuple(args, "s", &name)) { 569 | return NULL; 570 | } 571 | JSValue global = JS_GetGlobalObject(self->context); 572 | JSValue value = JS_GetPropertyStr(self->context, global, name); 573 | JS_FreeValue(self->context, global); 574 | return quickjs_to_python(self, value); 575 | } 576 | 577 | // _quickjs.Context.set 578 | // 579 | // Sets a global variable to the JS context. 580 | static PyObject *runtime_set(RuntimeData *self, PyObject *args) { 581 | const char *name; 582 | PyObject *item; 583 | if (!PyArg_ParseTuple(args, "sO", &name, &item)) { 584 | return NULL; 585 | } 586 | JSValue global = JS_GetGlobalObject(self->context); 587 | int ret = 0; 588 | if (python_to_quickjs_possible(self, item)) { 589 | ret = JS_SetPropertyStr(self->context, global, name, python_to_quickjs(self, item)); 590 | if (ret != 1) { 591 | PyErr_SetString(PyExc_TypeError, "Failed setting the variable."); 592 | } 593 | } 594 | JS_FreeValue(self->context, global); 595 | if (ret == 1) { 596 | Py_RETURN_NONE; 597 | } else { 598 | return NULL; 599 | } 600 | } 601 | 602 | // _quickjs.Context.set_memory_limit 603 | // 604 | // Sets the memory limit of the context. 605 | static PyObject *runtime_set_memory_limit(RuntimeData *self, PyObject *args) { 606 | Py_ssize_t limit; 607 | if (!PyArg_ParseTuple(args, "n", &limit)) { 608 | return NULL; 609 | } 610 | JS_SetMemoryLimit(self->runtime, limit); 611 | Py_RETURN_NONE; 612 | } 613 | 614 | // _quickjs.Context.set_time_limit 615 | // 616 | // Sets the CPU time limit of the context. This will be used in an interrupt handler. 617 | static PyObject *runtime_set_time_limit(RuntimeData *self, PyObject *args) { 618 | double limit; 619 | if (!PyArg_ParseTuple(args, "d", &limit)) { 620 | return NULL; 621 | } 622 | if (limit < 0) { 623 | self->has_time_limit = 0; 624 | } else { 625 | self->has_time_limit = 1; 626 | self->time_limit = (clock_t)(limit * CLOCKS_PER_SEC); 627 | } 628 | Py_RETURN_NONE; 629 | } 630 | 631 | // _quickjs.Context.set_max_stack_size 632 | // 633 | // Sets the max stack size in bytes. 634 | static PyObject *runtime_set_max_stack_size(RuntimeData *self, PyObject *args) { 635 | Py_ssize_t limit; 636 | if (!PyArg_ParseTuple(args, "n", &limit)) { 637 | return NULL; 638 | } 639 | JS_SetMaxStackSize(self->runtime, limit); 640 | Py_RETURN_NONE; 641 | } 642 | 643 | // _quickjs.Context.memory 644 | // 645 | // Sets the CPU time limit of the context. This will be used in an interrupt handler. 646 | static PyObject *runtime_memory(RuntimeData *self) { 647 | PyObject *dict = PyDict_New(); 648 | if (dict == NULL) { 649 | return NULL; 650 | } 651 | JSMemoryUsage usage; 652 | JS_ComputeMemoryUsage(self->runtime, &usage); 653 | #define MEM_USAGE_ADD_TO_DICT(key) \ 654 | { \ 655 | PyObject *value = PyLong_FromLongLong(usage.key); \ 656 | if (PyDict_SetItemString(dict, #key, value) != 0) { \ 657 | return NULL; \ 658 | } \ 659 | Py_DECREF(value); \ 660 | } 661 | MEM_USAGE_ADD_TO_DICT(malloc_size); 662 | MEM_USAGE_ADD_TO_DICT(malloc_limit); 663 | MEM_USAGE_ADD_TO_DICT(memory_used_size); 664 | MEM_USAGE_ADD_TO_DICT(malloc_count); 665 | MEM_USAGE_ADD_TO_DICT(memory_used_count); 666 | MEM_USAGE_ADD_TO_DICT(atom_count); 667 | MEM_USAGE_ADD_TO_DICT(atom_size); 668 | MEM_USAGE_ADD_TO_DICT(str_count); 669 | MEM_USAGE_ADD_TO_DICT(str_size); 670 | MEM_USAGE_ADD_TO_DICT(obj_count); 671 | MEM_USAGE_ADD_TO_DICT(obj_size); 672 | MEM_USAGE_ADD_TO_DICT(prop_count); 673 | MEM_USAGE_ADD_TO_DICT(prop_size); 674 | MEM_USAGE_ADD_TO_DICT(shape_count); 675 | MEM_USAGE_ADD_TO_DICT(shape_size); 676 | MEM_USAGE_ADD_TO_DICT(js_func_count); 677 | MEM_USAGE_ADD_TO_DICT(js_func_size); 678 | MEM_USAGE_ADD_TO_DICT(js_func_code_size); 679 | MEM_USAGE_ADD_TO_DICT(js_func_pc2line_count); 680 | MEM_USAGE_ADD_TO_DICT(js_func_pc2line_size); 681 | MEM_USAGE_ADD_TO_DICT(c_func_count); 682 | MEM_USAGE_ADD_TO_DICT(array_count); 683 | MEM_USAGE_ADD_TO_DICT(fast_array_count); 684 | MEM_USAGE_ADD_TO_DICT(fast_array_elements); 685 | MEM_USAGE_ADD_TO_DICT(binary_object_count); 686 | MEM_USAGE_ADD_TO_DICT(binary_object_size); 687 | return dict; 688 | } 689 | 690 | // _quickjs.Context.gc 691 | // 692 | // Runs garbage collection. 693 | static PyObject *runtime_gc(RuntimeData *self) { 694 | JS_RunGC(self->runtime); 695 | Py_RETURN_NONE; 696 | } 697 | 698 | 699 | static PyObject *runtime_add_callable(RuntimeData *self, PyObject *args) { 700 | const char *name; 701 | PyObject *callable; 702 | if (!PyArg_ParseTuple(args, "sO", &name, &callable)) { 703 | return NULL; 704 | } 705 | if (!PyCallable_Check(callable)) { 706 | PyErr_SetString(PyExc_TypeError, "Argument must be callable."); 707 | return NULL; 708 | } 709 | 710 | JSValue function = JS_NewObjectClass(self->context, js_python_function_class_id); 711 | if (JS_IsException(function)) { 712 | quickjs_exception_to_python(self->context); 713 | return NULL; 714 | } 715 | // TODO: Should we allow setting the .length of the function to something other than 0? 716 | JS_DefinePropertyValueStr(self->context, function, "name", JS_NewString(self->context, name), JS_PROP_CONFIGURABLE); 717 | PythonCallableNode *node = PyMem_Malloc(sizeof(PythonCallableNode)); 718 | if (!node) { 719 | JS_FreeValue(self->context, function); 720 | return NULL; 721 | } 722 | Py_INCREF(callable); 723 | node->obj = callable; 724 | node->prev = NULL; 725 | node->next = self->python_callables; 726 | if (self->python_callables) { 727 | self->python_callables->prev = node; 728 | } 729 | self->python_callables = node; 730 | JS_SetOpaque(function, node); 731 | 732 | JSValue global = JS_GetGlobalObject(self->context); 733 | if (JS_IsException(global)) { 734 | JS_FreeValue(self->context, function); 735 | quickjs_exception_to_python(self->context); 736 | return NULL; 737 | } 738 | // If this fails we don't notify the caller of this function. 739 | int ret = JS_SetPropertyStr(self->context, global, name, function); 740 | JS_FreeValue(self->context, global); 741 | if (ret != 1) { 742 | PyErr_SetString(PyExc_TypeError, "Failed adding the callable."); 743 | return NULL; 744 | } else { 745 | Py_RETURN_NONE; 746 | } 747 | } 748 | 749 | 750 | // _quickjs.Context.globalThis 751 | // 752 | // Global object of the JS context. 753 | static PyObject *runtime_global_this(RuntimeData *self, void *closure) { 754 | return quickjs_to_python(self, JS_GetGlobalObject(self->context)); 755 | } 756 | 757 | 758 | // All methods of the _quickjs.Context class. 759 | static PyMethodDef runtime_methods[] = { 760 | {"eval", (PyCFunction)runtime_eval, METH_VARARGS, "Evaluates a Javascript string."}, 761 | {"module", 762 | (PyCFunction)runtime_module, 763 | METH_VARARGS, 764 | "Evaluates a Javascript string as a module."}, 765 | {"execute_pending_job", (PyCFunction)runtime_execute_pending_job, METH_NOARGS, "Executes a pending job."}, 766 | {"parse_json", (PyCFunction)runtime_parse_json, METH_VARARGS, "Parses a JSON string."}, 767 | {"get", (PyCFunction)runtime_get, METH_VARARGS, "Gets a Javascript global variable."}, 768 | {"set", (PyCFunction)runtime_set, METH_VARARGS, "Sets a Javascript global variable."}, 769 | {"set_memory_limit", 770 | (PyCFunction)runtime_set_memory_limit, 771 | METH_VARARGS, 772 | "Sets the memory limit in bytes."}, 773 | {"set_time_limit", 774 | (PyCFunction)runtime_set_time_limit, 775 | METH_VARARGS, 776 | "Sets the CPU time limit in seconds (C function clock() is used)."}, 777 | {"set_max_stack_size", 778 | (PyCFunction)runtime_set_max_stack_size, 779 | METH_VARARGS, 780 | "Sets the maximum stack size in bytes. Default is 256kB."}, 781 | {"memory", (PyCFunction)runtime_memory, METH_NOARGS, "Returns the memory usage as a dict."}, 782 | {"gc", (PyCFunction)runtime_gc, METH_NOARGS, "Runs garbage collection."}, 783 | {"add_callable", (PyCFunction)runtime_add_callable, METH_VARARGS, "Wraps a Python callable."}, 784 | {NULL} /* Sentinel */ 785 | }; 786 | 787 | // All getsetters (properties) of the _quickjs.Context class. 788 | static PyGetSetDef runtime_getsetters[] = { 789 | {"globalThis", (getter)runtime_global_this, NULL, "Global object of the context.", NULL}, 790 | {NULL} /* Sentinel */ 791 | }; 792 | 793 | // Define the _quickjs.Context type. 794 | static PyTypeObject Context = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Context", 795 | .tp_doc = "Quickjs context", 796 | .tp_basicsize = sizeof(RuntimeData), 797 | .tp_itemsize = 0, 798 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, 799 | .tp_traverse = (traverseproc)runtime_traverse, 800 | .tp_clear = (inquiry)runtime_clear, 801 | .tp_new = runtime_new, 802 | .tp_dealloc = (destructor)runtime_dealloc, 803 | .tp_methods = runtime_methods, 804 | .tp_getset = runtime_getsetters}; 805 | 806 | // All global methods in _quickjs. 807 | static PyMethodDef myextension_methods[] = {{"test", (PyCFunction)test, METH_NOARGS, NULL}, 808 | {NULL, NULL}}; 809 | 810 | // Define the _quickjs module. 811 | static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT, 812 | "quickjs", 813 | NULL, 814 | sizeof(struct module_state), 815 | myextension_methods, 816 | NULL, 817 | NULL, 818 | NULL, 819 | NULL}; 820 | 821 | // This function runs when the module is first imported. 822 | PyMODINIT_FUNC PyInit__quickjs(void) { 823 | if (PyType_Ready(&Context) < 0) { 824 | return NULL; 825 | } 826 | if (PyType_Ready(&Object) < 0) { 827 | return NULL; 828 | } 829 | 830 | PyObject *module = PyModule_Create(&moduledef); 831 | if (module == NULL) { 832 | return NULL; 833 | } 834 | 835 | JS_NewClassID(&js_python_function_class_id); 836 | 837 | JSException = PyErr_NewException("_quickjs.JSException", NULL, NULL); 838 | if (JSException == NULL) { 839 | return NULL; 840 | } 841 | StackOverflow = PyErr_NewException("_quickjs.StackOverflow", JSException, NULL); 842 | if (StackOverflow == NULL) { 843 | return NULL; 844 | } 845 | 846 | Py_INCREF(&Context); 847 | PyModule_AddObject(module, "Context", (PyObject *)&Context); 848 | Py_INCREF(&Object); 849 | PyModule_AddObject(module, "Object", (PyObject *)&Object); 850 | PyModule_AddObject(module, "JSException", JSException); 851 | PyModule_AddObject(module, "StackOverflow", StackOverflow); 852 | return module; 853 | } 854 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "certifi" 5 | version = "2023.7.22" 6 | description = "Python package for providing Mozilla's CA Bundle." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 11 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 12 | ] 13 | 14 | [[package]] 15 | name = "cffi" 16 | version = "1.16.0" 17 | description = "Foreign Function Interface for Python calling C code." 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 22 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 23 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 24 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 25 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 26 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 27 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 28 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 29 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 30 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 31 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 32 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 33 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 34 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 35 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 36 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 37 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 38 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 39 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 40 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 41 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 42 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 43 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 44 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 45 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 46 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 47 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 48 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 49 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 50 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 51 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 52 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 53 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 54 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 55 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 56 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 57 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 58 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 59 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 60 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 61 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 62 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 63 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 64 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 65 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 66 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 67 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 68 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 69 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 70 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 71 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 72 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 73 | ] 74 | 75 | [package.dependencies] 76 | pycparser = "*" 77 | 78 | [[package]] 79 | name = "charset-normalizer" 80 | version = "3.3.2" 81 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 82 | optional = false 83 | python-versions = ">=3.7.0" 84 | files = [ 85 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 86 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 87 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 88 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 89 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 90 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 91 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 92 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 93 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 94 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 95 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 96 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 97 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 98 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 99 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 100 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 101 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 102 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 103 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 104 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 105 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 106 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 107 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 108 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 109 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 110 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 111 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 112 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 113 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 114 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 115 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 116 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 117 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 118 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 119 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 120 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 121 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 122 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 123 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 124 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 125 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 126 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 127 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 128 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 129 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 130 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 131 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 132 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 133 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 134 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 135 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 136 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 137 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 138 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 139 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 140 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 141 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 142 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 143 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 144 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 145 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 146 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 147 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 148 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 149 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 150 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 151 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 152 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 153 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 154 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 155 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 156 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 157 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 158 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 159 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 160 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 161 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 162 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 163 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 164 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 165 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 166 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 167 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 168 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 169 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 170 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 171 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 172 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 173 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 174 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 175 | ] 176 | 177 | [[package]] 178 | name = "cryptography" 179 | version = "41.0.6" 180 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 181 | optional = false 182 | python-versions = ">=3.7" 183 | files = [ 184 | {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, 185 | {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, 186 | {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, 187 | {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, 188 | {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, 189 | {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, 190 | {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, 191 | {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, 192 | {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, 193 | {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, 194 | {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, 195 | {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, 196 | {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, 197 | {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, 198 | {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, 199 | {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, 200 | {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, 201 | {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, 202 | {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, 203 | {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, 204 | {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, 205 | {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, 206 | {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, 207 | ] 208 | 209 | [package.dependencies] 210 | cffi = ">=1.12" 211 | 212 | [package.extras] 213 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 214 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 215 | nox = ["nox"] 216 | pep8test = ["black", "check-sdist", "mypy", "ruff"] 217 | sdist = ["build"] 218 | ssh = ["bcrypt (>=3.1.5)"] 219 | test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 220 | test-randomorder = ["pytest-randomly"] 221 | 222 | [[package]] 223 | name = "docutils" 224 | version = "0.20.1" 225 | description = "Docutils -- Python Documentation Utilities" 226 | optional = false 227 | python-versions = ">=3.7" 228 | files = [ 229 | {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, 230 | {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, 231 | ] 232 | 233 | [[package]] 234 | name = "idna" 235 | version = "3.4" 236 | description = "Internationalized Domain Names in Applications (IDNA)" 237 | optional = false 238 | python-versions = ">=3.5" 239 | files = [ 240 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 241 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 242 | ] 243 | 244 | [[package]] 245 | name = "importlib-metadata" 246 | version = "6.8.0" 247 | description = "Read metadata from Python packages" 248 | optional = false 249 | python-versions = ">=3.8" 250 | files = [ 251 | {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, 252 | {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, 253 | ] 254 | 255 | [package.dependencies] 256 | zipp = ">=0.5" 257 | 258 | [package.extras] 259 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 260 | perf = ["ipython"] 261 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 262 | 263 | [[package]] 264 | name = "importlib-resources" 265 | version = "6.1.0" 266 | description = "Read resources from Python packages" 267 | optional = false 268 | python-versions = ">=3.8" 269 | files = [ 270 | {file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"}, 271 | {file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"}, 272 | ] 273 | 274 | [package.dependencies] 275 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 276 | 277 | [package.extras] 278 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] 279 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] 280 | 281 | [[package]] 282 | name = "jaraco-classes" 283 | version = "3.3.0" 284 | description = "Utility functions for Python class constructs" 285 | optional = false 286 | python-versions = ">=3.8" 287 | files = [ 288 | {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, 289 | {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, 290 | ] 291 | 292 | [package.dependencies] 293 | more-itertools = "*" 294 | 295 | [package.extras] 296 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 297 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 298 | 299 | [[package]] 300 | name = "jeepney" 301 | version = "0.8.0" 302 | description = "Low-level, pure Python DBus protocol wrapper." 303 | optional = false 304 | python-versions = ">=3.7" 305 | files = [ 306 | {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, 307 | {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, 308 | ] 309 | 310 | [package.extras] 311 | test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] 312 | trio = ["async_generator", "trio"] 313 | 314 | [[package]] 315 | name = "keyring" 316 | version = "24.2.0" 317 | description = "Store and access your passwords safely." 318 | optional = false 319 | python-versions = ">=3.8" 320 | files = [ 321 | {file = "keyring-24.2.0-py3-none-any.whl", hash = "sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6"}, 322 | {file = "keyring-24.2.0.tar.gz", hash = "sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509"}, 323 | ] 324 | 325 | [package.dependencies] 326 | importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} 327 | importlib-resources = {version = "*", markers = "python_version < \"3.9\""} 328 | "jaraco.classes" = "*" 329 | jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} 330 | pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} 331 | SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} 332 | 333 | [package.extras] 334 | completion = ["shtab"] 335 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 336 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 337 | 338 | [[package]] 339 | name = "markdown-it-py" 340 | version = "3.0.0" 341 | description = "Python port of markdown-it. Markdown parsing, done right!" 342 | optional = false 343 | python-versions = ">=3.8" 344 | files = [ 345 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 346 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 347 | ] 348 | 349 | [package.dependencies] 350 | mdurl = ">=0.1,<1.0" 351 | 352 | [package.extras] 353 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 354 | code-style = ["pre-commit (>=3.0,<4.0)"] 355 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 356 | linkify = ["linkify-it-py (>=1,<3)"] 357 | plugins = ["mdit-py-plugins"] 358 | profiling = ["gprof2dot"] 359 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 360 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 361 | 362 | [[package]] 363 | name = "mdurl" 364 | version = "0.1.2" 365 | description = "Markdown URL utilities" 366 | optional = false 367 | python-versions = ">=3.7" 368 | files = [ 369 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 370 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 371 | ] 372 | 373 | [[package]] 374 | name = "more-itertools" 375 | version = "10.1.0" 376 | description = "More routines for operating on iterables, beyond itertools" 377 | optional = false 378 | python-versions = ">=3.8" 379 | files = [ 380 | {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, 381 | {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, 382 | ] 383 | 384 | [[package]] 385 | name = "nh3" 386 | version = "0.2.14" 387 | description = "Ammonia HTML sanitizer Python binding" 388 | optional = false 389 | python-versions = "*" 390 | files = [ 391 | {file = "nh3-0.2.14-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a"}, 392 | {file = "nh3-0.2.14-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75"}, 393 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450"}, 394 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e"}, 395 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e"}, 396 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad"}, 397 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2"}, 398 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525"}, 399 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6"}, 400 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4"}, 401 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5"}, 402 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d"}, 403 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6"}, 404 | {file = "nh3-0.2.14-cp37-abi3-win32.whl", hash = "sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873"}, 405 | {file = "nh3-0.2.14-cp37-abi3-win_amd64.whl", hash = "sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e"}, 406 | {file = "nh3-0.2.14.tar.gz", hash = "sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4"}, 407 | ] 408 | 409 | [[package]] 410 | name = "pkginfo" 411 | version = "1.9.6" 412 | description = "Query metadata from sdists / bdists / installed packages." 413 | optional = false 414 | python-versions = ">=3.6" 415 | files = [ 416 | {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, 417 | {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, 418 | ] 419 | 420 | [package.extras] 421 | testing = ["pytest", "pytest-cov"] 422 | 423 | [[package]] 424 | name = "platformdirs" 425 | version = "3.11.0" 426 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 427 | optional = false 428 | python-versions = ">=3.7" 429 | files = [ 430 | {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, 431 | {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, 432 | ] 433 | 434 | [package.extras] 435 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 436 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 437 | 438 | [[package]] 439 | name = "pycparser" 440 | version = "2.21" 441 | description = "C parser in Python" 442 | optional = false 443 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 444 | files = [ 445 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 446 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 447 | ] 448 | 449 | [[package]] 450 | name = "pygments" 451 | version = "2.16.1" 452 | description = "Pygments is a syntax highlighting package written in Python." 453 | optional = false 454 | python-versions = ">=3.7" 455 | files = [ 456 | {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, 457 | {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, 458 | ] 459 | 460 | [package.extras] 461 | plugins = ["importlib-metadata"] 462 | 463 | [[package]] 464 | name = "pywin32-ctypes" 465 | version = "0.2.2" 466 | description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 467 | optional = false 468 | python-versions = ">=3.6" 469 | files = [ 470 | {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, 471 | {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, 472 | ] 473 | 474 | [[package]] 475 | name = "readme-renderer" 476 | version = "42.0" 477 | description = "readme_renderer is a library for rendering readme descriptions for Warehouse" 478 | optional = false 479 | python-versions = ">=3.8" 480 | files = [ 481 | {file = "readme_renderer-42.0-py3-none-any.whl", hash = "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d"}, 482 | {file = "readme_renderer-42.0.tar.gz", hash = "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1"}, 483 | ] 484 | 485 | [package.dependencies] 486 | docutils = ">=0.13.1" 487 | nh3 = ">=0.2.14" 488 | Pygments = ">=2.5.1" 489 | 490 | [package.extras] 491 | md = ["cmarkgfm (>=0.8.0)"] 492 | 493 | [[package]] 494 | name = "requests" 495 | version = "2.31.0" 496 | description = "Python HTTP for Humans." 497 | optional = false 498 | python-versions = ">=3.7" 499 | files = [ 500 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 501 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 502 | ] 503 | 504 | [package.dependencies] 505 | certifi = ">=2017.4.17" 506 | charset-normalizer = ">=2,<4" 507 | idna = ">=2.5,<4" 508 | urllib3 = ">=1.21.1,<3" 509 | 510 | [package.extras] 511 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 512 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 513 | 514 | [[package]] 515 | name = "requests-toolbelt" 516 | version = "1.0.0" 517 | description = "A utility belt for advanced users of python-requests" 518 | optional = false 519 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 520 | files = [ 521 | {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, 522 | {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, 523 | ] 524 | 525 | [package.dependencies] 526 | requests = ">=2.0.1,<3.0.0" 527 | 528 | [[package]] 529 | name = "rfc3986" 530 | version = "2.0.0" 531 | description = "Validating URI References per RFC 3986" 532 | optional = false 533 | python-versions = ">=3.7" 534 | files = [ 535 | {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, 536 | {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, 537 | ] 538 | 539 | [package.extras] 540 | idna2008 = ["idna"] 541 | 542 | [[package]] 543 | name = "rich" 544 | version = "13.6.0" 545 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 546 | optional = false 547 | python-versions = ">=3.7.0" 548 | files = [ 549 | {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, 550 | {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, 551 | ] 552 | 553 | [package.dependencies] 554 | markdown-it-py = ">=2.2.0" 555 | pygments = ">=2.13.0,<3.0.0" 556 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 557 | 558 | [package.extras] 559 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 560 | 561 | [[package]] 562 | name = "secretstorage" 563 | version = "3.3.3" 564 | description = "Python bindings to FreeDesktop.org Secret Service API" 565 | optional = false 566 | python-versions = ">=3.6" 567 | files = [ 568 | {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, 569 | {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, 570 | ] 571 | 572 | [package.dependencies] 573 | cryptography = ">=2.0" 574 | jeepney = ">=0.6" 575 | 576 | [[package]] 577 | name = "setuptools" 578 | version = "68.2.2" 579 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 580 | optional = false 581 | python-versions = ">=3.8" 582 | files = [ 583 | {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, 584 | {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, 585 | ] 586 | 587 | [package.extras] 588 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 589 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 590 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 591 | 592 | [[package]] 593 | name = "tomli" 594 | version = "2.0.1" 595 | description = "A lil' TOML parser" 596 | optional = false 597 | python-versions = ">=3.7" 598 | files = [ 599 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 600 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 601 | ] 602 | 603 | [[package]] 604 | name = "twine" 605 | version = "4.0.2" 606 | description = "Collection of utilities for publishing packages on PyPI" 607 | optional = false 608 | python-versions = ">=3.7" 609 | files = [ 610 | {file = "twine-4.0.2-py3-none-any.whl", hash = "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8"}, 611 | {file = "twine-4.0.2.tar.gz", hash = "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"}, 612 | ] 613 | 614 | [package.dependencies] 615 | importlib-metadata = ">=3.6" 616 | keyring = ">=15.1" 617 | pkginfo = ">=1.8.1" 618 | readme-renderer = ">=35.0" 619 | requests = ">=2.20" 620 | requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" 621 | rfc3986 = ">=1.4.0" 622 | rich = ">=12.0.0" 623 | urllib3 = ">=1.26.0" 624 | 625 | [[package]] 626 | name = "typing-extensions" 627 | version = "4.8.0" 628 | description = "Backported and Experimental Type Hints for Python 3.8+" 629 | optional = false 630 | python-versions = ">=3.8" 631 | files = [ 632 | {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, 633 | {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, 634 | ] 635 | 636 | [[package]] 637 | name = "urllib3" 638 | version = "2.0.7" 639 | description = "HTTP library with thread-safe connection pooling, file post, and more." 640 | optional = false 641 | python-versions = ">=3.7" 642 | files = [ 643 | {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, 644 | {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, 645 | ] 646 | 647 | [package.extras] 648 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 649 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 650 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 651 | zstd = ["zstandard (>=0.18.0)"] 652 | 653 | [[package]] 654 | name = "wheel" 655 | version = "0.41.3" 656 | description = "A built-package format for Python" 657 | optional = false 658 | python-versions = ">=3.7" 659 | files = [ 660 | {file = "wheel-0.41.3-py3-none-any.whl", hash = "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942"}, 661 | {file = "wheel-0.41.3.tar.gz", hash = "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841"}, 662 | ] 663 | 664 | [package.extras] 665 | test = ["pytest (>=6.0.0)", "setuptools (>=65)"] 666 | 667 | [[package]] 668 | name = "yapf" 669 | version = "0.40.2" 670 | description = "A formatter for Python code" 671 | optional = false 672 | python-versions = ">=3.7" 673 | files = [ 674 | {file = "yapf-0.40.2-py3-none-any.whl", hash = "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b"}, 675 | {file = "yapf-0.40.2.tar.gz", hash = "sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b"}, 676 | ] 677 | 678 | [package.dependencies] 679 | importlib-metadata = ">=6.6.0" 680 | platformdirs = ">=3.5.1" 681 | tomli = ">=2.0.1" 682 | 683 | [[package]] 684 | name = "zipp" 685 | version = "3.17.0" 686 | description = "Backport of pathlib-compatible object wrapper for zip files" 687 | optional = false 688 | python-versions = ">=3.8" 689 | files = [ 690 | {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, 691 | {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, 692 | ] 693 | 694 | [package.extras] 695 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] 696 | testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 697 | 698 | [metadata] 699 | lock-version = "2.0" 700 | python-versions = ">=3.8" 701 | content-hash = "1369bf7409afa84ba0c69c4dd219f959171531e7e2747782f65a2c0807aa4036" 702 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "quickjs" 3 | version = "0" # Version in setup.py is used for distributing. 4 | description = "Python wrapper around https://bellard.org/quickjs/" 5 | authors = ["Petter Strandmark"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8" 9 | 10 | [tool.poetry.dev-dependencies] 11 | yapf = "*" 12 | setuptools = "*" 13 | wheel = "*" 14 | twine = "*" 15 | 16 | [build-system] 17 | requires = ["poetry>=1.0.5"] 18 | -------------------------------------------------------------------------------- /quickjs/__init__.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import json 3 | import threading 4 | from typing import Tuple, Callable 5 | 6 | import _quickjs 7 | 8 | 9 | def test(): 10 | return _quickjs.test() 11 | 12 | 13 | Context = _quickjs.Context 14 | Object = _quickjs.Object 15 | JSException = _quickjs.JSException 16 | StackOverflow = _quickjs.StackOverflow 17 | 18 | 19 | class Function: 20 | # There are unit tests demonstrating that we are crashing if different threads are accessing the 21 | # same runtime, even if it is not at the same time. So we run everything on the same thread in 22 | # order to prevent this. 23 | _threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1) 24 | 25 | def __init__(self, name: str, code: str, *, own_executor=False) -> None: 26 | """ 27 | Arguments: 28 | name: The name of the function in the provided code that will be executed. 29 | code: The source code of the function and possibly helper functions, classes, global 30 | variables etc. 31 | own_executor: Create an executor specifically for this function. The default is False in 32 | order to save system resources if a large number of functions are created. 33 | """ 34 | if own_executor: 35 | self._threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1) 36 | self._lock = threading.Lock() 37 | 38 | future = self._threadpool.submit(self._compile, name, code) 39 | concurrent.futures.wait([future]) 40 | self._context, self._f = future.result() 41 | 42 | def __call__(self, *args, run_gc=True): 43 | with self._lock: 44 | future = self._threadpool.submit(self._call, *args, run_gc=run_gc) 45 | concurrent.futures.wait([future]) 46 | return future.result() 47 | 48 | def set_memory_limit(self, limit): 49 | with self._lock: 50 | return self._context.set_memory_limit(limit) 51 | 52 | def set_time_limit(self, limit): 53 | with self._lock: 54 | return self._context.set_time_limit(limit) 55 | 56 | def set_max_stack_size(self, limit): 57 | with self._lock: 58 | return self._context.set_max_stack_size(limit) 59 | 60 | def memory(self): 61 | with self._lock: 62 | return self._context.memory() 63 | 64 | def add_callable(self, global_name: str, callable: Callable) -> None: 65 | with self._lock: 66 | self._context.add_callable(global_name, callable) 67 | 68 | def gc(self): 69 | """Manually run the garbage collection. 70 | 71 | It will run by default when calling the function unless otherwise specified. 72 | """ 73 | with self._lock: 74 | self._context.gc() 75 | 76 | def execute_pending_job(self) -> bool: 77 | with self._lock: 78 | return self._context.execute_pending_job() 79 | 80 | @property 81 | def globalThis(self) -> Object: 82 | with self._lock: 83 | return self._context.globalThis 84 | 85 | def _compile(self, name: str, code: str) -> Tuple[Context, Object]: 86 | context = Context() 87 | context.eval(code) 88 | f = context.get(name) 89 | return context, f 90 | 91 | def _call(self, *args, run_gc=True): 92 | def convert_arg(arg): 93 | if isinstance(arg, (type(None), str, bool, float, int)): 94 | return arg 95 | else: 96 | # More complex objects are passed through JSON. 97 | return self._context.parse_json(json.dumps(arg)) 98 | 99 | try: 100 | result = self._f(*[convert_arg(a) for a in args]) 101 | if isinstance(result, Object): 102 | result = json.loads(result.json()) 103 | return result 104 | finally: 105 | if run_gc: 106 | self._context.gc() 107 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import sys 3 | from typing import List 4 | 5 | from setuptools import setup, Extension 6 | 7 | CONFIG_VERSION = open("upstream-quickjs/VERSION").read().strip() 8 | extra_link_args: List[str] = [] 9 | 10 | if sys.platform == "win32": 11 | # To build for Windows: 12 | # 1. Install MingW-W64-builds from https://mingw-w64.org/doku.php/download 13 | # It is important to change the default to 64-bit when installing if a 14 | # 64-bit Python is installed in windows. 15 | # 2. Put the bin/ folder inside x86_64-8.1.0-posix-seh-rt_v6-rev0 in your 16 | # system PATH when compiling. 17 | # 3. The code below will moneky-patch distutils to work. 18 | import distutils.cygwinccompiler 19 | distutils.cygwinccompiler.get_msvcr = lambda: [] 20 | # Make sure that pthreads is linked statically, otherwise we run into problems 21 | # on computers where it is not installed. 22 | extra_link_args = ["-static"] 23 | 24 | 25 | def get_c_sources(include_headers=False): 26 | sources = [ 27 | "module.c", 28 | "upstream-quickjs/cutils.c", 29 | "upstream-quickjs/libbf.c", 30 | "upstream-quickjs/libregexp.c", 31 | "upstream-quickjs/libunicode.c", 32 | "upstream-quickjs/quickjs.c", 33 | ] 34 | if include_headers: 35 | sources += [ 36 | "upstream-quickjs/cutils.h", 37 | "upstream-quickjs/libbf.h", 38 | "upstream-quickjs/libregexp-opcode.h", 39 | "upstream-quickjs/libregexp.h", 40 | "upstream-quickjs/libunicode-table.h", 41 | "upstream-quickjs/libunicode.h", 42 | "upstream-quickjs/list.h", 43 | "upstream-quickjs/quickjs-atom.h", 44 | "upstream-quickjs/quickjs-opcode.h", 45 | "upstream-quickjs/quickjs.h", 46 | "upstream-quickjs/VERSION", 47 | ] 48 | return sources 49 | 50 | 51 | _quickjs = Extension( 52 | '_quickjs', 53 | define_macros=[('CONFIG_VERSION', f'"{CONFIG_VERSION}"'), ('CONFIG_BIGNUM', None)], 54 | # HACK. 55 | # See https://github.com/pypa/packaging-problems/issues/84. 56 | sources=get_c_sources(include_headers=("sdist" in sys.argv)), 57 | extra_compile_args=["-Werror=incompatible-pointer-types"], 58 | extra_link_args=extra_link_args) 59 | 60 | long_description = """ 61 | Python wrapper around https://bellard.org/quickjs/ . 62 | 63 | Translates types like `str`, `float`, `bool`, `list`, `dict` and combinations 64 | thereof to and from Javascript. 65 | 66 | QuickJS is currently thread-hostile, so this wrapper makes sure that all calls 67 | to the same JS runtime comes from the same thead. 68 | """ 69 | 70 | setup(author="Petter Strandmark", 71 | author_email="petter.strandmark@gmail.com", 72 | maintainer="Quentin Wenger", 73 | maintainer_email="matpi@protonmail.ch", 74 | name='quickjs', 75 | url='https://github.com/PetterS/quickjs', 76 | version='1.19.4', 77 | description='Wrapping the quickjs C library.', 78 | long_description=long_description, 79 | packages=["quickjs"], 80 | ext_modules=[_quickjs]) 81 | -------------------------------------------------------------------------------- /test_quickjs.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import gc 3 | import json 4 | import unittest 5 | 6 | import quickjs 7 | 8 | 9 | class LoadModule(unittest.TestCase): 10 | def test_42(self): 11 | self.assertEqual(quickjs.test(), 42) 12 | 13 | 14 | class Context(unittest.TestCase): 15 | def setUp(self): 16 | self.context = quickjs.Context() 17 | 18 | def test_eval_int(self): 19 | self.assertEqual(self.context.eval("40 + 2"), 42) 20 | 21 | def test_eval_float(self): 22 | self.assertEqual(self.context.eval("40.0 + 2.0"), 42.0) 23 | 24 | def test_eval_str(self): 25 | self.assertEqual(self.context.eval("'4' + '2'"), "42") 26 | 27 | def test_eval_bool(self): 28 | self.assertEqual(self.context.eval("true || false"), True) 29 | self.assertEqual(self.context.eval("true && false"), False) 30 | 31 | def test_eval_null(self): 32 | self.assertIsNone(self.context.eval("null")) 33 | 34 | def test_eval_undefined(self): 35 | self.assertIsNone(self.context.eval("undefined")) 36 | 37 | def test_wrong_type(self): 38 | with self.assertRaises(TypeError): 39 | self.assertEqual(self.context.eval(1), 42) 40 | 41 | def test_context_between_calls(self): 42 | self.context.eval("x = 40; y = 2;") 43 | self.assertEqual(self.context.eval("x + y"), 42) 44 | 45 | def test_function(self): 46 | self.context.eval(""" 47 | function special(x) { 48 | return 40 + x; 49 | } 50 | """) 51 | self.assertEqual(self.context.eval("special(2)"), 42) 52 | 53 | def test_get(self): 54 | self.context.eval("x = 42; y = 'foo';") 55 | self.assertEqual(self.context.get("x"), 42) 56 | self.assertEqual(self.context.get("y"), "foo") 57 | self.assertEqual(self.context.get("z"), None) 58 | 59 | def test_set(self): 60 | self.context.eval("x = 'overriden'") 61 | self.context.set("x", 42) 62 | self.context.set("y", "foo") 63 | self.assertTrue(self.context.eval("x == 42")) 64 | self.assertTrue(self.context.eval("y == 'foo'")) 65 | 66 | def test_module(self): 67 | self.context.module(""" 68 | export function test() { 69 | return 42; 70 | } 71 | """) 72 | 73 | def test_error(self): 74 | with self.assertRaisesRegex(quickjs.JSException, "ReferenceError: 'missing' is not defined"): 75 | self.context.eval("missing + missing") 76 | 77 | def test_lifetime(self): 78 | def get_f(): 79 | context = quickjs.Context() 80 | f = context.eval(""" 81 | a = function(x) { 82 | return 40 + x; 83 | } 84 | """) 85 | return f 86 | 87 | f = get_f() 88 | self.assertTrue(f) 89 | # The context has left the scope after f. f needs to keep the context alive for the 90 | # its lifetime. Otherwise, we will get problems. 91 | 92 | def test_backtrace(self): 93 | try: 94 | self.context.eval(""" 95 | function funcA(x) { 96 | x.a.b = 1; 97 | } 98 | function funcB(x) { 99 | funcA(x); 100 | } 101 | funcB({}); 102 | """) 103 | except Exception as e: 104 | msg = str(e) 105 | else: 106 | self.fail("Expected exception.") 107 | 108 | self.assertIn("at funcA (:3)\n", msg) 109 | self.assertIn("at funcB (:6)\n", msg) 110 | 111 | def test_memory_limit(self): 112 | code = """ 113 | (function() { 114 | let arr = []; 115 | for (let i = 0; i < 1000; ++i) { 116 | arr.push(i); 117 | } 118 | })(); 119 | """ 120 | self.context.eval(code) 121 | self.context.set_memory_limit(1000) 122 | with self.assertRaisesRegex(quickjs.JSException, "null"): 123 | self.context.eval(code) 124 | self.context.set_memory_limit(1000000) 125 | self.context.eval(code) 126 | 127 | def test_time_limit(self): 128 | code = """ 129 | (function() { 130 | let arr = []; 131 | for (let i = 0; i < 100000; ++i) { 132 | arr.push(i); 133 | } 134 | return arr; 135 | })(); 136 | """ 137 | self.context.eval(code) 138 | self.context.set_time_limit(0) 139 | with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"): 140 | self.context.eval(code) 141 | self.context.set_time_limit(-1) 142 | self.context.eval(code) 143 | 144 | def test_memory_usage(self): 145 | self.assertIn("memory_used_size", self.context.memory().keys()) 146 | 147 | def test_json_simple(self): 148 | self.assertEqual(self.context.parse_json("42"), 42) 149 | 150 | def test_json_error(self): 151 | with self.assertRaisesRegex(quickjs.JSException, "unexpected token"): 152 | self.context.parse_json("a b c") 153 | 154 | def test_execute_pending_job(self): 155 | self.context.eval("obj = {}") 156 | self.assertEqual(self.context.execute_pending_job(), False) 157 | self.context.eval("Promise.resolve().then(() => {obj.x = 1;})") 158 | self.assertEqual(self.context.execute_pending_job(), True) 159 | self.assertEqual(self.context.eval("obj.x"), 1) 160 | self.assertEqual(self.context.execute_pending_job(), False) 161 | 162 | def test_global(self): 163 | self.context.set("f", self.context.globalThis) 164 | self.assertTrue(isinstance(self.context.globalThis, quickjs.Object)) 165 | self.assertTrue(self.context.eval("f === globalThis")) 166 | with self.assertRaises(AttributeError): 167 | self.context.globalThis = 1 168 | 169 | 170 | class CallIntoPython(unittest.TestCase): 171 | def setUp(self): 172 | self.context = quickjs.Context() 173 | 174 | def test_make_function(self): 175 | self.context.add_callable("f", lambda x: x + 2) 176 | self.assertEqual(self.context.eval("f(40)"), 42) 177 | self.assertEqual(self.context.eval("f.name"), "f") 178 | 179 | def test_make_two_functions(self): 180 | for i in range(10): 181 | self.context.add_callable("f", lambda x: i + x + 2) 182 | self.context.add_callable("g", lambda x: i + x + 40) 183 | f = self.context.get("f") 184 | g = self.context.get("g") 185 | self.assertEqual(f(40) - i, 42) 186 | self.assertEqual(g(2) - i, 42) 187 | self.assertEqual(self.context.eval("((f, a) => f(a))")(f, 40) - i, 42) 188 | 189 | def test_make_function_call_from_js(self): 190 | self.context.add_callable("f", lambda x: x + 2) 191 | g = self.context.eval("""( 192 | function() { 193 | return f(20) + 20; 194 | } 195 | )""") 196 | self.assertEqual(g(), 42) 197 | 198 | def test_python_function_raises(self): 199 | def error(a): 200 | raise ValueError("A") 201 | 202 | self.context.add_callable("error", error) 203 | with self.assertRaisesRegex(quickjs.JSException, "Python call failed"): 204 | self.context.eval("error(0)") 205 | 206 | def test_python_function_not_callable(self): 207 | with self.assertRaisesRegex(TypeError, "Argument must be callable."): 208 | self.context.add_callable("not_callable", 1) 209 | 210 | def test_python_function_no_slots(self): 211 | for i in range(2**16): 212 | self.context.add_callable(f"a{i}", lambda i=i: i + 1) 213 | self.assertEqual(self.context.eval("a0()"), 1) 214 | self.assertEqual(self.context.eval(f"a{2**16 - 1}()"), 2**16) 215 | 216 | def test_function_after_context_del(self): 217 | def make(): 218 | ctx = quickjs.Context() 219 | ctx.add_callable("f", lambda: 1) 220 | f = ctx.get("f") 221 | del ctx 222 | return f 223 | gc.collect() 224 | f = make() 225 | self.assertEqual(f(), 1) 226 | 227 | def test_python_function_unwritable(self): 228 | self.context.eval(""" 229 | Object.defineProperty(globalThis, "obj", { 230 | value: "test", 231 | writable: false, 232 | }); 233 | """) 234 | with self.assertRaisesRegex(TypeError, "Failed adding the callable."): 235 | self.context.add_callable("obj", lambda: None) 236 | 237 | def test_python_function_is_function(self): 238 | self.context.add_callable("f", lambda: None) 239 | self.assertTrue(self.context.eval("f instanceof Function")) 240 | self.assertTrue(self.context.eval("typeof f === 'function'")) 241 | 242 | def test_make_function_two_args(self): 243 | def concat(a, b): 244 | return a + b 245 | 246 | self.context.add_callable("concat", concat) 247 | result = self.context.eval("concat(40, 2)") 248 | self.assertEqual(result, 42) 249 | 250 | concat = self.context.get("concat") 251 | result = self.context.eval("((f, a, b) => 22 + f(a, b))")(concat, 10, 10) 252 | self.assertEqual(result, 42) 253 | 254 | def test_make_function_two_string_args(self): 255 | """Without the JS_DupValue in js_c_function, this test crashes.""" 256 | def concat(a, b): 257 | return a + "-" + b 258 | 259 | self.context.add_callable("concat", concat) 260 | concat = self.context.get("concat") 261 | result = concat("aaa", "bbb") 262 | self.assertEqual(result, "aaa-bbb") 263 | 264 | def test_can_eval_in_same_context(self): 265 | self.context.add_callable("f", lambda: 40 + self.context.eval("1 + 1")) 266 | self.assertEqual(self.context.eval("f()"), 42) 267 | 268 | def test_can_call_in_same_context(self): 269 | inner = self.context.eval("(function() { return 42; })") 270 | self.context.add_callable("f", lambda: inner()) 271 | self.assertEqual(self.context.eval("f()"), 42) 272 | 273 | def test_delete_function_from_inside_js(self): 274 | self.context.add_callable("f", lambda: None) 275 | # Segfaults if js_python_function_finalizer does not handle threading 276 | # states carefully. 277 | self.context.eval("delete f") 278 | self.assertIsNone(self.context.get("f")) 279 | 280 | def test_invalid_argument(self): 281 | self.context.add_callable("p", lambda: 42) 282 | self.assertEqual(self.context.eval("p()"), 42) 283 | with self.assertRaisesRegex(quickjs.JSException, "Python call failed"): 284 | self.context.eval("p(1)") 285 | with self.assertRaisesRegex(quickjs.JSException, "Python call failed"): 286 | self.context.eval("p({})") 287 | 288 | def test_time_limit_disallowed(self): 289 | self.context.add_callable("f", lambda x: x + 2) 290 | self.context.set_time_limit(1000) 291 | with self.assertRaises(quickjs.JSException): 292 | self.context.eval("f(40)") 293 | 294 | def test_conversion_failure_does_not_raise_system_error(self): 295 | # https://github.com/PetterS/quickjs/issues/38 296 | 297 | def test_list(): 298 | return [1, 2, 3] 299 | 300 | self.context.add_callable("test_list", test_list) 301 | with self.assertRaises(quickjs.JSException): 302 | # With incorrect error handling, this (safely) made Python raise a SystemError 303 | # instead of a JS exception. 304 | self.context.eval("test_list()") 305 | 306 | 307 | class Object(unittest.TestCase): 308 | def setUp(self): 309 | self.context = quickjs.Context() 310 | 311 | def test_function_is_object(self): 312 | f = self.context.eval(""" 313 | a = function(x) { 314 | return 40 + x; 315 | } 316 | """) 317 | self.assertIsInstance(f, quickjs.Object) 318 | 319 | def test_function_call_int(self): 320 | f = self.context.eval(""" 321 | f = function(x) { 322 | return 40 + x; 323 | } 324 | """) 325 | self.assertEqual(f(2), 42) 326 | 327 | def test_function_call_int_two_args(self): 328 | f = self.context.eval(""" 329 | f = function(x, y) { 330 | return 40 + x + y; 331 | } 332 | """) 333 | self.assertEqual(f(3, -1), 42) 334 | 335 | def test_function_call_many_times(self): 336 | n = 1000 337 | f = self.context.eval(""" 338 | f = function(x, y) { 339 | return x + y; 340 | } 341 | """) 342 | s = 0 343 | for i in range(n): 344 | s += f(1, 1) 345 | self.assertEqual(s, 2 * n) 346 | 347 | def test_function_call_str(self): 348 | f = self.context.eval(""" 349 | f = function(a) { 350 | return a + " hej"; 351 | } 352 | """) 353 | self.assertEqual(f("1"), "1 hej") 354 | 355 | def test_function_call_str_three_args(self): 356 | f = self.context.eval(""" 357 | f = function(a, b, c) { 358 | return a + " hej " + b + " ho " + c; 359 | } 360 | """) 361 | self.assertEqual(f("1", "2", "3"), "1 hej 2 ho 3") 362 | 363 | def test_function_call_object(self): 364 | d = self.context.eval("d = {data: 42};") 365 | f = self.context.eval(""" 366 | f = function(d) { 367 | return d.data; 368 | } 369 | """) 370 | self.assertEqual(f(d), 42) 371 | # Try again to make sure refcounting works. 372 | self.assertEqual(f(d), 42) 373 | self.assertEqual(f(d), 42) 374 | 375 | def test_function_call_unsupported_arg(self): 376 | f = self.context.eval(""" 377 | f = function(x) { 378 | return 40 + x; 379 | } 380 | """) 381 | with self.assertRaisesRegex(TypeError, "Unsupported type"): 382 | self.assertEqual(f({}), 42) 383 | 384 | def test_json(self): 385 | d = self.context.eval("d = {data: 42};") 386 | self.assertEqual(json.loads(d.json()), {"data": 42}) 387 | 388 | def test_call_nonfunction(self): 389 | d = self.context.eval("({data: 42})") 390 | with self.assertRaisesRegex(quickjs.JSException, "TypeError: not a function"): 391 | d(1) 392 | 393 | def test_wrong_context(self): 394 | context1 = quickjs.Context() 395 | context2 = quickjs.Context() 396 | f = context1.eval("(function(x) { return x.a; })") 397 | d = context2.eval("({a: 1})") 398 | with self.assertRaisesRegex(ValueError, "Can not mix JS objects from different contexts."): 399 | f(d) 400 | 401 | 402 | class FunctionTest(unittest.TestCase): 403 | def test_adder(self): 404 | f = quickjs.Function( 405 | "adder", """ 406 | function adder(x, y) { 407 | return x + y; 408 | } 409 | """) 410 | self.assertEqual(f(1, 1), 2) 411 | self.assertEqual(f(100, 200), 300) 412 | self.assertEqual(f("a", "b"), "ab") 413 | 414 | def test_identity(self): 415 | identity = quickjs.Function( 416 | "identity", """ 417 | function identity(x) { 418 | return x; 419 | } 420 | """) 421 | for x in [True, [1], {"a": 2}, 1, 1.5, "hej", None]: 422 | self.assertEqual(identity(x), x) 423 | 424 | def test_bool(self): 425 | f = quickjs.Function( 426 | "f", """ 427 | function f(x) { 428 | return [typeof x ,!x]; 429 | } 430 | """) 431 | self.assertEqual(f(False), ["boolean", True]) 432 | self.assertEqual(f(True), ["boolean", False]) 433 | 434 | def test_empty(self): 435 | f = quickjs.Function("f", "function f() { }") 436 | self.assertEqual(f(), None) 437 | 438 | def test_lists(self): 439 | f = quickjs.Function( 440 | "f", """ 441 | function f(arr) { 442 | const result = []; 443 | arr.forEach(function(elem) { 444 | result.push(elem + 42); 445 | }); 446 | return result; 447 | }""") 448 | self.assertEqual(f([0, 1, 2]), [42, 43, 44]) 449 | 450 | def test_dict(self): 451 | f = quickjs.Function( 452 | "f", """ 453 | function f(obj) { 454 | return obj.data; 455 | }""") 456 | self.assertEqual(f({"data": {"value": 42}}), {"value": 42}) 457 | 458 | def test_time_limit(self): 459 | f = quickjs.Function( 460 | "f", """ 461 | function f() { 462 | let arr = []; 463 | for (let i = 0; i < 100000; ++i) { 464 | arr.push(i); 465 | } 466 | return arr; 467 | } 468 | """) 469 | f() 470 | f.set_time_limit(0) 471 | with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"): 472 | f() 473 | f.set_time_limit(-1) 474 | f() 475 | 476 | def test_garbage_collection(self): 477 | f = quickjs.Function( 478 | "f", """ 479 | function f() { 480 | let a = {}; 481 | let b = {}; 482 | a.b = b; 483 | b.a = a; 484 | a.i = 42; 485 | return a.i; 486 | } 487 | """) 488 | initial_count = f.memory()["obj_count"] 489 | for i in range(10): 490 | prev_count = f.memory()["obj_count"] 491 | self.assertEqual(f(run_gc=False), 42) 492 | current_count = f.memory()["obj_count"] 493 | self.assertGreater(current_count, prev_count) 494 | 495 | f.gc() 496 | self.assertLessEqual(f.memory()["obj_count"], initial_count) 497 | 498 | def test_deep_recursion(self): 499 | f = quickjs.Function( 500 | "f", """ 501 | function f(v) { 502 | if (v <= 0) { 503 | return 0; 504 | } else { 505 | return 1 + f(v - 1); 506 | } 507 | } 508 | """) 509 | 510 | self.assertEqual(f(100), 100) 511 | limit = 500 512 | with self.assertRaises(quickjs.StackOverflow): 513 | f(limit) 514 | f.set_max_stack_size(2000 * limit) 515 | self.assertEqual(f(limit), limit) 516 | 517 | def test_add_callable(self): 518 | f = quickjs.Function( 519 | "f", """ 520 | function f() { 521 | return pfunc(); 522 | } 523 | """) 524 | f.add_callable("pfunc", lambda: 42) 525 | 526 | self.assertEqual(f(), 42) 527 | 528 | def test_execute_pending_job(self): 529 | f = quickjs.Function( 530 | "f", """ 531 | obj = {x: 0, y: 0}; 532 | async function a() { 533 | obj.x = await 1; 534 | } 535 | a(); 536 | Promise.resolve().then(() => {obj.y = 1}); 537 | function f() { 538 | return obj.x + obj.y; 539 | } 540 | """) 541 | self.assertEqual(f(), 0) 542 | self.assertEqual(f.execute_pending_job(), True) 543 | self.assertEqual(f(), 1) 544 | self.assertEqual(f.execute_pending_job(), True) 545 | self.assertEqual(f(), 2) 546 | self.assertEqual(f.execute_pending_job(), False) 547 | 548 | def test_global(self): 549 | f = quickjs.Function( 550 | "f", """ 551 | function f() { 552 | } 553 | """) 554 | self.assertTrue(isinstance(f.globalThis, quickjs.Object)) 555 | with self.assertRaises(AttributeError): 556 | f.globalThis = 1 557 | 558 | 559 | class JavascriptFeatures(unittest.TestCase): 560 | def test_unicode_strings(self): 561 | identity = quickjs.Function( 562 | "identity", """ 563 | function identity(x) { 564 | return x; 565 | } 566 | """) 567 | context = quickjs.Context() 568 | for x in ["äpple", "≤≥", "☺"]: 569 | self.assertEqual(identity(x), x) 570 | self.assertEqual(context.eval('(function(){ return "' + x + '";})()'), x) 571 | 572 | def test_es2020_optional_chaining(self): 573 | f = quickjs.Function( 574 | "f", """ 575 | function f(x) { 576 | return x?.one?.two; 577 | } 578 | """) 579 | self.assertIsNone(f({})) 580 | self.assertIsNone(f({"one": 12})) 581 | self.assertEqual(f({"one": {"two": 42}}), 42) 582 | 583 | def test_es2020_null_coalescing(self): 584 | f = quickjs.Function( 585 | "f", """ 586 | function f(x) { 587 | return x ?? 42; 588 | } 589 | """) 590 | self.assertEqual(f(""), "") 591 | self.assertEqual(f(0), 0) 592 | self.assertEqual(f(11), 11) 593 | self.assertEqual(f(None), 42) 594 | 595 | def test_symbol_conversion(self): 596 | context = quickjs.Context() 597 | context.eval("a = Symbol();") 598 | context.set("b", context.eval("a")) 599 | self.assertTrue(context.eval("a === b")) 600 | 601 | def test_large_python_integers_to_quickjs(self): 602 | context = quickjs.Context() 603 | # Without a careful implementation, this made Python raise a SystemError/OverflowError. 604 | context.set("v", 10**25) 605 | # There is precision loss occurring in JS due to 606 | # the floating point implementation of numbers. 607 | self.assertTrue(context.eval("v == 1e25")) 608 | 609 | def test_bigint(self): 610 | context = quickjs.Context() 611 | self.assertEqual(context.eval(f"BigInt('{10**100}')"), 10**100) 612 | self.assertEqual(context.eval(f"BigInt('{-10**100}')"), -10**100) 613 | 614 | class Threads(unittest.TestCase): 615 | def setUp(self): 616 | self.context = quickjs.Context() 617 | self.executor = concurrent.futures.ThreadPoolExecutor() 618 | 619 | def tearDown(self): 620 | self.executor.shutdown() 621 | 622 | def test_concurrent(self): 623 | """Demonstrates that the execution will crash unless the function executes on the same 624 | thread every time. 625 | 626 | If the executor in Function is not present, this test will fail. 627 | """ 628 | data = list(range(1000)) 629 | jssum = quickjs.Function( 630 | "sum", """ 631 | function sum(data) { 632 | return data.reduce((a, b) => a + b, 0) 633 | } 634 | """) 635 | 636 | futures = [self.executor.submit(jssum, data) for _ in range(10)] 637 | expected = sum(data) 638 | for future in concurrent.futures.as_completed(futures): 639 | self.assertEqual(future.result(), expected) 640 | 641 | def test_concurrent_own_executor(self): 642 | data = list(range(1000)) 643 | jssum1 = quickjs.Function("sum", 644 | """ 645 | function sum(data) { 646 | return data.reduce((a, b) => a + b, 0) 647 | } 648 | """, 649 | own_executor=True) 650 | jssum2 = quickjs.Function("sum", 651 | """ 652 | function sum(data) { 653 | return data.reduce((a, b) => a + b, 0) 654 | } 655 | """, 656 | own_executor=True) 657 | 658 | futures = [self.executor.submit(f, data) for _ in range(10) for f in (jssum1, jssum2)] 659 | expected = sum(data) 660 | for future in concurrent.futures.as_completed(futures): 661 | self.assertEqual(future.result(), expected) 662 | 663 | 664 | class QJS(object): 665 | def __init__(self): 666 | self.interp = quickjs.Context() 667 | self.interp.eval('var foo = "bar";') 668 | 669 | 670 | class QuickJSContextInClass(unittest.TestCase): 671 | def test_github_issue_7(self): 672 | # This used to give stack overflow internal error, due to how QuickJS calculates stack 673 | # frames. Passes with the 2021-03-27 release. 674 | qjs = QJS() 675 | self.assertEqual(qjs.interp.eval('2+2'), 4) 676 | --------------------------------------------------------------------------------