diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..1ddbe74c --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,14 @@ +# chore(linting): lint all python files +aae30e864449442cf0b04e94f8a242b1b667de9a + +# chore(linting): lint all JavaScript files +16dc3153b3cb684ca72445ed058babc8f5d97f42 + +# chore(linting): lint all C++ files +58cd4b45777b046f03a63255c1d93e289e1cab5e + +# chore(linting): lint PyBytesProxyHandler.cc +d540ed6e0edfe9538dc726cf587dfb2cc76dde34 + +# chore(linting): lint PyObjectProxyHandler.cc +1d45ea98e42294cce16deec5454725d4de36f59f \ No newline at end of file diff --git a/.github/workflows/test-and-publish.yaml b/.github/workflows/test-and-publish.yaml index 16351cda..ad562623 100644 --- a/.github/workflows/test-and-publish.yaml +++ b/.github/workflows/test-and-publish.yaml @@ -5,63 +5,152 @@ on: branches: - main tags: - - '*' + - 'v*' workflow_call: workflow_dispatch: + inputs: + debug_enabled_os: + type: choice + description: Optionally, choose an OS to run the build with SSH debugging on (https://github.com/fawazahmed0/action-debug) + required: false + options: + - '' + - 'ubuntu-22.04' + - 'macos-15-intel' + - 'macos-14' + - 'windows-2022' + debug_enabled_python: + type: choice + description: Choose a Python version to run the build with SSH debugging on + required: false + options: + - '' + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' + build_type: + type: choice + description: 'Choose the build type to use' + required: false + default: 'Debug' + options: + - 'Debug' + - 'Profile' + - 'Sanitize' + - 'DRelease' + - 'Release' + - 'None' pull_request: env: # don't upgrade outdated brew packages because the process is too slow HOMEBREW_NO_INSTALL_UPGRADE: 1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + # apt-get should run in noninteractive mode + DEBIAN_FRONTEND: noninteractive defaults: run: # run with Git Bash on Windows shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-spidermonkey-unix: strategy: fail-fast: false matrix: - # Use Ubuntu 20.04 / macOS 13 + Python 3.10 to build SpiderMonkey - os: [ 'ubuntu-20.04', 'macos-13', 'm2ci' ] + # Use Ubuntu 22.04 / macOS 15 x86_64 / macOS 14 arm64 + Python 3.10 to build SpiderMonkey + os: [ 'ubuntu-22.04', 'macos-15-intel', 'macos-14', 'ubuntu-22.04-arm' ] # macOS 14 runner exclusively runs on M1 hardwares + # see https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available python_version: [ '3.10' ] runs-on: ${{ matrix.os }} + container: ${{ (startsWith(matrix.os, 'ubuntu') && 'ubuntu:20.04') || null }} # Use the Ubuntu 20.04 container inside Ubuntu 22.04 runner to build steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python_version }} + - uses: actions/checkout@v4 + - name: Read the mozilla-central commit hash to be used + run: echo "MOZCENTRAL_VERSION=$(cat mozcentral.version)" >> $GITHUB_ENV - name: Cache spidermonkey build id: cache-spidermonkey - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./_spidermonkey_install/* - key: spidermonkey115.1.0-${{ runner.os }}-${{ runner.arch }} - lookup-only: true # skip download + key: spidermonkey-${{ env.MOZCENTRAL_VERSION }}-${{ runner.os }}-${{ runner.arch }} + - name: Setup container + if: ${{ startsWith(matrix.os, 'ubuntu') && steps.cache-spidermonkey.outputs.cache-hit != 'true' }} + run: | + apt-get update -y + apt-get install -y sudo libnss3-dev libssl-dev + apt-get install -y curl make git build-essential + apt-get install -y zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev # required for pyenv + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata + echo "AGENT_TOOLSDIRECTORY=/" >> $GITHUB_ENV # do not use the Python installation cached for Ubuntu 22.04 + - name: Setup LLVM + if: ${{ startsWith(matrix.os, 'ubuntu') && steps.cache-spidermonkey.outputs.cache-hit != 'true' }} + run: | + apt-get install -y llvm clang + apt-get install -y lsb-release wget software-properties-common gnupg + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + ./llvm.sh 18 # install LLVM version 18 + update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-18 18 + update-alternatives --install /usr/bin/clang clang /usr/bin/clang-18 18 + update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-18 18 + clang --version + clang++ --version + - name: Setup Python + if: ${{ startsWith(matrix.os, 'ubuntu') && steps.cache-spidermonkey.outputs.cache-hit != 'true' }} + run: | + # Use pyenv to install Python version that is not available via `actions/setup-python` + unset PYENV_ROOT + curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + echo "$HOME/.pyenv/bin" >> $GITHUB_PATH # ~/.bashrc file is not read, so we need to add to GITHUB_PATH manually + echo "$HOME/.pyenv/shims" >> $GITHUB_PATH + echo "PYENV_ROOT=$HOME/.pyenv" >> $GITHUB_ENV + export PATH="$HOME/.pyenv/bin:$PATH" + pyenv install $PYTHON_VERSION + pyenv global $PYTHON_VERSION + env: + PYTHON_VERSION: ${{ matrix.python_version }} + - uses: actions/setup-python@v5 + if: ${{ !startsWith(matrix.os, 'ubuntu') && steps.cache-spidermonkey.outputs.cache-hit != 'true' }} + with: + python-version: ${{ matrix.python_version }} - name: Setup XCode - if: ${{ matrix.os == 'macos-13' && steps.cache-spidermonkey.outputs.cache-hit != 'true' }} - # SpiderMonkey 115 ESR requires XCode SDK version at least 13.3 - # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#installed-sdks - run: sudo xcode-select -switch /Applications/Xcode_14.3.app + if: ${{ matrix.os == 'macos-15-intel' && steps.cache-spidermonkey.outputs.cache-hit != 'true' }} + # Xcode 16.x removed the old linker behaviour (-ld64 or -ld_classic) that SpiderMonkey relies on, so we need to switch to an older version + run: sudo xcode-select -switch /Applications/Xcode_16.0.app - name: Build spidermonkey if: ${{ steps.cache-spidermonkey.outputs.cache-hit != 'true' }} run: ./setup.sh + - name: Upload spidermonkey build as CI artifacts + uses: actions/upload-artifact@v4 + with: + name: spidermonkey-${{ env.MOZCENTRAL_VERSION }}-${{ runner.os }}-${{ runner.arch }} + path: ./_spidermonkey_install/ build-spidermonkey-win: - runs-on: windows-2019 + runs-on: windows-2022 + # SpiderMonkey requires Visual Studio 2022 or newer. + # The Windows 2019 runner only has Visual Studio Enterprise 2019 installed. steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Read the mozilla-central commit hash to be used + run: echo "MOZCENTRAL_VERSION=$(cat mozcentral.version)" >> $GITHUB_ENV - name: Cache spidermonkey build id: cache-spidermonkey - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./_spidermonkey_install/* - key: spidermonkey115.1.0-${{ runner.os }}-${{ runner.arch }} - lookup-only: true # skip download + key: spidermonkey-${{ env.MOZCENTRAL_VERSION }}-${{ runner.os }}-${{ runner.arch }} - name: Install dependencies if: ${{ steps.cache-spidermonkey.outputs.cache-hit != 'true' }} shell: powershell @@ -69,7 +158,7 @@ jobs: # Already installed in Github Actions runner # choco install -y cmake --installargs 'ADD_CMAKE_TO_PATH=System' # add CMake to system PATH # choco install -y llvm gnuwin32-m4 - choco install -y wget make + choco install -y wget make unzip - name: Install MozillaBuild if: ${{ steps.cache-spidermonkey.outputs.cache-hit != 'true' }} run: | @@ -77,34 +166,66 @@ jobs: powershell -command 'Start-Process -Wait -FilePath "./MozillaBuildSetup-Latest.exe" -ArgumentList "/S"' - name: Build spidermonkey in MozillaBuild environment if: ${{ steps.cache-spidermonkey.outputs.cache-hit != 'true' }} + env: + # Preserve MozillaBuild v4.0.x behaviour + # see https://groups.google.com/u/1/a/mozilla.org/g/dev-platform/c/hF51Q3j6ca8 + USE_MINTTY: 0 run: /c/mozilla-build/start-shell.bat -use-full-path -here ./setup.sh + - name: Upload spidermonkey build as CI artifacts + uses: actions/upload-artifact@v4 + with: + name: spidermonkey-${{ env.MOZCENTRAL_VERSION }}-${{ runner.os }}-${{ runner.arch }} + path: ./_spidermonkey_install/ build-and-test: needs: [build-spidermonkey-unix, build-spidermonkey-win] strategy: fail-fast: false matrix: - # The lowest supported version is Ubuntu 20.04 + Python 3.8 or macOS 12 + Python 3.9 - os: [ 'ubuntu-20.04', 'macos-12', 'windows-2019', 'm2ci' ] - python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12-dev' ] - exclude: - # macOS 12 comes with Python 3.9 by default, so we drop ci support for Python 3.8 on macOS - # FIXME: We can't build on macOS 11 for now because our prebuilt `uncrustify` binary requires macOS 12 - - os: 'macos-12' - python_version: '3.8' - # actions/setup-python: The version '3.8'/'3.9' with architecture 'arm64' was not found for macOS. - # see https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - - os: 'm2ci' - python_version: '3.8' - - os: 'm2ci' - python_version: '3.9' + os: [ 'ubuntu-22.04', 'macos-15-intel', 'macos-14', 'windows-2022', 'ubuntu-22.04-arm' ] + python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] runs-on: ${{ matrix.os }} + container: ${{ (startsWith(matrix.os, 'ubuntu') && 'ubuntu:20.04') || null }} steps: - - uses: actions/checkout@v3 + - name: Setup container + if: ${{ startsWith(matrix.os, 'ubuntu') }} + run: | + apt-get update -y + apt-get install -y sudo libnss3-dev libssl-dev + apt-get install -y curl zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev # required for pyenv + apt-get install -y git # required for `actions/checkout` + apt-get install -y nodejs npm # required for pminit to build + apt-get install -y build-essential + apt-get install -y strace # required to run JS tests + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata # tzdata may ask for user interaction if not explicitly installed here + echo "AGENT_TOOLSDIRECTORY=/" >> $GITHUB_ENV # do not use the Python installation cached for Ubuntu 22.04 + git config --global --add safe.directory '*' # silence "git failed because of dubious ownership" + + # CMake 3.25 or higher is required + apt-get install -y ca-certificates gpg wget + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ focal main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null + apt-get update -y && apt-get install -y cmake + - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 # fetch all history for all branches and tags # poetry-dynamic-versioning needs git tags to produce the correct version number - - uses: actions/setup-python@v4 + - name: Setup Python + if: ${{ startsWith(matrix.os, 'ubuntu') }} + run: | + # Use pyenv to install Python version that is not available via `actions/setup-python` + unset PYENV_ROOT + curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + echo "$HOME/.pyenv/bin" >> $GITHUB_PATH # ~/.bashrc file is not read, so we need to add to GITHUB_PATH manually + echo "$HOME/.pyenv/shims" >> $GITHUB_PATH + echo "PYENV_ROOT=$HOME/.pyenv" >> $GITHUB_ENV + export PATH="$HOME/.pyenv/bin:$PATH" + pyenv install $PYTHON_VERSION + pyenv global $PYTHON_VERSION + env: + PYTHON_VERSION: ${{ matrix.python_version }} + - uses: actions/setup-python@v5 + if: ${{ !startsWith(matrix.os, 'ubuntu') }} with: python-version: ${{ matrix.python_version }} - name: Setup Poetry @@ -116,28 +237,43 @@ jobs: echo "Installing Dependencies" if [[ "$OSTYPE" == "linux-gnu"* ]]; then # Linux sudo apt-get update -y - sudo apt-get install -y cmake graphviz llvm - # Install Doxygen - # the newest version in Ubuntu 20.04 repository is 1.8.17, but we need Doxygen 1.9 series - wget -c -q https://www.doxygen.nl/files/doxygen-1.9.7.linux.bin.tar.gz - tar xf doxygen-1.9.7.linux.bin.tar.gz - cd doxygen-1.9.7 && sudo make install && cd - - rm -rf doxygen-1.9.7 doxygen-1.9.7.linux.bin.tar.gz + sudo apt-get install -y cmake llvm elif [[ "$OSTYPE" == "darwin"* ]]; then # macOS brew update || true # allow failure - brew install cmake doxygen graphviz pkg-config wget coreutils # `coreutils` installs the `realpath` command + brew install cmake pkg-config wget unzip coreutils # `coreutils` installs the `realpath` command fi echo "Installing python deps" poetry self add "poetry-dynamic-versioning[plugin]" - poetry env use python3 # use the correct Python version we've set up + echo "Use the correct Python version we've set up" + poetry env use python$PYTHON_VERSION || poetry env use python3 # use the correct Python version we've set up poetry install --no-root --only=dev echo "Installed Dependencies" + env: + PYTHON_VERSION: ${{ matrix.python_version }} + - name: Build Docs # only build docs once + if: ${{ matrix.os == 'ubuntu-22.04' && matrix.python_version == '3.11' }} + run: | + sudo apt-get install -y graphviz + # the newest version in Ubuntu 20.04 repository is 1.8.17, but we need Doxygen 1.9 series + wget -c -q https://www.doxygen.nl/files/doxygen-1.9.7.linux.bin.tar.gz + tar xf doxygen-1.9.7.linux.bin.tar.gz + cd doxygen-1.9.7 && sudo make install && cd - + rm -rf doxygen-1.9.7 doxygen-1.9.7.linux.bin.tar.gz + BUILD_DOCS=1 BUILD_TYPE=None poetry install + - name: Upload Doxygen-generated docs as CI artifacts + if: ${{ matrix.os == 'ubuntu-22.04' && matrix.python_version == '3.11' }} + uses: actions/upload-artifact@v4 + with: + name: docs-${{ github.run_id }}-${{ github.sha }} + path: ./build/docs/html/ + - name: Read the mozilla-central commit hash to be used + run: echo "MOZCENTRAL_VERSION=$(cat mozcentral.version)" >> $GITHUB_ENV - name: Use cached spidermonkey build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./_spidermonkey_install/* - key: spidermonkey115.1.0-${{ runner.os }}-${{ runner.arch }} + key: spidermonkey-${{ env.MOZCENTRAL_VERSION }}-${{ runner.os }}-${{ runner.arch }} fail-on-cache-miss: true # SpiderMonkey is expected to be cached in its dedicated job - name: Build pminit run: | @@ -149,33 +285,46 @@ jobs: - name: Build wheel run: | echo $(poetry run python --version) - poetry build --format=wheel + WORKFLOW_BUILD_TYPE=${{ inputs.build_type }} + BUILD_TYPE=${WORKFLOW_BUILD_TYPE:-"Debug"} poetry build --format=wheel ls -lah ./dist/ + - name: Make the wheels we build also support lower versions of macOS + if: ${{ matrix.os == 'macos-15-intel' || matrix.os == 'macos-14' }} + # Change the platform tag part of the wheel filename to `macosx_11_0_xxx` (means to support macOS 11.0 and above) + # See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-format + # A wheel package file will only be selected by pip to install if the platform tag satisfies, regardless of whether the binary compatibility actually is. + # Otherwise, pip would fallback to compile from the source distribution. + run: | + cd ./dist/ + for file in *.whl; do + mv "$file" "$(echo "$file" | sed -E 's/macosx_[0-9]+_[0-9]+/macosx_11_0/')"; + done - name: Upload wheel as CI artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheel-${{ github.run_id }}-${{ github.sha }} + name: wheel-${{ github.run_id }}-${{ github.sha }}-${{ runner.os }}_${{ runner.arch }}_Python${{ matrix.python_version }} path: ./dist/ - - name: Upload Doxygen-generated docs as CI artifacts - uses: actions/upload-artifact@v3 - if: ${{ matrix.os == 'ubuntu-20.04' && matrix.python_version == '3.11' }} # making sure we only upload once - with: - name: docs-${{ github.run_id }}-${{ github.sha }} - path: ./build/docs/html/ - name: Run Python tests (pytest) run: | - poetry run python -m pip install --force-reinstall --verbose ./dist/* + WORKFLOW_BUILD_TYPE=${{ inputs.build_type }} + BUILD_TYPE=${WORKFLOW_BUILD_TYPE:-"Debug"} poetry run python -m pip install --force-reinstall --verbose ./dist/* poetry run python -m pytest tests/python - name: Run JS tests (peter-jr) + if: ${{ (success() || failure()) }} run: | poetry run bash ./peter-jr ./tests/js/ + - name: SSH debug session + if: ${{ (success() || failure()) && github.event_name == 'workflow_dispatch' && inputs.debug_enabled_os == matrix.os && inputs.debug_enabled_python == matrix.python_version}} + uses: fawazahmed0/action-debug@main + with: + credentials: "admin:admin" sdist: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' - name: Setup Poetry @@ -188,24 +337,25 @@ jobs: poetry build --format=sdist ls -lah ./dist/ - name: Upload sdist as CI artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheel-${{ github.run_id }}-${{ github.sha }} + name: wheel-${{ github.run_id }}-${{ github.sha }}-sdist path: ./dist/ publish: needs: [build-and-test, sdist] - runs-on: ubuntu-20.04 - if: ${{ success() && github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} + runs-on: ubuntu-22.04 + if: ${{ success() && github.event_name == 'push' && github.ref_type == 'tag' }} steps: # no need to checkout - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' - run: pip install twine - name: Download wheels built - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: wheel-${{ github.run_id }}-${{ github.sha }} + pattern: wheel-${{ github.run_id }}-${{ github.sha }}-* + merge-multiple: true path: ./dist/ - run: ls -lah ./dist/ - name: Publish package @@ -217,8 +367,8 @@ jobs: # Implement a very basic Python package repository (https://peps.python.org/pep-0503/) # and deploy the static files to GitHub Pages needs: [build-and-test, sdist] - runs-on: ubuntu-20.04 - if: ${{ (success() || failure()) && github.ref_name == 'main' }} # publish nightly builds regardless of tests failure + runs-on: ubuntu-22.04 + if: ${{ (success() || failure()) && (github.ref_name == 'main' || github.ref_type == 'tag') }} # publish nightly builds regardless of tests failure permissions: # grant GITHUB_TOKEN the permissions required to make a Pages deployment pages: write id-token: write @@ -228,12 +378,13 @@ jobs: steps: # don't checkout - name: Download wheels built - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: wheel-${{ github.run_id }}-${{ github.sha }} + pattern: wheel-${{ github.run_id }}-${{ github.sha }}-* + merge-multiple: true path: ./dist/ - name: Download docs html generated by Doxygen - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: docs-${{ github.run_id }}-${{ github.sha }} path: ./docs/ @@ -267,9 +418,52 @@ jobs: html+="
  • pminit
  • " html+="" echo "$html" > ./index.html - - uses: actions/upload-pages-artifact@v1 + - uses: actions/upload-pages-artifact@v3 with: path: ./ - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 + publish-archive: + # Publish to ⊇istributive's archive server (https://archive.distributed.computer/releases/pythonmonkey/) + needs: [build-and-test, sdist] + runs-on: ubuntu-22.04 + if: ${{ (success() || failure()) && (github.ref_name == 'main' || github.ref_type == 'tag') }} + environment: + name: archive + url: https://archive.distributed.computer/releases/pythonmonkey/${{ steps.get_path.outputs.ARCHIVE_PATH }} + steps: + # no need to checkout + - name: Download wheels built + uses: actions/download-artifact@v4 + with: + pattern: wheel-${{ github.run_id }}-${{ github.sha }}-* + merge-multiple: true + path: ./ + - name: Download docs html generated by Doxygen + uses: actions/download-artifact@v4 + with: + name: docs-${{ github.run_id }}-${{ github.sha }} + path: ./docs/ + - name: Get the pythonmonkey/pminit version number + run: | + file=$(ls ./pminit*.tar.gz | head -1) + pm_version=$(basename "${file%.tar.gz}" | cut -d- -f2) # match /pminit-([^-]+).tar.gz/ + echo "PM_VERSION=$pm_version" >> $GITHUB_ENV + - name: Get the archive type (nightly or releases) and path + id: get_path + run: | + path="$ARCHIVE_TYPE/$PM_VERSION/" + echo "$path" + echo "ARCHIVE_PATH=$path" >> $GITHUB_OUTPUT + env: + ARCHIVE_TYPE: ${{ (github.ref_type == 'tag' && 'releases') || 'nightly' }} + - name: SCP to the archive server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.ARCHIVE_HOST }} + username: pythonmonkey + key: ${{ secrets.ARCHIVE_KEY }} + source: ./* + target: archive/${{ steps.get_path.outputs.ARCHIVE_PATH }} + overwrite: true diff --git a/.github/workflows/update-mozcentral-version.yaml b/.github/workflows/update-mozcentral-version.yaml new file mode 100644 index 00000000..ec3c6a41 --- /dev/null +++ b/.github/workflows/update-mozcentral-version.yaml @@ -0,0 +1,43 @@ +name: 'Create pull requests to update mozilla-central version to the latest' + +on: + # schedule: + # - cron: "00 14 */100,1-7 * 1" # run on the first Monday of each month at 14:00 UTC (10:00 Eastern Daylight Time) + # See https://blog.healthchecks.io/2022/09/schedule-cron-job-the-funky-way/ + workflow_call: + workflow_dispatch: # or you can run it manually + +defaults: + run: + shell: bash + +jobs: + update: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Check Version + # Check the latest changes on mozilla-central via the Mercurial pushlog HTTP API + # See https://mozilla-version-control-tools.readthedocs.io/en/latest/hgmo/pushlog.html#hgweb-commands + run: | + COMMIT_HASH=$( + curl -L -s "https://hg.mozilla.org/mozilla-central/json-pushes?tipsonly=1&version=2" |\ + jq --join-output '(.lastpushid | tostring) as $pushid | empty, .pushes[$pushid].changesets[0]' + ) + echo "MOZCENTRAL_VERSION=$COMMIT_HASH" >> $GITHUB_ENV + echo "MOZCENTRAL_VERSION_SHORT=${COMMIT_HASH:0:7}" >> $GITHUB_ENV + - name: Update `mozcentral.version` File + run: echo -n $MOZCENTRAL_VERSION > mozcentral.version + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + add-paths: mozcentral.version + commit-message: | + chore(deps): upgrade SpiderMonkey to `${{ env.MOZCENTRAL_VERSION }}` + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + branch: chore/upgrade-spidermonkey-to-${{ env.MOZCENTRAL_VERSION_SHORT }} + title: Upgrade SpiderMonkey to mozilla-central commit `${{ env.MOZCENTRAL_VERSION }}` + body: | + Changeset: https://hg.mozilla.org/mozilla-central/rev/${{ env.MOZCENTRAL_VERSION }} + labels: dependencies + assignees: Xmader diff --git a/.gitignore b/.gitignore index 6cec5d71..8f796a40 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,17 @@ lib/* .pytest_cache .DS_Store firefox-*.tar.xz +firefox-*.zip firefox-*/ +mozilla-central-* __pycache__ Testing/Temporary -_spidermonkey_install +_spidermonkey_install* +uncrustify-*.tar.gz +uncrustify-*/ +uncrustify +uncrustify.exe +*.uncrustify __pycache__/* dist *.so diff --git a/.vscode/launch.json b/.vscode/launch.json index 5f5e6ea3..6a4a53c7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ "run" ], }, - "preLaunchTask": "Build", + "preLaunchTask": "Fast build", "cwd": "${fileDirname}", "environment": [], "externalConsole": false, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 30f2d436..d98405b5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -26,6 +26,27 @@ "isDefault": true } }, + { + "label": "Fast build", + "type": "process", + "command": "poetry", + "args": [ + "run", + "python", + "./build.py", + ], + "problemMatcher": [ + "$gcc" + ], + "options": { + "env": { + "BUILD_TYPE": "Debug" + } + }, + "group": { + "kind": "build", + } + }, // // Test // diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cbe02d0..1577c299 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,23 +17,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -# Add an external; appends to `PYTHONMONKEY_EXTERNAL_FILES` in the parent scope. -function(pythonmonkey_add_external PYTHONMONKEY_EXTERNAL) - add_subdirectory("cmake/externals/${PYTHONMONKEY_EXTERNAL}") - set(PYTHONMONKEY_EXTERNAL_FILE "cmake/externals/${PYTHONMONKEY_EXTERNAL}/CMakeLists.txt") - source_group( - TREE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/externals/${PYTHONMONKEY_EXTERNAL}" - PREFIX "Externals\\${PYTHONMONKEY_EXTERNAL}" - FILES "${PYTHONMONKEY_EXTERNAL_FILE}" - ) - list(APPEND PYTHONMONKEY_EXTERNAL_FILES "${PYTHONMONKEY_EXTERNAL_FILE}") - - set(PYTHONMONKEY_EXTERNAL_FILES ${PYTHONMONKEY_EXTERNAL_FILES} PARENT_SCOPE) -endfunction() - -file (GLOB SOURCE_FILES "src/*.cc" "src/internalBinding/*.cc") # Find all C++ files in the src directory -file (GLOB HEADER_FILES "include/*.hh") # Find all header files in the include directory -file (GLOB PYTHON_FILES "python/*.cc" "python/*.hh") # Find all the python bindings in the python directory +file (GLOB_RECURSE HEADER_FILES "include/*.hh") # Find all header files in the include directory and below +file (GLOB_RECURSE SOURCE_FILES "src/*.cc") # Find all C++ files in the src directory and below + include_directories(${CMAKE_CURRENT_LIST_DIR}) @@ -43,47 +29,97 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) ### Code block from: https://cliutils.gitlab.io/modern-cmake/chapters/projects/submodule.html include(FetchContent) - SET(COMPILE_FLAGS "-ggdb -Ofast -fno-rtti") # optimize but also emit debug symbols - SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILE_FLAGS}" ) - - set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) - if(APPLE) - find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) - find_package(SpiderMonkey REQUIRED) - set(PYTHON_MAJOR $ENV{Python_VERSION_MAJOR}) - set(PYTHON_MINOR $ENV{Python_VERSION_MINOR}) - set(PYTHONLIBS_VERSION_STRING ${Python_VERSION}) - set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS}) - set(PYTHON_LIBRARIES ${Python_LIBRARIES}) - message("Apple - Using Python:${Python_VERSION_MAJOR} - Libraries:${PYTHON_LIBRARIES} - IncludeDirs: ${PYTHON_INCLUDE_DIR}") - elseif(UNIX) - find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) - set(Python_FIND_VIRTUALENV FIRST) # (require cmake >= v3.15 and this is the default) use the Python version configured by pyenv if available - set(PYTHON_LIBRARIES ${Python_LIBRARIES}) - set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS}) - find_package(SpiderMonkey REQUIRED) - elseif(WIN32) - find_package(PythonInterp 3.8 REQUIRED) - find_package(PythonLibs 3.8 REQUIRED) - find_package(SpiderMonkey REQUIRED) - set(PYTHONLIBS_VERSION_STRING $ENV{PY_VERSION}) + if (WIN32) + SET(COMPILE_FLAGS "/GR- /W0") + + SET(OPTIMIZED "/O2") + SET(UNOPTIMIZED "/Od") + SET(KEEP_SYMBOLS "/DEBUG:FULL") + SET(STRIP_SYMBOLS "/DEBUG:NONE") + SET(PROFILE "/PROFILE") + SET(ADDRESS_SANITIZE "/fsanitize=address /Oy-") + else() + SET(COMPILE_FLAGS "-fno-rtti -Wno-invalid-offsetof") + + SET(OPTIMIZED "-Ofast -DNDEBUG") + SET(UNOPTIMIZED "-O0") + SET(KEEP_SYMBOLS "-ggdb") + SET(STRIP_SYMBOLS "-s") + SET(PROFILE "-pg") + SET(ADDRESS_SANITIZE "-fsanitize=address -fno-omit-frame-pointer") + endif() + SET(PROFILE_FLAGS "${UNOPTIMIZED} ${KEEP_SYMBOLS} ${PROFILE}") + SET(SANITIZE_FLAGS "${UNOPTIMIZED} ${KEEP_SYMBOLS} ${ADDRESS_SANITIZE}") + SET(DEBUG_FLAGS "${UNOPTIMIZED} ${KEEP_SYMBOLS}") + SET(DRELEASE_FLAGS "${OPTIMIZED} ${KEEP_SYMBOLS}") + SET(RELEASE_FLAGS "${OPTIMIZED} ${STRIP_SYMBOLS}") + + if(GENERATOR_IS_MULTI_CONFIG) + set(CMAKE_CONFIGURATION_TYPES "Profile;Sanitize;Debug;DRelease;Release;None" CACHE STRING "" FORCE) + string(APPEND COMPILE_FLAGS "$<$:${PROFILE_FLAGS}> $<$:${SANITIZE_FLAGS}> $<$:${DEBUG_FLAGS}> $<$:${DRELEASE_FLAGS}> $<$:${RELEASE_FLAGS}>") + else() + set_property(CACHE PM_BUILD_TYPE PROPERTY HELPSTRING "Choose the type of build") + set_property(CACHE PM_BUILD_TYPE PROPERTY STRINGS "Profile;Sanitize;Debug;DRelease;Release;None") + if(PM_BUILD_TYPE STREQUAL "Profile") + list(APPEND COMPILE_FLAGS "${PROFILE_FLAGS}") + elseif(PM_BUILD_TYPE STREQUAL "Sanitize") + list(APPEND COMPILE_FLAGS "${SANITIZE_FLAGS}") + elseif(PM_BUILD_TYPE STREQUAL "Debug") + list(APPEND COMPILE_FLAGS "${DEBUG_FLAGS}") + elseif(PM_BUILD_TYPE STREQUAL "DRelease") + list(APPEND COMPILE_FLAGS "${DRELEASE_FLAGS}") + elseif(PM_BUILD_TYPE STREQUAL "None") + message("PM_BUILD_TYPE is None. Not compiling.") + else() #Release build + message("PM_BUILD_TYPE not detected or invalid value, defaulting to Release build.") + set(PM_BUILD_TYPE Release CACHE STRING "" FORCE) + list(APPEND COMPILE_FLAGS "${RELEASE_FLAGS}") + endif() + message("PythonMonkey build type is: ${PM_BUILD_TYPE}") + list(JOIN COMPILE_FLAGS " " COMPILE_FLAGS) endif() - include_directories(${PYTHON_INCLUDE_DIRS}) - include_directories(${SPIDERMONKEY_INCLUDE_DIR}) + + if(NOT PM_BUILD_TYPE STREQUAL "None") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILE_FLAGS}") + + set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) + if(APPLE) + find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) + find_package(SpiderMonkey REQUIRED) + set(PYTHON_MAJOR $ENV{Python_VERSION_MAJOR}) + set(PYTHON_MINOR $ENV{Python_VERSION_MINOR}) + set(PYTHONLIBS_VERSION_STRING ${Python_VERSION}) + set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS}) + set(PYTHON_LIBRARIES ${Python_LIBRARIES}) + elseif(UNIX) + find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) + set(Python_FIND_VIRTUALENV FIRST) # (require cmake >= v3.15 and this is the default) use the Python version configured by pyenv if available + set(PYTHON_LIBRARIES ${Python_LIBRARIES}) + set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS}) + find_package(SpiderMonkey REQUIRED) + elseif(WIN32) + find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) + set(Python_FIND_VIRTUALENV FIRST) # (require cmake >= v3.15 and this is the default) use the Python version configured by pyenv if available + set(PYTHON_LIBRARIES ${Python_LIBRARIES}) + set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS}) + find_package(SpiderMonkey REQUIRED) + endif() + message("${CMAKE_SYSTEM_NAME} - Using Python:${Python_VERSION} - Libraries:${Python_LIBRARIES} - IncludeDirs: ${Python_INCLUDE_DIRS}") + include_directories(${Python_INCLUDE_DIRS}) + include_directories(${SPIDERMONKEY_INCLUDE_DIR}) + # Add compiled folder directories + add_subdirectory(src) + endif(NOT PM_BUILD_TYPE STREQUAL "None") # Add doxygen if this is the main app - find_package(Doxygen) - if(Doxygen_FOUND) + option(BUILD_DOCS "Build documentation" OFF) + if(BUILD_DOCS) + find_package(Doxygen) + if(Doxygen_FOUND) add_subdirectory(cmake/docs) - else() + else() message(STATUS "Doxygen not found. Not building docs.") + endif() endif() endif() - -# Add compiled folder directories -add_subdirectory(src) - -pythonmonkey_add_external("uncrustify") -pythonmonkey_add_external("autopep8") -add_subdirectory(cmake/format) diff --git a/LICENSE b/LICENSE index 6a6eff92..085d2547 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Distributive Corp. +Copyright (c) 2023-2024 Distributive Corp. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +19,272 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------- + +We have adapted some parameter parsing and algorithmic logic for our Python +subclasses from cPython + + + +Python Software Foundation License Version 2 + +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 +Python Software Foundation; All Rights Reserved + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + +------------------------------------------------------------------------------- + +We have adapted JS facilities classes such as Console.js from Node + + +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +------------------------------------------------------------------------------- + +We have adapted some parameter parsing and algorithmic logic for our Javascript Proxies from +Mozilla's Spidermonkey + + + +Mozilla Public License +Version 2.0 +1. Definitions +1.1. “Contributor” +means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. + +1.2. “Contributor Version” +means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” +means Covered Software of a particular Contributor. + +1.4. “Covered Software” +means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. + +1.5. “Incompatible With Secondary Licenses” +means + +that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or + +that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. + +1.6. “Executable Form” +means any form of the work other than Source Code Form. + +1.7. “Larger Work” +means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. + +1.8. “License” +means this document. + +1.9. “Licensable” +means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. + +1.10. “Modifications” +means any of the following: + +any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or + +any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor +means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. + +1.12. “Secondary License” +means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” +means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) +means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. + +2. License Grants and Conditions +2.1. Grants +Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: + +under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and + +under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. + +2.2. Effective Date +The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. + +2.3. Limitations on Grant Scope +The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: + +for any code that a Contributor has removed from Covered Software; or + +for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or + +under Patent Claims infringed by Covered Software in the absence of its Contributions. + +This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). + +2.4. Subsequent Licenses +No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). + +2.5. Representation +Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use +This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. + +3. Responsibilities +3.1. Distribution of Source Form +All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form +If You distribute Covered Software in Executable Form then: + +such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and + +You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work +You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). + +3.4. Notices +You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms +You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. + +5. Termination +5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. + +6. Disclaimer of Warranty +Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. + +7. Limitation of Liability +Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation +Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous +This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. + +10. Versions of the License +10.1. New Versions +Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. + +10.2. Effect of New Versions +You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. + +10.3. Modified Versions +If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses +If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice +This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. + diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5eece9f1 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# @file Makefile +# Not part of the PythonMonkey build - just workflow helper for Wes. +# @author Wes Garland, wes@distributive.network +# @date March 2024 +# + +BUILD = Debug # (case-insensitive) Release, DRelease, Debug, Sanitize, Profile, or None +DOCS = false +VERBOSE = true +PYTHON = python3 +RUN = poetry run + +OS_NAME := $(shell uname -s) + +ifeq ($(OS_NAME),Linux) +CPU_COUNT = $(shell cat /proc/cpuinfo | grep -c processor) +MAX_JOBS = 10 +CPUS := $(shell test $(CPU_COUNT) -lt $(MAX_JOBS) && echo $(CPU_COUNT) || echo $(MAX_JOBS)) +PYTHON_BUILD_ENV += CPUS=$(CPUS) +endif + +ifeq ($(BUILD),Profile) +PYTHON_BUILD_ENV += BUILD_TYPE=Profile +else ifeq ($(BUILD),Sanitize) +PYTHON_BUILD_ENV += BUILD_TYPE=Sanitize +else ifeq ($(BUILD),Debug) +PYTHON_BUILD_ENV += BUILD_TYPE=Debug +else ifeq ($(BUILD),DRelease) +PYTHON_BUILD_ENV += BUILD_TYPE=DRelease +else ifeq ($(BUILD), None) +PYTHON_BUILD_ENV += BUILD_TYPE=None +else # Release build +PYTHON_BUILD_ENV += BUILD_TYPE=Release +endif + +ifeq ($(DOCS),true) +PYTHON_BUILD_ENV += BUILD_DOCS=1 +endif + +ifeq ($(VERBOSE),true) +PYTHON_BUILD_ENV += VERBOSE=1 +endif + +.PHONY: build test all clean debug +build: + $(PYTHON_BUILD_ENV) $(PYTHON) ./build.py + +test: + $(RUN) ./peter-jr tests + $(RUN) pytest tests/python + +all: build test + +clean: + rm -rf build/src/CMakeFiles/pythonmonkey.dir + rm -f build/src/pythonmonkey.so + rm -f python/pythonmonkey/pythonmonkey.so + +debug: + @echo JOBS=$(JOBS) + @echo CPU_COUNT=$(CPU_COUNT) + @echo OS_NAME=$(OS_NAME) \ No newline at end of file diff --git a/README.md b/README.md index ffa1a7cd..31e6efac 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # PythonMonkey -![Testing Suite](https://github.com/Kings-Distributed-Systems/PythonMonkey/actions/workflows/tests.yaml/badge.svg) +[![Test and Publish Suite](https://github.com/Distributive-Network/PythonMonkey/actions/workflows/test-and-publish.yaml/badge.svg)](https://github.com/Distributive-Network/PythonMonkey/actions/workflows/test-and-publish.yaml) ## About -[PythonMonkey](https://pythonmonkey.io) is a Mozilla [SpiderMonkey](https://firefox-source-docs.mozilla.org/js/index.html) JavaScript engine embedded into the Python VM, -using the Python engine to provide the JS host environment. +[PythonMonkey](https://pythonmonkey.io) is a Mozilla [SpiderMonkey](https://firefox-source-docs.mozilla.org/js/index.html) JavaScript engine embedded into the Python Runtime, +using the Python engine to provide the Javascript host environment. + +We feature JavaScript Array and Object methods implemented on Python List and Dictionaries using the cPython C API, and the inverse using the Mozilla Firefox Spidermonkey JavaScript C++ API. + +This project has reached MVP as of September 2024. It is under maintenance by [Distributive](https://distributive.network/). -This product is in an early stage, approximately 80% to MVP as of July 2023. It is under active development by [Distributive](https://distributive.network/). External contributions and feedback are welcome and encouraged. ### tl;dr @@ -24,15 +27,17 @@ js_eval("console.log")('hello, world') - Make writing code in either JS or Python a developer preference - Use JavaScript libraries from Python - Use Python libraries from JavaScript -- Same process runs both JS and Python VMs - no serialization, pipes, etc +- The same process runs both JavaScript and Python VirtualMachines - no serialization, pipes, etc +- Python Lists and Dicts behave as Javacript Arrays and Objects, and vice-versa, fully adapting to the given context. ### Data Interchange -- Strings share immutable backing stores whenever possible (when allocating engine choses UCS-2 or Latin-1 internal string representation) to keep memory consumption under control, and to make it possible to move very large strings between JS and Python library code without memory-copy overhead. +- Strings share immutable backing stores whenever possible (when allocating engine choses UCS-2 or Latin-1 internal string representation) to keep memory consumption under control, and to make it possible to move very large strings between JavaScript and Python library code without memory-copy overhead. - TypedArrays share mutable backing stores. -- JS objects are represented by Python dicts +- JS objects are represented by Python dicts through a Dict subclass for optimal compatibility. Similarly for JS arrays and Python lists. - JS Date objects are represented by Python datetime.datetime objects - Intrinsics (boolean, number, null, undefined) are passed by value - JS Functions are automatically wrapped so that they behave like Python functions, and vice-versa +- Python Lists are represented by JS true arrays and support all Array methods through a JS API Proxy. Similarly for Python Dicts and JS objects. ### Roadmap - [done] JS instrinsics coerce to Python intrinsics @@ -48,12 +53,13 @@ js_eval("console.log")('hello, world') - [done] Python `require` function, returns a coerced dict of module exports - [done] Python functions coerce to JS function wrappers - [done] CommonJS module system .py loader, loads Python modules for use by JS -- JS object->Python dict coercion supports inherited-property lookup (via __getattribute__?) - [done] Python host environment supplies event loop, including EventEmitter, setTimeout, etc. -- Python host environment supplies XMLHttpRequest (other project?) -- Python host environment supplies basic subsets of NodeJS's fs, path, process, etc, modules; as-needed by dcp-client (other project?) +- [done] Python host environment supplies XMLHttpRequest - [done] Python TypedArrays coerce to JS TypeArrays - [done] JS TypedArrays coerce to Python TypeArrays +- [done] Python lists coerce to JS Arrays +- [done] JS arrays coerce to Python lists +- [done] PythonMonkey can run the dcp-client npm package from Distributive. ## Build Instructions @@ -62,22 +68,29 @@ Read this if you want to build a local version. 1. You will need the following installed (which can be done automatically by running `./setup.sh`): - bash - cmake - - Doxygen 1.9 series - - graphviz + - Doxygen 1.9 series (if you want to build the docs) + - graphviz (if you want to build the docs) - llvm - rust - python3.8 or later with header files (python3-dev) - - spidermonkey 115.1.0 or later + - spidermonkey latest from mozilla-central - npm (nodejs) - [Poetry](https://python-poetry.org/docs/#installation) - [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning) -2. Run `poetry install`. This command automatically compiles the project and installs the project as well as dependencies into the poetry virtualenv. +2. Run `poetry install`. This command automatically compiles the project and installs the project as well as dependencies into the poetry virtualenv. If you would like to build the docs, set the `BUILD_DOCS` environment variable, like so: `BUILD_DOCS=1 poetry install`. +PythonMonkey supports multiple build types, which you can build by setting the `BUILD_TYPE` environment variable, like so: `BUILD_TYPE=Debug poetry install`. The build types are (case-insensitive): +- `Release`: stripped symbols, maximum optimizations (default) +- `DRelease`: same as `Release`, except symbols are not stripped +- `Debug`: minimal optimizations +- `Sanitize`: same as `Debug`, except with [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) enabled +- `Profile`: same as `Debug`, except profiling is enabled +- `None`: don't compile (useful if you only want to build the docs) If you are using VSCode, you can just press Ctrl + Shift + B to [run build task](https://code.visualstudio.com/docs/editor/tasks#_custom-tasks) - We have [the `tasks.json` file configured for you](.vscode/tasks.json). ## Running tests -1. Compile the project +1. Compile the project 2. Install development dependencies: `poetry install --no-root --only=dev` 3. From the root directory, run `poetry run pytest ./tests/python` 4. From the root directory, run `poetry run bash ./peter-jr ./tests/js/` @@ -123,10 +136,19 @@ $ poetry build --format=wheel ``` and install them by `pip install ./dist/*`. +## Uninstallation + +Installing `pythonmonkey` will also install the `pminit` package as a dependency. However, `pip uninstall`ing a package won't automatically remove its dependencies. +If you want to cleanly remove `pythonmonkey` from your system, do the following: + +```bash +$ pip uninstall pythonmonkey pminit +``` + ## Debugging Steps 1. [build the project locally](#build-instructions) -2. To use gdb, run `poetry run gdb python`. +2. To use gdb, run `poetry run gdb python`. See [Python Wiki: DebuggingWithGdb](https://wiki.python.org/moin/DebuggingWithGdb) If you are using VSCode, it's more convenient to debug in [VSCode's built-in debugger](https://code.visualstudio.com/docs/editor/debugging). Simply press F5 on an open Python file in the editor to start debugging - We have [the `launch.json` file configured for you](https://github.com/Distributive-Network/PythonMonkey/blob/main/.vscode/launch.json). @@ -137,18 +159,43 @@ If you are using VSCode, it's more convenient to debug in [VSCode's built-in deb * https://github.com/Distributive-Network/PythonMonkey-examples * https://github.com/Distributive-Network/PythonMonkey-Crypto-JS-Fullstack-Example -## API -These methods are exported from the pythonmonkey module. - -* eval(code, evalOpts) -* isCompilableUnit(code) -* collect() -* bigint(int) -* `SpiderMonkeyError` -* `JSObjectProxy` -* `null` - -See definitions in [python/pythonmonkey/pythonmonkey.pyi](https://github.com/Distributive-Network/PythonMonkey/blob/main/python/pythonmonkey/pythonmonkey.pyi). +## Official API +These methods are exported from the pythonmonkey module. See definitions in [python/pythonmonkey/pythonmonkey.pyi](https://github.com/Distributive-Network/PythonMonkey/blob/main/python/pythonmonkey/pythonmonkey.pyi). + +### eval(code, options) +Evaluate JavaScript code. The semantics of this eval are very similar to the eval used in JavaScript; +the last expression evaluated in the `code` string is used as the return value of this function. To +evaluate `code` in strict mode, the first expression should be the string `"use strict"`. + +#### options +The eval function supports an options object that can affect how JS code is evaluated in powerful ways. +They are largely based on SpiderMonkey's `CompileOptions`. The supported option keys are: +- `filename`: set the filename of this code for the purposes of generating stack traces etc. +- `lineno`: set the line number offset of this code for the purposes of generating stack traces etc. +- `column`: set the column number offset of this code for the purposes of generating stack traces etc. +- `mutedErrors`: if set to `True`, eval errors or unhandled rejections are ignored ("muted"). Default `False`. +- `noScriptRval`: if `False`, return the last expression value of the script as the result value to the caller. Default `False`. +- `selfHosting`: *experimental* +- `strict`: forcibly evaluate in strict mode (`"use strict"`). Default `False`. +- `module`: indicate the file is an ECMAScript module (always strict mode code and disallow HTML comments). Default `False`. +- `fromPythonFrame`: generate the equivalent of filename, lineno, and column based on the location of + the Python call to eval. This makes it possible to evaluate Python multiline string literals and + generate stack traces in JS pointing to the error in the Python source file. + +#### tricks +- function literals evaluate as `undefined` in JavaScript; if you want to return a function, you must + evaluate an expression: + ```python + pythonmonkey.eval("myFunction() { return 123 }; myFunction") + ``` + or + ```python + pythonmonkey.eval("(myFunction() { return 123 })") + ``` +- function expressions are a great way to build JS IIFEs that accept Python arguments + ```python + pythonmonkey.eval("(thing) => console.log('you said', thing)")("this string came from Python") + ``` ### require(moduleIdentifier) Return the exports of a CommonJS module identified by `moduleIdentifier`, using standard CommonJS @@ -183,9 +230,44 @@ necessary unless the main entry point of your program is written in JavaScript. Care should be taken to ensure that only one program module is run per JS context. +### isCompilableUnit(code) +Examines the string `code` and returns False if the string might become a valid JS statement with +the addition of more lines. This is used internally by pmjs and can be very helpful for building +JavaScript REPLs; the idea is to accumulate lines in a buffer until isCompilableUnit is true, then +evaluate the entire buffer. + +### new(function) +Returns a Python function which invokes `function` with the JS new operator. +```python +import pythonmonkey as pm + +>>> pm.eval("class MyClass { constructor() { console.log('ran ctor') }}") +>>> MyClass = pm.eval("MyClass") +>>> MyClass() +Traceback (most recent call last): + File "", line 1, in +pythonmonkey.SpiderMonkeyError: TypeError: class constructors must be invoked with 'new' + +>>> MyClassCtor = pm.new(MyClass) +>>> MyClassCtor() +ran ctor +{} +>>> +``` + +### typeof(value) +This is the JS `typeof` operator, wrapped in a function so that it can be used easily from Python. + +### Standard Classes and Globals +All of the JS Standard Classes (Array, Function, Object, Date...) and objects (globalThis, +FinalizationRegistry...) are available as exports of the pythonmonkey module. These exports are +generated by enumerating the global variable in the current SpiderMonkey context. The current list is: +
    undefined, Boolean, JSON, Date, Math, Number, String, RegExp, Error, InternalError, AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, ArrayBuffer, Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Uint8ClampedArray, BigInt64Array, BigUint64Array, BigInt, Proxy, WeakMap, Map, Set, DataView, Symbol, Intl, Reflect, WeakSet, Promise, WebAssembly, WeakRef, Iterator, AsyncIterator, NaN, Infinity, isNaN, isFinite, parseFloat, parseInt, escape, unescape, decodeURI, encodeURI, decodeURIComponent, encodeURIComponent, Function, Object, debuggerGlobal, FinalizationRegistry, Array, globalThis
    + ## Built-In Functions See definitions in [python/pythonmonkey/global.d.ts](https://github.com/Distributive-Network/PythonMonkey/blob/main/python/pythonmonkey/global.d.ts). +Including: - `console` - `atob` @@ -208,8 +290,8 @@ pythonmonkey module. - `python.stderr` - an object with `read` and `write` methods, which read and write to stderr - `python.exec` - the Python exec function - `python.eval` - the Python eval function -- `python.exit` - the Python exit function (wrapped to return BigInt in place of number) -- `python.paths` - the Python sys.paths list (currently a copy; will become an Array-like reflection) +- `python.exit` - exit via sys.exit(); the exit code is the function argument or `python.exit.code`. +- `python.paths` - the Python sys.paths list, visible in JS as an Array ## Type Transfer (Coercion / Wrapping) When sending variables from Python into JavaScript, PythonMonkey will intelligently coerce or wrap your @@ -226,6 +308,8 @@ Where shared backing store is not possible, PythonMonkey will automatically emit the "real" data structure as its value authority. Only immutable intrinsics are copied. This means that if you update an object in JavaScript, the corresponding Dict in Python will be updated, etc. +JavaScript Array and Object methods are implemented on Python List and Dictionaries, and vice-versa. + | Python Type | JavaScript Type | |:------------|:----------------| | String | string @@ -233,7 +317,7 @@ that if you update an object in JavaScript, the corresponding Dict in Python wil | Bool | boolean | Function | function | Dict | object -| List | Array-like object +| List | Array | datetime | Date object | awaitable | Promise | Error | Error object @@ -241,14 +325,14 @@ that if you update an object in JavaScript, the corresponding Dict in Python wil | JavaScript Type | Python Type | |:---------------------|:----------------| -| string | String +| string | pythonmonkey.JSStringProxy (String) | number | Float | bigint | pythonmonkey.bigint (Integer) | boolean | Bool -| function | Function +| function | pythonmonkey.JSFunctionProxy || pythonmonkey.JSMethodProxy (Function || Method) | object - most | pythonmonkey.JSObjectProxy (Dict) | object - Date | datetime -| object - Array | List +| object - Array | pythonmonkey.JSArrayProxy (List) | object - Promise | awaitable | object - ArrayBuffer | Buffer | object - type arrays | Buffer @@ -294,7 +378,7 @@ import asyncio async def async_fn(): await pm.eval(""" - new Promise((resolve) => setTimeout((...args) => { + new Promise((resolve) => setTimeout((...args) => { console.log(args); resolve(); }, 1000, 42, "abc") @@ -320,10 +404,10 @@ in Python. Simply decorate a Dict named `exports` inside a file with a `.py` ext loaded by `require()` -- in either JavaScript or Python. ### Program Module -The program module, or main module, is a special module in CommonJS. In a program module, +The program module, or main module, is a special module in CommonJS. In a program module: - variables defined in the outermost scope are properties of `globalThis` - returning from the outermost scope is a syntax error - - the `arguments` variable in an Array-like object which holds your program's argument vector + - the `arguments` variable in an Array which holds your program's argument vector (command-line arguments) ```console @@ -350,7 +434,7 @@ exports['today'] = date.today() # Troubleshooting Tips ## CommonJS (require) -If you are having trouble with the CommonJS require function, set environment variable `DEBUG='ctx-module*'` and you can see the filenames it tries to laod. +If you are having trouble with the CommonJS require function, set environment variable `DEBUG='ctx-module*'` and you can see the filenames it tries to load. ## pmdb @@ -387,7 +471,7 @@ List of commands: ```console $ pmjs -Welcome to PythonMonkey v0.2.0. +Welcome to PythonMonkey v1.0.0. Type ".help" for more information. > .python import sys > .python sys.path @@ -399,5 +483,5 @@ $1 = { '0': '/home/wes/git/pythonmonkey2', '5': '/home/wes/git/pythonmonkey2/python' } > $1[3] '/usr/lib/python3.10/lib-dynload' -> +> ``` diff --git a/build.py b/build.py index 7d26be19..bf887cbd 100644 --- a/build.py +++ b/build.py @@ -2,9 +2,12 @@ # Main PythonMonkey build automation script. Run with `poetry build`. # @author Hamada Gasmallah, hamada@distributive.network # @date April 2023 +# @copyright Copyright (c) 2023 Distributive Corp. # + import subprocess -import os, sys +import os +import sys import platform from typing import Optional @@ -12,49 +15,63 @@ BUILD_DIR = os.path.join(TOP_DIR, "build") # Get number of CPU cores -CPUS = os.cpu_count() or 1 +CPUS = os.getenv('CPUS') or os.cpu_count() or 1 + +BUILD_TYPE = os.environ["BUILD_TYPE"].title() if "BUILD_TYPE" in os.environ else "Release" +BUILD_DOCS = "ON" if "BUILD_DOCS" in os.environ and os.environ["BUILD_DOCS"] in ("1", "ON", "on") else "OFF" + def execute(cmd: str, cwd: Optional[str] = None): - popen = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, - shell = True, text = True, cwd = cwd ) - for stdout_line in iter(popen.stdout.readline, ""): - sys.stdout.write(stdout_line) - sys.stdout.flush() + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=True, text=True, cwd=cwd) + for stdout_line in iter(popen.stdout.readline, ""): + sys.stdout.write(stdout_line) + sys.stdout.flush() + + popen.stdout.close() + return_code = popen.wait() + if return_code: + raise subprocess.CalledProcessError(return_code, cmd) - popen.stdout.close() - return_code = popen.wait() - if return_code: - raise subprocess.CalledProcessError(return_code, cmd) def ensure_spidermonkey(): - # Check if SpiderMonkey libs already exist - spidermonkey_lib_exist = os.path.exists(os.path.join( TOP_DIR, "_spidermonkey_install/lib" )) - if spidermonkey_lib_exist: - return + # Check if SpiderMonkey libs already exist + spidermonkey_lib_exist = os.path.exists(os.path.join(TOP_DIR, "_spidermonkey_install/lib")) + if spidermonkey_lib_exist: + return + + # Build SpiderMonkey + execute("bash ./setup.sh", cwd=TOP_DIR) - # Build SpiderMonkey - execute("bash ./setup.sh", cwd = TOP_DIR) def run_cmake_build(): - os.makedirs(BUILD_DIR, exist_ok=True) # mkdir -p - if platform.system() == "Windows": - execute("cmake .. -T ClangCL", cwd=BUILD_DIR) # use Clang/LLVM toolset for Visual Studio - else: - execute("cmake ..", cwd=BUILD_DIR) - execute(f"cmake --build . -j{CPUS} --config Release", cwd=BUILD_DIR) + os.makedirs(BUILD_DIR, exist_ok=True) # mkdir -p + + if platform.system() == "Windows": + # use Clang/LLVM toolset for Visual Studio + execute(f"cmake -DBUILD_DOCS={BUILD_DOCS} -DPM_BUILD_TYPE={BUILD_TYPE} .. -T ClangCL", cwd=BUILD_DIR) + else: + execute(f"cmake -DBUILD_DOCS={BUILD_DOCS} -DPM_BUILD_TYPE={BUILD_TYPE} ..", cwd=BUILD_DIR) + execute(f"cmake --build . -j{CPUS} --config Release", cwd=BUILD_DIR) + def copy_artifacts(): - if platform.system() == "Windows": - execute("cp ./build/src/*/pythonmonkey.pyd ./python/pythonmonkey/", cwd=TOP_DIR) # Release or Debug build - execute("cp ./_spidermonkey_install/lib/mozjs-*.dll ./python/pythonmonkey/", cwd=TOP_DIR) - else: - execute("cp ./build/src/pythonmonkey.so ./python/pythonmonkey/", cwd=TOP_DIR) - execute("cp ./_spidermonkey_install/lib/libmozjs* ./python/pythonmonkey/", cwd=TOP_DIR) + + if platform.system() == "Windows": + execute("cp ./build/src/*/pythonmonkey.pyd ./python/pythonmonkey/", cwd=TOP_DIR) # Release or Debug build + execute("cp ./_spidermonkey_install/lib/mozjs-*.dll ./python/pythonmonkey/", cwd=TOP_DIR) + else: + execute("cp ./build/src/pythonmonkey.so ./python/pythonmonkey/", cwd=TOP_DIR) + execute("cp ./_spidermonkey_install/lib/libmozjs* ./python/pythonmonkey/", cwd=TOP_DIR) + def build(): + if BUILD_TYPE != "None": # do not build SpiderMonkey if we are not compiling ensure_spidermonkey() - run_cmake_build() + run_cmake_build() + if BUILD_TYPE != "None": # do not copy artifacts if we did not build them copy_artifacts() + if __name__ == "__main__": - build() + build() diff --git a/cmake/docs/CMakeLists.txt b/cmake/docs/CMakeLists.txt index 4ada1336..4563c06d 100644 --- a/cmake/docs/CMakeLists.txt +++ b/cmake/docs/CMakeLists.txt @@ -1,23 +1,14 @@ -# first we can indicate the documentation build as an option and set it to ON by default -option(BUILD_DOC "Build documentation" ON) +# set input and output files +set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) +set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) -# check if Doxygen is installed -find_package(Doxygen) -if (DOXYGEN_FOUND) - # set input and output files - set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) - set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) +# request to configure the file +configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) +message("Building docs with Doxygen") - # request to configure the file - configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) - message("Doxygen build started") - - # note the option ALL which allows to build the docs together with the application - add_custom_target( doc_doxygen ALL - COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating API documentation with Doxygen" - VERBATIM ) -else (DOXYGEN_FOUND) - message("Doxygen need to be installed to generate the doxygen documentation") -endif (DOXYGEN_FOUND) \ No newline at end of file +# note the option ALL which allows to build the docs together with the application +add_custom_target( doc_doxygen ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM ) \ No newline at end of file diff --git a/cmake/docs/Doxyfile.in b/cmake/docs/Doxyfile.in index 094e53d1..e75b34e7 100644 --- a/cmake/docs/Doxyfile.in +++ b/cmake/docs/Doxyfile.in @@ -1,6 +1,5 @@ OUTPUT_DIRECTORY = "@CMAKE_BINARY_DIR@/docs/" INPUT = "@CMAKE_SOURCE_DIR@/README.md" \ - "@CMAKE_SOURCE_DIR@/docs/" \ "@CMAKE_SOURCE_DIR@/src/" \ "@CMAKE_SOURCE_DIR@/include/" \ "@CMAKE_SOURCE_DIR@/python/pythonmonkey/" @@ -38,10 +37,9 @@ HTML_EXTRA_FILES = @CMAKE_CURRENT_SOURCE_DIR@/favicon.ico \ @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-paragraph-link.js \ @CMAKE_CURRENT_SOURCE_DIR@/doxygen-awesome-css/doxygen-awesome-interactive-toc.js HTML_COLORSTYLE = LIGHT -HTML_TIMESTAMP = YES +TIMESTAMP = YES # Diagrams with Graphviz HAVE_DOT = YES DOT_IMAGE_FORMAT = svg -DOT_TRANSPARENT = YES INTERACTIVE_SVG = YES diff --git a/cmake/externals/autopep8/CMakeLists.txt b/cmake/externals/autopep8/CMakeLists.txt deleted file mode 100644 index ebc95787..00000000 --- a/cmake/externals/autopep8/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2022 Distributive Inc. All Rights Reserved. - -set(AUTOPEP8_ROOT "${CMAKE_CURRENT_BINARY_DIR}/install" CACHE PATH - "The autopep8 root directory." -) -if(NOT AUTOPEP8_ROOT STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/install") - return() -endif() - - set(AUTOPEP8_DOWNLOAD_PACKAGE_FILE_ID "5d/9b/1ed75f8c9086fafe0e9bbb379a70c43b1aa9dff6154ddcfb818f78cb0736/autopep8-1.7.0-py2.py3-none-any.whl") - set(AUTOPEP8_DOWNLOAD_SHA256 - "6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087" - ) - -string(CONCAT AUTOPEP8_DOWNLOAD_URL - "https://files.pythonhosted.org/" - "packages/${AUTOPEP8_DOWNLOAD_PACKAGE_FILE_ID}" -) - -include("${CMAKE_ROOT}/Modules/FetchContent.cmake") - -FetchContent_Declare(autopep8 - DOWNLOAD_EXTRACT_TIMESTAMP FALSE - PREFIX "${CMAKE_CURRENT_BINARY_DIR}" - SOURCE_DIR "${AUTOPEP8_ROOT}" - URL "${AUTOPEP8_DOWNLOAD_URL}" - URL_HASH "SHA256=${AUTOPEP8_DOWNLOAD_SHA256}" - TLS_VERIFY TRUE -) -FetchContent_MakeAvailable(autopep8) diff --git a/cmake/externals/uncrustify/CMakeLists.txt b/cmake/externals/uncrustify/CMakeLists.txt deleted file mode 100644 index ba95422b..00000000 --- a/cmake/externals/uncrustify/CMakeLists.txt +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2022 Distributive Inc. All Rights Reserved. - -set(UNCRUSTIFY_ROOT "${CMAKE_CURRENT_BINARY_DIR}/install" CACHE PATH - "The Uncrustify root directory." -) -if(NOT UNCRUSTIFY_ROOT STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/install") - return() -endif() - -# 0.1.12 -if(CMAKE_SYSTEM_NAME MATCHES "^Darwin$") - set(UNCRUSTIFY_DOWNLOAD_PACKAGE_FILE_ID "47722390") - set(UNCRUSTIFY_DOWNLOAD_SHA256 - "f53c51c30f8482cf801bf4db11becc7ec62a7e86f0fc90878150e9a85bb7638b" - ) -elseif(CMAKE_SYSTEM_NAME MATCHES "^Linux$") - set(UNCRUSTIFY_DOWNLOAD_PACKAGE_FILE_ID "47722428") - set(UNCRUSTIFY_DOWNLOAD_SHA256 - "595a4634831777bf77612ca0223ef5cf8da9aac4aee885291a886bf9b358153d" - ) -elseif(CMAKE_SYSTEM_NAME MATCHES "^Windows$") - set(UNCRUSTIFY_DOWNLOAD_PACKAGE_FILE_ID "47722782") - set(UNCRUSTIFY_DOWNLOAD_SHA256 - "2505c9397c0a6f96c16570044bd21ea7992f27118cc1f2e3d7a6945b8e2a4702" - ) -else() - message(WARNING "No prebuilt Uncrustify library for this platform.") - return() -endif() - -string(CONCAT UNCRUSTIFY_DOWNLOAD_URL - "https://gitlab.com/Distributed-Compute-Protocol/uncrustify-build/-/" - "package_files/${UNCRUSTIFY_DOWNLOAD_PACKAGE_FILE_ID}/download" -) - -include("${CMAKE_ROOT}/Modules/FetchContent.cmake") - -FetchContent_Declare(Uncrustify - DOWNLOAD_EXTRACT_TIMESTAMP FALSE - PREFIX "${CMAKE_CURRENT_BINARY_DIR}" - SOURCE_DIR "${UNCRUSTIFY_ROOT}" - URL "${UNCRUSTIFY_DOWNLOAD_URL}" - URL_HASH "SHA256=${UNCRUSTIFY_DOWNLOAD_SHA256}" - TLS_VERIFY TRUE -) -FetchContent_MakeAvailable(Uncrustify) diff --git a/cmake/format/CMakeLists.txt b/cmake/format/CMakeLists.txt deleted file mode 100644 index 19d8c7ce..00000000 --- a/cmake/format/CMakeLists.txt +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) 2022 Distributive Inc. All Rights Reserved. - -if(NOT UNCRUSTIFY_EXECUTABLE) - if(UNCRUSTIFY_ROOT STREQUAL "") - message(STATUS "Target '${PROJECT_NAME}-format-cpp' not added:" - " UNCRUSTIFY_ROOT not set" - ) - return() - endif() - - find_program(UNCRUSTIFY_EXECUTABLE - NAMES "uncrustify" - DOC "Uncrustify executable path" - PATHS "${UNCRUSTIFY_ROOT}" - PATH_SUFFIXES "bin" - NO_DEFAULT_PATH - ) - if(NOT UNCRUSTIFY_EXECUTABLE) - message(STATUS "Target '${PROJECT_NAME}-format-cpp' not added:" - " Uncrustify not found" - ) - return() - endif() -endif() -message(STATUS "Using Uncrustify: ${UNCRUSTIFY_EXECUTABLE}") - -option(PYTHONMONKEY_CPP_FORMAT_FIX "Automatically fix formatting errors." ON) -if(PYTHONMONKEY_CPP_FORMAT_FIX) - message(STATUS "Automatically fixing C++ formatting errors") - set(PYTHONMONKEY_CPP_FORMAT_OPTIONS "--replace" "--if-changed" "--no-backup") - set(PYTHONMONKEY_CPP_FORMAT_COMMENT "Checking and fixing code formatting...") -else() - message(STATUS "Reporting C++ formatting errors without fixing") - set(PYTHONMONKEY_CPP_FORMAT_OPTIONS "--check") - string(CONCAT PYTHONMONKEY_CPP_FORMAT_COMMENT "Checking code formatting" - " (regenerate with PYTHONMONKEY_CPP_FORMAT_FIX=ON to automatically fix errors)..." - ) -endif() - -file(GLOB_RECURSE PYTHONMONKEY_CPP_FORMAT_FILES CONFIGURE_DEPENDS - "${PROJECT_SOURCE_DIR}/include/*.hh" - "${PROJECT_SOURCE_DIR}/src/*.cc" -) - -set(PYTHONMONKEY_CPP_FORMAT_COMMAND - "${UNCRUSTIFY_EXECUTABLE}" "-c" "uncrustify.cfg" - ${PYTHONMONKEY_CPP_FORMAT_OPTIONS} - ${PYTHONMONKEY_CPP_FORMAT_FILES} -) - -add_custom_target("${PROJECT_NAME}-format-cpp" ALL - COMMAND ${PYTHONMONKEY_CPP_FORMAT_COMMAND} - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - COMMENT "${PYTHONMONKEY_CPP_FORMAT_COMMENT}" - VERBATIM - SOURCES "uncrustify.cfg" -) -# pythonmonkey_target_initialize("${PROJECT_NAME}-format-cpp") - -if(NOT AUTOPEP8_SCRIPT) - if(AUTOPEP8_ROOT STREQUAL "") - message(STATUS "Target '${PROJECT_NAME}-format-python' not added:" - " AUTOPEP8_ROOT not set" - ) - return() - endif() - - find_program(AUTOPEP8_SCRIPT - NAMES "autopep8.py" - DOC "autopep8 script path" - PATHS "${AUTOPEP8_ROOT}" - NO_DEFAULT_PATH - ) - if(NOT AUTOPEP8_SCRIPT) - message(STATUS "Target '${PROJECT_NAME}-format-python' not added:" - " autopep8 not found" - ) - return() - endif() -endif() -message(STATUS "Using autopep8: ${AUTOPEP8_SCRIPT}") - -option(PYTHONMONKEY_PYTHON_FORMAT_FIX "Automatically fix formatting errors." ON) -if(PYTHONMONKEY_PYTHON_FORMAT_FIX) - message(STATUS "Automatically fixing Python formatting errors") - set(PYTHONMONKEY_PYTHON_FORMAT_OPTIONS "--in-place" "--verbose" "--aggressive" "--aggressive") - set(PYTHONMONKEY_PYTHON_FORMAT_COMMENT "Checking and fixing code formatting...") -else() - message(STATUS "Reporting Python formatting errors without fixing") - set(PYTHONMONKEY_PYTHON_FORMAT_OPTIONS "--diff" "--verbose" "--aggressive" "--aggressive") - string(CONCAT PYTHONMONKEY_PYTHON_FORMAT_COMMENT "Checking code formatting" - " (regenerate with PYTHONMONKEY_PYTHON_FORMAT_FIX=ON to automatically fix errors)..." - ) -endif() - -file(GLOB_RECURSE PYTHONMONKEY_PYTHON_FORMAT_FILES CONFIGURE_DEPENDS - "${PROJECT_SOURCE_DIR}/python/*.py" - "${PROJECT_SOURCE_DIR}/tests/python/*.py" -) - -set(PYTHONMONKEY_PYTHON_FORMAT_COMMAND - "python3 ${AUTOPEP8_SCRIPT}" - ${PYTHONMONKEY_PYTHON_FORMAT_OPTIONS} - ${PYTHONMONKEY_PYTHON_FORMAT_FILES} -) - -execute_process( - COMMAND ${PYTHONMONKEY_PYTHON_FORMAT_COMMAND} - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" -) \ No newline at end of file diff --git a/cmake/modules/FindSpiderMonkey.cmake b/cmake/modules/FindSpiderMonkey.cmake index bd9f7b93..84fca1ca 100644 --- a/cmake/modules/FindSpiderMonkey.cmake +++ b/cmake/modules/FindSpiderMonkey.cmake @@ -24,8 +24,6 @@ # SPIDERMONKEY_FOUND - True if SpiderMonkey found. # SPIDERMONKEY_THREADSAFE - True if SpiderMonkey is compiled with multi threading support. -#Last Change: 2022-10-03 (Caleb Aikens) - include(CheckIncludeFileCXX) include(CheckCXXSourceCompiles) include(CheckCXXSourceRuns) @@ -36,10 +34,26 @@ if(SPIDERMONKEY_FOUND) set(SPIDERMONKEY_FIND_QUIETLY TRUE) endif() +# Get the SpiderMonkey major version number +# See https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/old-configure.in#l1081 +file(GLOB LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/_spidermonkey_install") +execute_process(COMMAND + "sh" "-c" "./js*-config --version" # Run "_spidermonkey_install/bin/js*-config --version" to print the full version number + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/_spidermonkey_install/bin" + OUTPUT_VARIABLE MOZILLA_VERSION +) +string(STRIP ${MOZILLA_VERSION} MOZILLA_VERSION) +string(REGEX REPLACE "^([0-9]+)(\\.[0-9]+)*([ab][0-9]|)?" # Only the MAJOR and the "a1" (indicator of nightly build) part is needed + "\\1\\3" # see https://hg.mozilla.org/releases/mozilla-esr102/file/tip/build/moz.configure/init.configure#l959 + MOZILLA_SYMBOLVERSION + ${MOZILLA_VERSION} +) + # SpiderMonkey search paths set(SPIDERMONKEY_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/_spidermonkey_install" "${CMAKE_CURRENT_SOURCE_DIR}/_spidermonkey_install/lib" + "${CMAKE_CURRENT_SOURCE_DIR}/_spidermonkey_install/include/mozjs-${MOZILLA_SYMBOLVERSION}" ${SPIDERMONKEY_ROOT} $ENV{SPIDERMONKEY_ROOT} ~/Library/Frameworks @@ -59,7 +73,7 @@ set(SPIDERMONKEY_PATHS set(SPIDERMONKEY_HEADERS jsapi.h js/RequiredDefines.h) # SpiderMonkey include suffix paths -set(SPIDERMONKEY_INCLUDE_SUFFIX_PATHS include/mozjs-115/) +set(SPIDERMONKEY_INCLUDE_SUFFIX_PATHS include/mozjs-${MOZILLA_SYMBOLVERSION}/) # Find SpiderMonkey include path find_path(SPIDERMONKEY_INCLUDE_DIR ${SPIDERMONKEY_HEADERS} @@ -70,7 +84,7 @@ find_path(SPIDERMONKEY_INCLUDE_DIR ${SPIDERMONKEY_HEADERS} ) # SpiderMonkey libs -set(SPIDERMONKEY_LIBRARY_NAMES libmozjs-115.so libmozjs-115.dylib mozjs-115.lib) +set(SPIDERMONKEY_LIBRARY_NAMES libmozjs-${MOZILLA_SYMBOLVERSION}.so libmozjs-${MOZILLA_SYMBOLVERSION}.dylib mozjs-${MOZILLA_SYMBOLVERSION}.lib) set(SPIDERMONKEY_LIB_SUFFIX_PATHS js/src/build lib) @@ -92,8 +106,8 @@ check_cxx_source_compiles( "#include int main() { - JSRuntime *rt = JS_NewRuntime(8L * 1024L * 1024L); - if (rt != NULL) + JSContext *cx = JS_NewContext(JS::DefaultHeapMaxBytes); + if (cx != NULL) { return 0; } diff --git a/examples/use-python-module.py b/examples/use-python-module.py index 3d1b8f65..e493a014 100644 --- a/examples/use-python-module.py +++ b/examples/use-python-module.py @@ -5,4 +5,4 @@ import pythonmonkey as pm -pm.require('./use-python-module'); +pm.require('./use-python-module') diff --git a/examples/use-python-module/my-python-module.py b/examples/use-python-module/my-python-module.py index 236b873d..c0f02f21 100644 --- a/examples/use-python-module/my-python-module.py +++ b/examples/use-python-module/my-python-module.py @@ -1,5 +1,5 @@ def helloWorld(): print('hello, world!') -exports['helloWorld'] = helloWorld +exports['helloWorld'] = helloWorld diff --git a/examples/use-require.py b/examples/use-require.py index d986766e..254209c0 100644 --- a/examples/use-require.py +++ b/examples/use-require.py @@ -5,6 +5,5 @@ import pythonmonkey as pm -pm.require('./use-require/test1'); +pm.require('./use-require/test1') print("Done") - diff --git a/examples/use-require/test2.js b/examples/use-require/test2.js index 92076222..e39780f1 100644 --- a/examples/use-require/test2.js +++ b/examples/use-require/test2.js @@ -1,9 +1,9 @@ -'use strict' +'use strict'; exports.makeOutput = function makeOutput() { const argv = Array.from(arguments); argv.unshift('TEST OUTPUT: '); python.print.apply(null, argv); -} +}; diff --git a/githooks/pre-commit b/githooks/pre-commit new file mode 100755 index 00000000..fe574f22 --- /dev/null +++ b/githooks/pre-commit @@ -0,0 +1,129 @@ +#!/bin/bash +# +# @file pre-commit +# This hook script lints all code with Uncrustify, ESLint, and autopep8 on +# what is about to be commited. Called by "git commit". If this script +# exits with a non-zero status nothing will be committed. +# +# To disable checks on C++ files, do "git config hooks.nocclinting true", +# and "git config hooks.nocclinting false" to re-enable checks. +# +# To disable checks on JavaScript files, do "git config hooks.nojslinting true", +# and "git config hooks.nojslinting false" to re-enable checks. +# +# To disable checks on Python files, do "git config hooks.nopylinting true", +# and "git config hooks.pylinting false" to re-enable checks. +# +# To commit without this hook running at all, do "git commit --no-verify" +# +# @author Caleb Aikens, caleb@distributive.network +# @date Apr 2024 + +nocclinting=$(git config --type=bool hooks.nocclinting) +nojslinting=$(git config --type=bool hooks.nojslinting) +nopylinting=$(git config --type=bool hooks.nopylinting) + +RED="\e[31m" +YELLOW="\e[33m" +ENDCOLOUR="\e[0m" + +UNCRUSTIFY_SUCCESS=true +if [ "$nocclinting" != "true" ]; then + echo "linting C++ files ..." + for ccFile in `git diff --cached --name-only --diff-filter=ACM | grep -E '.cc$|.hh$' | cat`; + do + ./uncrustify --check -c uncrustify.cfg "${ccFile}" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + UNCRUSTIFY_SUCCESS=false + echo -e "uncrustify failed on: ${RED}${ccFile}${ENDCOLOUR}" + fi + done + echo "finished linting C++ files." +fi + +if [ "$UNCRUSTIFY_SUCCESS" = false ]; then +echo -e "$( +cat <'. +This will output the fixed file in the same directory with a .uncrustify extension. +You can automatically fix the file with './uncrustify -c uncrustify.cfg --replace --no-backup ' +Alternatively, you can temporarily disable uncrustify linting in a C++ file with '/* *INDENT-OFF* */' +and re-enable it with '/* *INDENT-ON* */'. +If you know what you are doing you can disable this check using: + + git config hooks.nocclinting true +${ENDCOLOUR} +EOF +)" +fi + +ESLINT_SUCCESS=true +if [ "$nojslinting" != "true" ]; then + echo "linting JavaScript files ..." + for jsFile in `git diff --cached --name-only --diff-filter=ACM | grep -E '.js$|.simple$' | cat`; + do + ESLINT_USE_FLAT_CONFIG=false eslint --max-warnings=1 "${jsFile}" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + ESLINT_SUCCESS=false + echo -e "eslint failed on: ${RED}${jsFile}${ENDCOLOUR}" + fi + done + echo "finished linting JavaScript files." +fi + +if [ "$ESLINT_SUCCESS" = false ]; then +echo -e "$( +cat <'. +Most issues can be automatically fixed with 'eslint --fix '. +NOTE: If using v9.0.0 of eslint or greater, you will have to set the ESLINT_USE_FLAT_CONFIG +environment variable to false. +Alternatively, you can temporarily disable eslint linting in a js file with '/* eslint-disable */' +and re-enable it with '/* eslint-enable */'. +If you know what you are doing you can disable this check using: + + git config hooks.nojslinting true +${ENDCOLOUR} +EOF +)" +fi + +AUTOPEP8_SUCCESS=true +if [ "$nopylinting" != "true" ]; then + echo "linting python files ..." + for pythonFile in `git diff --cached --name-only --diff-filter=ACM | grep -E '.py$|.pyi$' | cat`; + do + autopep8 "${pythonFile}" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + AUTOPEP8_SUCCESS=false + echo -e "autopep8 failed on: ${RED}${pythonFile}${ENDCOLOUR}" + fi + done + echo "finished linting python files." +fi + +if [ "$AUTOPEP8_SUCCESS" = false ]; then +echo -e "$( +cat <'. +Most issues can be automatically fixed with 'autopep8 -i '. +A description of each warning code can be seen with 'autopep8 --list-fixes', +or at: https://pep8.readthedocs.io/en/release-1.7.x/intro.html +Alternatively, you can temporarily disable autopep8 linting in a python file with '# autopep8: off' +and re-enable it with '# autopep8: on'. +If you know what you are doing you can disable this check using: + + git config hooks.nopylinting true +${ENDCOLOUR} +EOF +)" +fi + +if [ "$UNCRUSTIFY_SUCCESS" = false ] ||[ "$ESLINT_SUCCESS" = false ] || [ "$AUTOPEP8_SUCCESS" = false ]; then + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/include/BoolType.hh b/include/BoolType.hh index 5e2cc05c..a3815847 100644 --- a/include/BoolType.hh +++ b/include/BoolType.hh @@ -1,31 +1,24 @@ /** * @file BoolType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing python bools - * @version 0.1 * @date 2022-12-02 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ #ifndef PythonMonkey_BoolType_ #define PythonMonkey_BoolType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include /** - * @brief This struct represents the 'bool' type in Python, which is represented as a 'long' in C++. It inherits from the PyType struct + * @brief This struct represents the 'bool' type in Python, which is represented as a 'long' in C++ */ -struct BoolType : public PyType { +struct BoolType { public: - BoolType(PyObject *object); - BoolType(long n); - const TYPE returnType = TYPE::BOOL; - long getValue() const; + static PyObject *getPyObject(long n); }; #endif \ No newline at end of file diff --git a/include/BufferType.hh b/include/BufferType.hh index ea2bd954..9c40d72e 100644 --- a/include/BufferType.hh +++ b/include/BufferType.hh @@ -1,46 +1,41 @@ /** * @file BufferType.hh - * @author Tom Tang (xmader@distributive.network) + * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing ArrayBuffers - * @version 0.1 * @date 2023-04-27 * - * @copyright Copyright (c) 2023 Distributive Corp. + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #ifndef PythonMonkey_BufferType_ #define PythonMonkey_BufferType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include #include -struct BufferType : public PyType { +struct BufferType { public: - BufferType(PyObject *object); - /** * @brief Construct a new BufferType object from a JS TypedArray or ArrayBuffer, as a Python [memoryview](https://docs.python.org/3.9/c-api/memoryview.html) object * * @param cx - javascript context pointer * @param bufObj - JS object to be coerced + * + * @returns PyObject* pointer to the resulting PyObject */ - BufferType(JSContext *cx, JS::HandleObject bufObj); - - const TYPE returnType = TYPE::BUFFER; + static PyObject *getPyObject(JSContext *cx, JS::HandleObject bufObj); /** * @brief Convert a Python object that [provides the buffer interface](https://docs.python.org/3.9/c-api/typeobj.html#buffer-object-structures) to JS TypedArray. * The subtype (Uint8Array, Float64Array, ...) is automatically determined by the Python buffer's [format](https://docs.python.org/3.9/c-api/buffer.html#c.Py_buffer.format) * * @param cx - javascript context pointer + * @param pyObject - the object to be converted */ - JSObject *toJsTypedArray(JSContext *cx); + static JSObject *toJsTypedArray(JSContext *cx, PyObject *pyObject); /** * @returns Is the given JS object either a TypedArray or an ArrayBuffer? diff --git a/include/DateType.hh b/include/DateType.hh index 23f59707..98ae9bfe 100644 --- a/include/DateType.hh +++ b/include/DateType.hh @@ -1,44 +1,38 @@ /** * @file DateType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing python dates - * @version 0.1 * @date 2022-12-21 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ #ifndef PythonMonkey_DateType_ #define PythonMonkey_DateType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include #include /** - * @brief This struct represents the 'datetime' type in Python from the datetime module, which is represented as a 'Date' object in JS. It inherits from the PyType struct + * @brief This struct represents the 'datetime' type in Python from the datetime module, which is represented as a 'Date' object in JS */ -struct DateType : public PyType { +struct DateType { public: - DateType(PyObject *object); /** * @brief Convert a JS Date object to Python datetime */ - DateType(JSContext *cx, JS::HandleObject dateObj); - - const TYPE returnType = TYPE::DATE; + static PyObject *getPyObject(JSContext *cx, JS::HandleObject dateObj); /** * @brief Convert a Python datetime object to JS Date * * @param cx - javascript context pointer + * @param pyObject - the python datetime object to be converted */ - JSObject *toJsDate(JSContext *cx); + static JSObject *toJsDate(JSContext *cx, PyObject *pyObject); }; #endif \ No newline at end of file diff --git a/include/DictType.hh b/include/DictType.hh index 099caafd..2bbea5f0 100644 --- a/include/DictType.hh +++ b/include/DictType.hh @@ -1,58 +1,36 @@ /** * @file DictType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct representing python dictionaries - * @version 0.1 * @date 2022-08-10 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ #ifndef PythonMonkey_DictType_ #define PythonMonkey_DictType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include /** - * @brief This struct represents a dictionary in python. It derives from the PyType struct + * @brief This struct represents a dictionary in python. * * @author Giovanni */ -struct DictType : public PyType { +struct DictType { public: - DictType(); - DictType(PyObject *object); - /** * @brief Construct a new DictType object from a JSObject. * * @param cx - pointer to the JSContext * @param jsObject - pointer to the JSObject to be coerced + * + * @returns PyObject* pointer to the resulting PyObject */ - DictType(JSContext *cx, JS::Handle jsObject); - - const TYPE returnType = TYPE::DICT; -/** - * @brief The 'set' method for a python dictionary. Sets the approprite 'key' in the dictionary with the appropriate 'value' - * - * @param key The key of the dictionary item - * @param value The value of the dictionary item - */ - void set(PyType *key, PyType *value); - -/** - * @brief Gets the dictionary item at the given 'key' - * - * @param key The key of the item in question - * @return PyType* Returns a pointer to the appropriate PyType object - */ - PyType *get(PyType *key) const; + static PyObject *getPyObject(JSContext *cx, JS::Handle jsObject); }; #endif \ No newline at end of file diff --git a/include/ExceptionType.hh b/include/ExceptionType.hh index 86045e8f..0f2e969f 100644 --- a/include/ExceptionType.hh +++ b/include/ExceptionType.hh @@ -1,20 +1,16 @@ /** * @file ExceptionType.hh - * @author Tom Tang (xmader@distributive.network) + * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing Python Exception objects from a corresponding JS Error object - * @version 0.1 * @date 2023-04-11 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #ifndef PythonMonkey_ExceptionType_ #define PythonMonkey_ExceptionType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include @@ -22,26 +18,26 @@ /** * @brief This struct represents a Python Exception object from the corresponding JS Error object */ -struct ExceptionType : public PyType { +struct ExceptionType { public: - ExceptionType(PyObject *object); - /** * @brief Construct a new SpiderMonkeyError from the JS Error object. * * @param cx - javascript context pointer * @param error - JS Error object to be converted + * + * @returns PyObject* pointer to the resulting PyObject */ - ExceptionType(JSContext *cx, JS::HandleObject error); - - const TYPE returnType = TYPE::EXCEPTION; + static PyObject *getPyObject(JSContext *cx, JS::HandleObject error); /** - * @brief Convert a python [*Exception object](https://docs.python.org/3/c-api/exceptions.html#standard-exceptions) to JS Error object + * @brief Convert a python Exception object to a JS Error object * * @param cx - javascript context pointer + * @param exceptionValue - Exception object pointer, cannot be NULL + * @param traceBack - Exception traceback pointer, can be NULL */ - JSObject *toJsError(JSContext *cx); + static JSObject *toJsError(JSContext *cx, PyObject *exceptionValue, PyObject *traceBack); }; #endif \ No newline at end of file diff --git a/include/FloatType.hh b/include/FloatType.hh index 2544c251..c105ba04 100644 --- a/include/FloatType.hh +++ b/include/FloatType.hh @@ -1,32 +1,24 @@ /** * @file FloatType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing python floats - * @version 0.1 * @date 2022-12-02 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ #ifndef PythonMonkey_FloatType_ #define PythonMonkey_FloatType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include /** - * @brief This struct represents the 'float' type in Python, which is represented as a 'double' in C++. It inherits from the PyType struct + * @brief This struct represents the 'float' type in Python, which is represented as a 'double' in C++ */ -struct FloatType : public PyType { +struct FloatType { public: - FloatType(PyObject *object); - FloatType(long n); - FloatType(double n); - const TYPE returnType = TYPE::FLOAT; - double getValue() const; + static PyObject *getPyObject(double n); }; #endif \ No newline at end of file diff --git a/include/FuncType.hh b/include/FuncType.hh index d8ce62f6..5a5d0d01 100644 --- a/include/FuncType.hh +++ b/include/FuncType.hh @@ -1,29 +1,26 @@ /** * @file FuncType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct representing python functions - * @version 0.1 * @date 2022-08-08 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ + #ifndef PythonMonkey_FuncType_ #define PythonMonkey_FuncType_ -#include "PyType.hh" -#include "TypeEnum.hh" +#include #include /** - * @brief This struct represents the 'function' type in Python. It inherits from the PyType struct + * @brief This struct represents the 'function' type in Python */ -struct FuncType : public PyType { +struct FuncType { public: - FuncType(PyObject *object); - const TYPE returnType = TYPE::FUNC; - const char *getValue() const; + static PyObject *getPyObject(JSContext *cx, JS::HandleValue fval); }; #endif \ No newline at end of file diff --git a/include/IntType.hh b/include/IntType.hh index 81111216..8db78f8a 100644 --- a/include/IntType.hh +++ b/include/IntType.hh @@ -1,48 +1,42 @@ /** * @file IntType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) & Tom Tang (xmader@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network), Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing python ints - * @version 0.2 * @date 2023-03-16 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #ifndef PythonMonkey_IntType_ #define PythonMonkey_IntType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include /** - * @brief This struct represents the 'int' type (arbitrary-precision) in Python. It inherits from the PyType struct + * @brief This struct represents the 'int' type (arbitrary-precision) in Python */ -struct IntType : public PyType { +struct IntType { public: - IntType(PyObject *object); - IntType(long n); - /** - * @brief Construct a new IntType object from a JS::BigInt. + * @brief Construct a new PyObject from a JS::BigInt. * * @param cx - javascript context pointer * @param bigint - JS::BigInt pointer + * + * @returns PyObject* pointer to the resulting PyObject */ - IntType(JSContext *cx, JS::BigInt *bigint); - - const TYPE returnType = TYPE::INT; + static PyObject *getPyObject(JSContext *cx, JS::BigInt *bigint); /** - * @brief Convert the IntType object to a JS::BigInt + * @brief Convert an int object to a JS::BigInt * * @param cx - javascript context pointer + * @param pyObject - the int object to be converted */ - JS::BigInt *toJsBigInt(JSContext *cx); + static JS::BigInt *toJsBigInt(JSContext *cx, PyObject *pyObject); }; #endif \ No newline at end of file diff --git a/include/JSArrayIterProxy.hh b/include/JSArrayIterProxy.hh new file mode 100644 index 00000000..f0ca0683 --- /dev/null +++ b/include/JSArrayIterProxy.hh @@ -0,0 +1,110 @@ +/** + * @file JSArrayIterProxy.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSArrayIterProxy is a custom C-implemented python type that derives from PyListIter + * @version 0.1 + * @date 2024-01-15 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSArrayIterProxy_ +#define PythonMonkey_JSArrayIterProxy_ + + +#include + +#include + + +// redeclare hidden type +typedef struct { + PyObject_HEAD + int it_index; + bool reversed; + PyListObject *it_seq; /* Set to NULL when iterator is exhausted */ +} PyListIterObject; + +/** + * @brief The typedef for the backing store that will be used by JSArrayIterProxy objects. + * + */ +typedef struct { + PyListIterObject it; +} JSArrayIterProxy; + +/** + * @brief This struct is a bundle of methods used by the JSArrayProxy type + * + */ +struct JSArrayIterProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSArrayProxy + * + * @param self - The JSArrayIterProxy to be free'd + */ + static void JSArrayIterProxy_dealloc(JSArrayIterProxy *self); + + /** + * @brief .tp_traverse method + * + * @param self - The JSArrayIterProxy + * @param visit - The function to be applied on each element of the list + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSArrayIterProxy_traverse(JSArrayIterProxy *self, visitproc visit, void *arg); + + /** + * @brief .tp_clear method + * + * @param self - The JSArrayIterProxy + * @return 0 on success + */ + static int JSArrayIterProxy_clear(JSArrayIterProxy *self); + + /** + * @brief .tp_iter method + * + * @param self - The JSArrayIterProxy + * @return PyObject* - an interator over the iterator + */ + static PyObject *JSArrayIterProxy_iter(JSArrayIterProxy *self); + + /** + * @brief .tp_next method + * + * @param self - The JSArrayIterProxy + * @return PyObject* - next object in iteration + */ + static PyObject *JSArrayIterProxy_next(JSArrayIterProxy *self); + + /** + * @brief length method + * + * @param self - The JSArrayIterProxy + * @return PyObject* - number of objects left to iterate over in iteration + */ + static PyObject *JSArrayIterProxy_len(JSArrayIterProxy *self); +}; + + +PyDoc_STRVAR(length_hint_doc, "Private method returning an estimate of len(list(it))."); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSArrayIterProxy_methods[] = { + {"__length_hint__", (PyCFunction)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_len, METH_NOARGS, length_hint_doc}, + {NULL, NULL} /* sentinel */ +}; + +/** + * @brief Struct for the JSArrayProxyType, used by all JSArrayProxy objects + */ +extern PyTypeObject JSArrayIterProxyType; + +#endif \ No newline at end of file diff --git a/include/JSArrayProxy.hh b/include/JSArrayProxy.hh new file mode 100644 index 00000000..0f452708 --- /dev/null +++ b/include/JSArrayProxy.hh @@ -0,0 +1,415 @@ +/** + * @file JSArrayProxy.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSArrayProxy is a custom C-implemented python type that derives from list. It acts as a proxy for JSArrays from Spidermonkey, and behaves like a list would. + * @version 0.1 + * @date 2023-11-22 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSArrayProxy_ +#define PythonMonkey_JSArrayProxy_ + + +#include + +#include + + +/** + * @brief The typedef for the backing store that will be used by JSArrayProxy objects. All it contains is a pointer to the JSObject + * + */ +typedef struct { + PyListObject list; + JS::PersistentRootedObject *jsArray; +} JSArrayProxy; + +/** + * @brief This struct is a bundle of methods used by the JSArrayProxy type + * + */ +struct JSArrayProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSArrayProxy + * + * @param self - The JSArrayProxy to be free'd + */ + static void JSArrayProxy_dealloc(JSArrayProxy *self); + + /** + * @brief Length method (.mp_length and .sq_length), returns the number of keys in the JSObject, used by the python len() method + * + * @param self - The JSArrayProxy + * @return Py_ssize_t The length of the JSArrayProxy + */ + static Py_ssize_t JSArrayProxy_length(JSArrayProxy *self); + + /** + * @brief returns a value from the JSArrayProxy given a key, or dispatches to the given key method if such method is found + * + * @param self - The JSArrayProxy + * @param key - The key for the value in the JSArrayProxy + * @return PyObject* NULL on exception, the corresponding value otherwise + */ + static PyObject *JSArrayProxy_get(JSArrayProxy *self, PyObject *key); + + + /** + * @brief Getter method (.mp_subscript), returns a value from the JSArrayProxy given a key which can be a slice, used by several built-in python methods as well as the [] and operator + * + * @param self - The JSArrayProxy + * @param key - The key for the value in the JSArrayProxy + * @return PyObject* NULL on exception, the corresponding value otherwise + */ + static PyObject *JSArrayProxy_get_subscript(JSArrayProxy *self, PyObject *key); + + /** + * @brief Assign method (.mp_ass_subscript), assigns a key-value pair if value is non-NULL, or deletes a key-value pair if value is NULL + * + * @param self - The JSArrayProxy + * @param key - The key to be set or deleted + * @param value If NULL, the key-value pair is deleted, if not NULL then a key-value pair is assigned + * @return int -1 on exception, any other value otherwise + */ + static int JSArrayProxy_assign_key(JSArrayProxy *self, PyObject *key, PyObject *value); + + /** + * @brief Comparison method (.tp_richcompare), returns appropriate boolean given a comparison operator and other pyObject + * + * @param self - The JSArrayProxy + * @param other - Any other PyObject + * @param op - Which boolean operator is being performed (Py_EQ for equality, Py_NE for inequality, all other operators are not implemented) + * @return PyObject* - True or false depending on result of comparison + */ + static PyObject *JSArrayProxy_richcompare(JSArrayProxy *self, PyObject *other, int op); + + /** + * @brief Return an iterator object to make JSArrayProxy iterable + * + * @param self - The JSArrayProxy + * @return PyObject* - iterator object + */ + static PyObject *JSArrayProxy_iter(JSArrayProxy *self); + + /** + * @brief Return a reverse iterator object to make JSArrayProxy backwards iterable + * + * @param self - The JSArrayProxy + * @return PyObject* - iterator object + */ + static PyObject *JSArrayProxy_iter_reverse(JSArrayProxy *self); + + /** + * @brief Compute a string representation of the JSArrayProxy + * + * @param self - The JSArrayProxy + * @return the string representation (a PyUnicodeObject) on success, NULL on failure + */ + static PyObject *JSArrayProxy_repr(JSArrayProxy *self); + + /** + * @brief concat method (.sq_concat), concatenates + * + * @param self - The JSArrayProxy + * @param value - The value to be concatenated + * @return PyObject* NULL on exception, the corresponding new value otherwise + */ + static PyObject *JSArrayProxy_concat(JSArrayProxy *self, PyObject *value); + + /** + * @brief repeat method (.sq_repeat), repeat self n number of time + * + * @param self - The JSArrayProxy + * @param n The number of times to repeat + * @return PyObject* NULL on exception, the corresponding new value otherwise + */ + static PyObject *JSArrayProxy_repeat(JSArrayProxy *self, Py_ssize_t n); + + /** + * @brief Test contains method (.sq_contains) + * + * @param self - The JSObjectProxy + * @param element - The element in the JSArrayProxy + * @return int 1 if element is in List, 0 if not, and -1 on error + */ + static int JSArrayProxy_contains(JSArrayProxy *self, PyObject *element); + + /** + * @brief inplace_concat method (.sq_inplace_concat), concatenates in_place + * + * @param self - The JSArrayProxy + * @param value - The value to be concatenated + * @return PyObject* self + */ + static PyObject *JSArrayProxy_inplace_concat(JSArrayProxy *self, PyObject *value); + + /** + * @brief inplace_repeat method (.sq_inplace_repeat), repeats in_place + * + * @param self - The JSArrayProxy + * @param n The number of times to repeat + * @return PyObject* self + */ + static PyObject *JSArrayProxy_inplace_repeat(JSArrayProxy *self, Py_ssize_t n); + + /** + * @brief clear method, empties the array + * + * @param self - The JSArrayProxy + * @return None + */ + static PyObject *JSArrayProxy_clear_method(JSArrayProxy *self); + + /** + * @brief copy method + * + * @param self - The JSArrayProxy + * @return a shallow copy of the list + */ + static PyObject *JSArrayProxy_copy(JSArrayProxy *self); + + /** + * @brief append method + * + * @param self - The JSArrayProxy + * @param value - The value to be appended + * @return PyObject* NULL on exception, None otherwise + */ + static PyObject *JSArrayProxy_append(JSArrayProxy *self, PyObject *value); + + /** + * @brief insert method + * + * @param self - The JSArrayProxy + * @param args - arguments to the insert method + * @param nargs - number of arguments to the insert method + * @return PyObject* NULL on exception, None otherwise + */ + static PyObject *JSArrayProxy_insert(JSArrayProxy *self, PyObject *const *args, Py_ssize_t nargs); + + /** + * @brief extend method + * + * @param self - The JSArrayProxy + * @param iterable - The value to be appended + * @return PyObject* NULL on exception, None otherwise + */ + static PyObject *JSArrayProxy_extend(JSArrayProxy *self, PyObject *iterable); + + /** + * @brief pop method + * + * @param self - The JSArrayProxy + * @param args - arguments to the pop method + * @param nargs - number of arguments to the pop method + * @return PyObject* NULL on exception, the corresponding value otherwise + */ + static PyObject *JSArrayProxy_pop(JSArrayProxy *self, PyObject *const *args, Py_ssize_t nargs); + + /** + * @brief remove method Remove first occurrence of value + * + * @param self - The JSArrayProxy + * @param value - The value to be appended + * @return PyObject* NULL on exception, None otherwise + */ + static PyObject *JSArrayProxy_remove(JSArrayProxy *self, PyObject *value); + + /** + * @brief index method + * + * @param self - The JSArrayProxy + * @param args - arguments to the index method + * @param nargs - number of arguments to the index method + * @return PyObject* NULL on exception, the corresponding index of the found value as PyLong otherwise + */ + static PyObject *JSArrayProxy_index(JSArrayProxy *self, PyObject *const *args, Py_ssize_t nargs); + + /** + * @brief count method + * + * @param self - The JSArrayProxy + * @param value - The value to be appended + * @return PyObject* NULL on exception, the corresponding count of the found value as PyLong otherwise + */ + static PyObject *JSArrayProxy_count(JSArrayProxy *self, PyObject *value); + + /** + * @brief reverse method Reverse list in place + * + * @param self - The JSArrayProxy + * @return PyObject* NULL on exception, None otherwise + */ + static PyObject *JSArrayProxy_reverse(JSArrayProxy *self); + + /** + * @brief sort method sort in place + * + * @param self - The JSArrayProxy + * @param args - arguments to the sort method (not used) + * @param kwargs - keyword arguments to the sort method (reverse=True|False, key=keyfunction) + * @return PyObject* NULL on exception, None otherwise + */ + static PyObject *JSArrayProxy_sort(JSArrayProxy *self, PyObject *args, PyObject *kwargs); + + /** + * @brief tp_traverse + * + * @param self - The JSArrayProxy + * @param visit - The function to be applied on each element of the list + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSArrayProxy_traverse(JSArrayProxy *self, visitproc visit, void *arg); + + /** + * @brief tp_clear + * + * @param self - The JSArrayProxy + * @return 0 on success + */ + static int JSArrayProxy_clear(JSArrayProxy *self); +}; + + +/** + * @brief Struct for the methods that define the Mapping protocol + * + */ +static PyMappingMethods JSArrayProxy_mapping_methods = { + .mp_length = (lenfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_length, + .mp_subscript = (binaryfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_get_subscript, + .mp_ass_subscript = (objobjargproc)JSArrayProxyMethodDefinitions::JSArrayProxy_assign_key +}; + +/** + * @brief Struct for the methods that define the Sequence protocol + * + */ +static PySequenceMethods JSArrayProxy_sequence_methods = { + .sq_length = (lenfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_length, + .sq_concat = (binaryfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_concat, + .sq_repeat = (ssizeargfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_repeat, + .sq_contains = (objobjproc)JSArrayProxyMethodDefinitions::JSArrayProxy_contains, + .sq_inplace_concat = (binaryfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_inplace_concat, + .sq_inplace_repeat = (ssizeargfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_inplace_repeat +}; + + +PyDoc_STRVAR(py_list_clear__doc__, + "clear($self, /)\n" + "--\n" + "\n" + "Remove all items from list."); + +PyDoc_STRVAR(list_copy__doc__, + "copy($self, /)\n" + "--\n" + "\n" + "Return a shallow copy of the list."); + +PyDoc_STRVAR(list_append__doc__, + "append($self, object, /)\n" + "--\n" + "\n" + "Append object to the end of the list."); + +PyDoc_STRVAR(list_insert__doc__, + "insert($self, index, object, /)\n" + "--\n" + "\n" + "Insert object before index."); + +PyDoc_STRVAR(py_list_extend__doc__, + "extend($self, iterable, /)\n" + "--\n" + "\n" + "Extend list by appending elements from the iterable."); + +PyDoc_STRVAR(list_pop__doc__, + "pop($self, index=-1, /)\n" + "--\n" + "\n" + "Remove and return item at index (default last).\n" + "\n" + "Raises IndexError if list is empty or index is out of range."); + +PyDoc_STRVAR(list_remove__doc__, + "remove($self, value, /)\n" + "--\n" + "\n" + "Remove first occurrence of value.\n" + "\n" + "Raises ValueError if the value is not present."); + +PyDoc_STRVAR(list_index__doc__, + "index($self, value, start=0, stop=sys.maxsize, /)\n" + "--\n" + "\n" + "Return first index of value.\n" + "\n" + "Raises ValueError if the value is not present."); + + +PyDoc_STRVAR(list_count__doc__, + "count($self, value, /)\n" + "--\n" + "\n" + "Return number of occurrences of value."); + +PyDoc_STRVAR(list_reverse__doc__, + "reverse($self, /)\n" + "--\n" + "\n" + "Reverse *IN PLACE*."); + +PyDoc_STRVAR(list_sort__doc__, + "sort($self, /, *, key=None, reverse=False)\n" + "--\n" + "\n" + "Sort the list in ascending order and return None.\n" + "\n" + "The sort is in-place (i.e. the list itself is modified) and stable (i.e. the\n" + "order of two equal elements is maintained).\n" + "\n" + "If a key function is given, apply it once to each list item and sort them,\n" + "ascending or descending, according to their function values.\n" + "\n" + "The reverse flag can be set to sort in descending order."); + +PyDoc_STRVAR(list___reversed____doc__, + "__reversed__($self, /)\n" + "--\n" + "\n" + "Return a reverse iterator over the list."); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSArrayProxy_methods[] = { + {"__reversed__", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_iter_reverse, METH_NOARGS, list___reversed____doc__}, + {"clear", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_clear_method, METH_NOARGS, py_list_clear__doc__}, + {"copy", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_copy, METH_NOARGS, list_copy__doc__}, + {"append", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_append, METH_O, list_append__doc__}, + {"insert", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_insert, METH_FASTCALL, list_insert__doc__}, + {"extend", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_extend, METH_O, py_list_extend__doc__}, + {"pop", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_pop, METH_FASTCALL, list_pop__doc__}, + {"remove", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_remove, METH_O, list_remove__doc__}, + {"index", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_index, METH_FASTCALL, list_index__doc__}, + {"count", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_count, METH_O, list_count__doc__}, + {"reverse", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_reverse, METH_NOARGS, list_reverse__doc__}, + {"sort", (PyCFunction)JSArrayProxyMethodDefinitions::JSArrayProxy_sort, METH_VARARGS|METH_KEYWORDS, list_sort__doc__}, + {NULL, NULL} /* sentinel */ +}; + +/** + * @brief Struct for the JSArrayProxyType, used by all JSArrayProxy objects + */ +extern PyTypeObject JSArrayProxyType; + +#endif \ No newline at end of file diff --git a/include/JSFunctionProxy.hh b/include/JSFunctionProxy.hh new file mode 100644 index 00000000..82109b63 --- /dev/null +++ b/include/JSFunctionProxy.hh @@ -0,0 +1,65 @@ +/** + * @file JSFunctionProxy.hh + * @author Caleb Aikens (caleb@distributive.network) + * @brief JSFunctionProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a function would. + * @date 2023-09-28 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSFunctionProxy_ +#define PythonMonkey_JSFunctionProxy_ + +#include + +#include +/** + * @brief The typedef for the backing store that will be used by JSFunctionProxy objects. All it contains is a pointer to the JSFunction + * + */ +typedef struct { + PyObject_HEAD + JS::PersistentRootedObject *jsFunc; +} JSFunctionProxy; + +/** + * @brief This struct is a bundle of methods used by the JSFunctionProxy type + * + */ +struct JSFunctionProxyMethodDefinitions { +public: +/** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSFunction before freeing the JSFunctionProxy + * + * @param self - The JSFunctionProxy to be free'd + */ + static void JSFunctionProxy_dealloc(JSFunctionProxy *self); + + /** + * @brief New method (.tp_new), creates a new instance of the JSFunctionProxy type, exposed as the __new()__ method in python + * + * @param type - The type of object to be created, will always be JSFunctionProxyType or a derived type + * @param args - arguments to the __new()__ method, not used + * @param kwds - keyword arguments to the __new()__ method, not used + * @return PyObject* - A new instance of JSFunctionProxy + */ + static PyObject *JSFunctionProxy_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + + /** + * @brief Call method (.tp_call), called when the JSFunctionProxy is called + * + * @param self - this callable, might be a free function or a method + * @param args - args to the function + * @param kwargs - keyword args to the function + * @return PyObject* - Result of the function call + */ + static PyObject *JSFunctionProxy_call(PyObject *self, PyObject *args, PyObject *kwargs); +}; + +/** + * @brief Struct for the JSFunctionProxyType, used by all JSFunctionProxy objects + */ +extern PyTypeObject JSFunctionProxyType; + +#endif \ No newline at end of file diff --git a/include/JSMethodProxy.hh b/include/JSMethodProxy.hh new file mode 100644 index 00000000..680db116 --- /dev/null +++ b/include/JSMethodProxy.hh @@ -0,0 +1,68 @@ +/** + * @file JSMethodProxy.hh + * @author Caleb Aikens (caleb@distributive.network) + * @brief JSMethodProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a method would, treating `self` as `this`. + * @date 2023-11-14 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSMethodProxy_ +#define PythonMonkey_JSMethodProxy_ + +#include "include/JSFunctionProxy.hh" + +#include + +#include +/** + * @brief The typedef for the backing store that will be used by JSMethodProxy objects. All it contains is a pointer to the JSFunction and a pointer to self + * + */ +typedef struct { + PyObject_HEAD + PyObject *self; + JS::PersistentRootedObject *jsFunc; +} JSMethodProxy; + +/** + * @brief This struct is a bundle of methods used by the JSMethodProxy type + * + */ +struct JSMethodProxyMethodDefinitions { +public: +/** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSFunction before freeing the JSMethodProxy + * + * @param self - The JSMethodProxy to be free'd + */ + static void JSMethodProxy_dealloc(JSMethodProxy *self); + + /** + * @brief New method (.tp_new), creates a new instance of the JSMethodProxy type, exposed as the __new()__ method in python + * + * @param type - The type of object to be created, will always be JSMethodProxyType or a derived type + * @param args - arguments to the __new()__ method, expected to be a JSFunctionProxy, and an object to bind self to + * @param kwds - keyword arguments to the __new()__ method, not used + * @return PyObject* - A new instance of JSMethodProxy + */ + static PyObject *JSMethodProxy_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + + /** + * @brief Call method (.tp_call), called when the JSMethodProxy is called, properly handling `self` and `this` + * + * @param self - the JSMethodProxy being called + * @param args - args to the method + * @param kwargs - keyword args to the method + * @return PyObject* - Result of the method call + */ + static PyObject *JSMethodProxy_call(PyObject *self, PyObject *args, PyObject *kwargs); +}; + +/** + * @brief Struct for the JSMethodProxyType, used by all JSMethodProxy objects + */ +extern PyTypeObject JSMethodProxyType; + +#endif \ No newline at end of file diff --git a/include/JSObjectItemsProxy.hh b/include/JSObjectItemsProxy.hh new file mode 100644 index 00000000..59d16517 --- /dev/null +++ b/include/JSObjectItemsProxy.hh @@ -0,0 +1,133 @@ +/** + * @file JSObjectItemsProxy.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectItemsProxy is a custom C-implemented python type that derives from dict items + * @date 2024-01-19 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSObjectItemsProxy_ +#define PythonMonkey_JSObjectItemsProxy_ + +#include + +#include +#include "include/pyshim.hh" + + +/** + * @brief The typedef for the backing store that will be used by JSObjectItemsProxy objects + * + */ +typedef struct { + _PyDictViewObject dv; +} JSObjectItemsProxy; + +/** + * @brief This struct is a bundle of methods used by the JSObjectItemsProxy type + * + */ +struct JSObjectItemsProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSObjectItemsProxy + * + * @param self - The JSObjectItemsProxy to be free'd + */ + static void JSObjectItemsProxy_dealloc(JSObjectItemsProxy *self); + + /** + * @brief .tp_traverse method + * + * @param self - The JSObjectItemsProxy + * @param visit - The function to be applied on each element of the list + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSObjectItemsProxy_traverse(JSObjectItemsProxy *self, visitproc visit, void *arg); + + /** + * @brief .tp_clear method + * + * @param self - The JSObjectItemsProxy + * @return 0 on success + */ + static int JSObjectItemsProxy_clear(JSObjectItemsProxy *self); + + /** + * @brief Length method (.sq_length), returns the number of key-value pairs in the JSObject, used by the python len() method + * + * @param self - The JSObjectProxy + * @return Py_ssize_t The length of the JSObjectProxy + */ + static Py_ssize_t JSObjectItemsProxy_length(JSObjectItemsProxy *self); + + /** + * @brief Return an iterator object to make JSObjectItemsProxy iterable, emitting (key, value) tuples + * + * @param self - The JSObjectItemsProxy + * @return PyObject* - iterator object + */ + static PyObject *JSObjectItemsProxy_iter(JSObjectItemsProxy *self); + + /** + * @brief Compute a string representation of the JSObjectItemsProxy + * + * @param self - The JSObjectItemsProxy + * @return the string representation (a PyUnicodeObject) on success, NULL on failure + */ + static PyObject *JSObjectItemsProxy_repr(JSObjectItemsProxy *self); + + /** + * @brief reverse iterator method + * + * @param self - The JSObjectItemsProxy + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectItemsProxy_iter_reverse(JSObjectItemsProxy *self); + + /** + * @brief mapping method + * + * @param self - The JSObjectItemsProxy + * @param Py_UNUSED + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectItemsProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)); +}; + +/** + * @brief Struct for the methods that define the Sequence protocol + * + */ +static PySequenceMethods JSObjectItemsProxy_sequence_methods = { + .sq_length = (lenfunc)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_length, + // .sq_contains = TODO tuple support +}; + +PyDoc_STRVAR(items_reversed_keys_doc, + "Return a reverse iterator over the dict keys."); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSObjectItemsProxy_methods[] = { + // {"isdisjoint"}, // TODO tuple support + {"__reversed__", (PyCFunction)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_iter_reverse, METH_NOARGS, items_reversed_keys_doc}, + {NULL, NULL} /* sentinel */ +}; + +static PyGetSetDef JSObjectItemsProxy_getset[] = { + {"mapping", JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_mapping, (setter)NULL, "dictionary that this view refers to", NULL}, + {0} +}; + +/** + * @brief Struct for the JSObjectItemsProxyType, used by all JSObjectItemsProxy objects + */ +extern PyTypeObject JSObjectItemsProxyType; + +#endif \ No newline at end of file diff --git a/include/JSObjectIterProxy.hh b/include/JSObjectIterProxy.hh new file mode 100644 index 00000000..e2246ce0 --- /dev/null +++ b/include/JSObjectIterProxy.hh @@ -0,0 +1,116 @@ +/** + * @file JSObjectIterProxy.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectIterProxy is a custom C-implemented python type that derives from PyDictIterKey + * @date 2024-01-17 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSObjectIterProxy_ +#define PythonMonkey_JSObjectIterProxy_ + + +#include + +#include + +#define KIND_KEYS 0 +#define KIND_VALUES 1 +#define KIND_ITEMS 2 + + +/** + * @brief The typedef for the backing store that will be used by JSObjectIterProxy objects. + * + */ + +typedef struct { + PyObject_HEAD + JS::PersistentRootedIdVector *props; + int it_index; + bool reversed; + int kind; + PyDictObject *di_dict; /* Set to NULL when iterator is exhausted */ +} dictiterobject; + + +typedef struct { + dictiterobject it; +} JSObjectIterProxy; + +/** + * @brief This struct is a bundle of methods used by the JSArrayProxy type + * + */ +struct JSObjectIterProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSArrayProxy + * + * @param self - The JSObjectIterProxy to be free'd + */ + static void JSObjectIterProxy_dealloc(JSObjectIterProxy *self); + + /** + * @brief .tp_traverse method + * + * @param self - The JSObjectIterProxy + * @param visit - The function to be applied on each element of the list + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSObjectIterProxy_traverse(JSObjectIterProxy *self, visitproc visit, void *arg); + + /** + * @brief .tp_clear method + * + * @param self - The JSObjectIterProxy + * @return 0 on success + */ + static int JSObjectIterProxy_clear(JSObjectIterProxy *self); + + /** + * @brief .tp_iter method + * + * @param self - The JSObjectIterProxy + * @return PyObject* - an interator over the iterator + */ + static PyObject *JSObjectIterProxy_iter(JSObjectIterProxy *self); + + /** + * @brief .tp_next method + * + * @param self - The JSObjectIterProxy + * @return PyObject* - next object in iteration + */ + static PyObject *JSObjectIterProxy_nextkey(JSObjectIterProxy *self); + + /** + * @brief length method + * + * @param self - The JSObjectIterProxy + * @return PyObject* - number of objects left to iterate over in iteration + */ + static PyObject *JSObjectIterProxy_len(JSObjectIterProxy *self); +}; + + +PyDoc_STRVAR(dict_length_hint_doc, "Private method returning an estimate of len(list(it))."); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSObjectIterProxy_methods[] = { + {"__length_hint__", (PyCFunction)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_len, METH_NOARGS, dict_length_hint_doc}, + {NULL, NULL} /* sentinel */ +}; + +/** + * @brief Struct for the JSArrayProxyType, used by all JSArrayProxy objects + */ +extern PyTypeObject JSObjectIterProxyType; + +#endif \ No newline at end of file diff --git a/include/JSObjectKeysProxy.hh b/include/JSObjectKeysProxy.hh new file mode 100644 index 00000000..e2a8beef --- /dev/null +++ b/include/JSObjectKeysProxy.hh @@ -0,0 +1,180 @@ +/** + * @file JSObjectKeysProxy.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectKeysProxy is a custom C-implemented python type that derives from dict keys + * @date 2024-01-16 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSObjectKeysProxy_ +#define PythonMonkey_JSObjectKeysProxy_ + +#include + +#include +#include "include/pyshim.hh" + + +/** + * @brief The typedef for the backing store that will be used by JSObjectKeysProxy objects + * + */ +typedef struct { + _PyDictViewObject dv; +} JSObjectKeysProxy; + +/** + * @brief This struct is a bundle of methods used by the JSObjectKeysProxy type + * + */ +struct JSObjectKeysProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSObjectKeysProxy + * + * @param self - The JSObjectKeysProxy to be free'd + */ + static void JSObjectKeysProxy_dealloc(JSObjectKeysProxy *self); + + /** + * @brief .tp_traverse method + * + * @param self - The JSObjectKeysProxy + * @param visit - The function to be applied on each element of the list + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSObjectKeysProxy_traverse(JSObjectKeysProxy *self, visitproc visit, void *arg); + + /** + * @brief .tp_clear method + * + * @param self - The JSObjectKeysProxy + * @return 0 on success + */ + static int JSObjectKeysProxy_clear(JSObjectKeysProxy *self); + + /** + * @brief Length method (.sq_length), returns the number of key-value pairs in the JSObject, used by the python len() method + * + * @param self - The JSObjectProxy + * @return Py_ssize_t The length of the JSObjectProxy + */ + static Py_ssize_t JSObjectKeysProxy_length(JSObjectKeysProxy *self); + + /** + * @brief Test method (.sq_contains), returns whether a key exists, used by the in operator + * + * @param self - The JSObjectKeysProxy + * @param key - The key for the value in the JSObjectKeysProxy + * @return int 1 if `key` is in dict, 0 if not, and -1 on error + */ + static int JSObjectKeysProxy_contains(JSObjectKeysProxy *self, PyObject *key); + + /** + * @brief Comparison method (.tp_richcompare), returns appropriate boolean given a comparison operator and other pyobject + * + * @param self - The JSObjectKeysProxy + * @param other - Any other PyObject + * @param op - Which boolean operator is being performed (Py_EQ for equality, Py_NE for inequality, all other operators are not implemented) + * @return PyObject* - True or false depending on result of comparison + */ + static PyObject *JSObjectKeysProxy_richcompare(JSObjectKeysProxy *self, PyObject *other, int op); + + /** + * @brief Return an iterator object to make JSObjectKeysProxy iterable, emitting (key, value) tuples + * + * @param self - The JSObjectKeysProxy + * @return PyObject* - iterator object + */ + static PyObject *JSObjectKeysProxy_iter(JSObjectKeysProxy *self); + + /** + * @brief Compute a string representation of the JSObjectKeysProxy + * + * @param self - The JSObjectKeysProxy + * @return the string representation (a PyUnicodeObject) on success, NULL on failure + */ + static PyObject *JSObjectKeysProxy_repr(JSObjectKeysProxy *self); + + /** + * @brief Set intersect operation + * + * @param self - The JSObjectKeysProxy + * @param other - The other PyObject to be and'd, expected to be dict or JSObjectKeysProxy + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectKeysProxy_intersect(JSObjectKeysProxy *self, PyObject *other); + + /** + * @brief Set disjoint method + * + * @param self - The JSObjectKeysProxy + * @param other - The other PyObject to be and'd, expected to be dict or JSObjectKeysProxy + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectKeysProxy_isDisjoint(JSObjectKeysProxy *self, PyObject *other); + + /** + * @brief reverse iterator method + * + * @param self - The JSObjectKeysProxy + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectKeysProxy_iter_reverse(JSObjectKeysProxy *self); + + /** + * @brief mapping method + * + * @param self - The JSObjectKeysProxy + * @param Py_UNUSED + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectKeysProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)); +}; + +/** + * @brief Struct for the methods that define the Sequence protocol + * + */ +static PySequenceMethods JSObjectKeysProxy_sequence_methods = { + .sq_length = (lenfunc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_length, + .sq_contains = (objobjproc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_contains +}; + +static PyNumberMethods JSObjectKeysProxy_number_methods = { + // .nb_subtract = default is fine + .nb_and = (binaryfunc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_intersect, + // .nb_xor = default is fine + // .nb_or = default is fine +}; + +PyDoc_STRVAR(isdisjoint_doc, + "Return True if the view and the given iterable have a null intersection."); + +PyDoc_STRVAR(reversed_keys_doc, + "Return a reverse iterator over the dict keys."); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSObjectKeysProxy_methods[] = { + {"isdisjoint", (PyCFunction)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_isDisjoint, METH_O, isdisjoint_doc}, + {"__reversed__", (PyCFunction)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_iter_reverse, METH_NOARGS, reversed_keys_doc}, + {NULL, NULL} /* sentinel */ +}; + +static PyGetSetDef JSObjectKeysProxy_getset[] = { + {"mapping", JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_mapping, (setter)NULL, "dictionary that this view refers to", NULL}, + {0} +}; + +/** + * @brief Struct for the JSObjectKeysProxyType, used by all JSObjectKeysProxy objects + */ +extern PyTypeObject JSObjectKeysProxyType; + +#endif \ No newline at end of file diff --git a/include/JSObjectProxy.hh b/include/JSObjectProxy.hh index d66ac0d5..2179be80 100644 --- a/include/JSObjectProxy.hh +++ b/include/JSObjectProxy.hh @@ -1,27 +1,30 @@ /** * @file JSObjectProxy.hh - * @author Caleb Aikens (caleb@distributive.network) & Tom Tang (xmader@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief JSObjectProxy is a custom C-implemented python type that derives from dict. It acts as a proxy for JSObjects from Spidermonkey, and behaves like a dict would. - * @version 0.1 * @date 2023-06-26 * - * Copyright (c) 2023 Distributive Corp. + * @copyright Copyright (c) 2023 Distributive Corp. * */ +#ifndef PythonMonkey_JSObjectProxy_ +#define PythonMonkey_JSObjectProxy_ + #include #include #include + /** * @brief The typedef for the backing store that will be used by JSObjectProxy objects. All it contains is a pointer to the JSObject * */ typedef struct { PyDictObject dict; - JS::RootedObject jsObject; + JS::PersistentRootedObject *jsObject; } JSObjectProxy; /** @@ -38,41 +41,39 @@ public: static void JSObjectProxy_dealloc(JSObjectProxy *self); /** - * @brief New method (.tp_new), creates a new instance of the JSObjectProxy type, exposed as the __new()__ method in python + * @brief Length method (.mp_length), returns the number of key-value pairs in the JSObject, used by the python len() method * - * @param type - The type of object to be created, will always be JSObjectProxyType or a derived type - * @param args - arguments to the __new()__ method, not used - * @param kwds - keyword arguments to the __new()__ method, not used - * @return PyObject* - A new instance of JSObjectProxy + * @param self - The JSObjectProxy + * @return Py_ssize_t The length of the JSObjectProxy */ - static PyObject *JSObjectProxy_new(PyTypeObject *type, PyObject *args, PyObject *kwds); + static Py_ssize_t JSObjectProxy_length(JSObjectProxy *self); /** - * @brief Initialization method (.tp_init), initializes a newly created instance of JSObjectProxy. exposed as the __init()__ method in python + * @brief Getter method, returns a value from the JSObjectProxy given a key, used by several built-in python methods as well as the . operator * - * @param self - The JSObjectProxy to be initialized - * @param args - arguments to the __init()__ method, expected to be a dict - * @param kwds - keyword arguments to the __init()__ method, not used - * @return int - -1 on exception, return any other value otherwise + * @param self - The JSObjectProxy + * @param key - The key for the value in the JSObjectProxy + * @return PyObject* NULL on exception, the corresponding value otherwise */ - static int JSObjectProxy_init(JSObjectProxy *self, PyObject *args, PyObject *kwds); + static PyObject *JSObjectProxy_get(JSObjectProxy *self, PyObject *key); /** - * @brief Length method (.mp_length), returns the number of key-value pairs in the JSObject, used by the python len() method + * @brief Getter method (.mp_subscript), returns a value from the JSObjectProxy given a key, used by the [] operator * * @param self - The JSObjectProxy - * @return Py_ssize_t The length of the JSObjectProxy + * @param key - The key for the value in the JSObjectProxy + * @return PyObject* NULL on exception, the corresponding value otherwise */ - static Py_ssize_t JSObjectProxy_length(JSObjectProxy *self); + static PyObject *JSObjectProxy_get_subscript(JSObjectProxy *self, PyObject *key); /** - * @brief Getter method (.mp_subscript), returns a value from the JSObjectProxy given a key, used by several built-in python methods as well as the [] operator + * @brief Test method (.sq_contains), returns whether a key exists, used by the in operator * * @param self - The JSObjectProxy * @param key - The key for the value in the JSObjectProxy - * @return PyObject* NULL on exception, the corresponding value otherwise + * @return int 1 if `key` is in dict, 0 if not, and -1 on error */ - static PyObject *JSObjectProxy_get(JSObjectProxy *self, PyObject *key); + static int JSObjectProxy_contains(JSObjectProxy *self, PyObject *key); /** * @brief Assign method (.mp_ass_subscript), assigns a key-value pair if value is non-NULL, or deletes a key-value pair if value is NULL @@ -84,15 +85,6 @@ public: */ static int JSObjectProxy_assign(JSObjectProxy *self, PyObject *key, PyObject *value); - /** - * @brief Helper function for various JSObjectProxy methods, sets a key-value pair on a JSObject given a python string key and a JS::Value value - * - * @param jsObject - The underlying backing store JSObject for the JSObjectProxy - * @param key - The key to be assigned or deleted - * @param value - The JS::Value to be assigned - */ - static void JSObjectProxy_set_helper(JS::HandleObject jsObject, PyObject *key, JS::HandleValue value); - /** * @brief Comparison method (.tp_richcompare), returns appropriate boolean given a comparison operator and other pyobject * @@ -111,6 +103,7 @@ public: * @param visited * @return bool - Whether the compared objects are equal or not */ + // private static bool JSObjectProxy_richcompare_helper(JSObjectProxy *self, PyObject *other, std::unordered_map &visited); /** @@ -121,6 +114,14 @@ public: */ static PyObject *JSObjectProxy_iter(JSObjectProxy *self); + /** + * @brief Implements next operator function + * + * @param self - The JSObjectProxy + * @return PyObject* - call result + */ + static PyObject *JSObjectProxy_iter_next(JSObjectProxy *self); + /** * @brief Compute a string representation of the JSObjectProxy * @@ -128,20 +129,222 @@ public: * @return the string representation (a PyUnicodeObject) on success, NULL on failure */ static PyObject *JSObjectProxy_repr(JSObjectProxy *self); + + /** + * @brief Set union operation + * + * @param self - The JSObjectProxy + * @param other - The other PyObject to be or'd, expected to be dict or JSObjectProxy + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectProxy_or(JSObjectProxy *self, PyObject *other); + + /** + * @brief Set union operation, in place + * + * @param self - The JSObjectProxy + * @param other - The other PyObject to be or'd, expected to be dict or JSObjectProxy + * @return PyObject* The resulting new dict, must be same object as self + */ + static PyObject *JSObjectProxy_ior(JSObjectProxy *self, PyObject *other); + + /** + * @brief get method + * + * @param self - The JSObjectProxy + * @param args - arguments to the method + * @param nargs - number of args to the method + * @return PyObject* the value for key if first arg key is in the dictionary, else second arg default + */ + static PyObject *JSObjectProxy_get_method(JSObjectProxy *self, PyObject *const *args, Py_ssize_t nargs); + + /** + * @brief setdefault method + * + * @param self - The JSObjectProxy + * @param args - arguments to the method + * @param nargs - number of args to the method + * @return PyObject* the value for key if first arg key is in the dictionary, else second default + */ + static PyObject *JSObjectProxy_setdefault_method(JSObjectProxy *self, PyObject *const *args, Py_ssize_t nargs); + + /** + * @brief pop method + * + * @param self - The JSObjectProxy + * @param args - arguments to the method + * @param nargs - number of args to the method + * @return PyObject* If the first arg key is not found, return the second arg default if given; otherwise raise a KeyError + */ + static PyObject *JSObjectProxy_pop_method(JSObjectProxy *self, PyObject *const *args, Py_ssize_t nargs); + + /** + * @brief clear method + * + * @param self - The JSObjectProxy + * @return None + */ + static PyObject *JSObjectProxy_clear_method(JSObjectProxy *self); + + /** + * @brief copy method + * + * @param self - The JSObjectProxy + * @return PyObject* copy of the dict + */ + static PyObject *JSObjectProxy_copy_method(JSObjectProxy *self); + + /** + * @brief update method update the dict with another dict or iterable + * + * @param self - The JSObjectProxy + * @param args - arguments to the sort method + * @param kwds - keyword arguments to the sort method (key-value pairs to be updated in the dict) + * @return None + */ + static PyObject *JSObjectProxy_update_method(JSObjectProxy *self, PyObject *args, PyObject *kwds); + + /** + * @brief keys method + * + * @param self - The JSObjectProxy + * @return PyObject* keys of the dict + */ + static PyObject *JSObjectProxy_keys_method(JSObjectProxy *self); + + /** + * @brief values method + * + * @param self - The JSObjectProxy + * @return PyObject* values view of the dict + */ + static PyObject *JSObjectProxy_values_method(JSObjectProxy *self); + + /** + * @brief items method + * + * @param self - The JSObjectProxy + * @return PyObject* items view of the dict + */ + static PyObject *JSObjectProxy_items_method(JSObjectProxy *self); + + /** + * @brief tp_traverse + * + * @param self - The JSObjectProxy + * @param visit - The function to be applied on each element of the object + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSObjectProxy_traverse(JSObjectProxy *self, visitproc visit, void *arg); + + /** + * @brief tp_clear + * + * @param self - The JSObjectProxy + * @return 0 on success + */ + static int JSObjectProxy_clear(JSObjectProxy *self); }; +// docs for methods, copied from cpython +PyDoc_STRVAR(getitem__doc__, + "__getitem__($self, key, /)\n--\n\nReturn self[key]."); + +PyDoc_STRVAR(dict_get__doc__, + "get($self, key, default=None, /)\n" + "--\n" + "\n" + "Return the value for key if key is in the dictionary, else default."); + +PyDoc_STRVAR(dict_setdefault__doc__, + "setdefault($self, key, default=None, /)\n" + "--\n" + "\n" + "Insert key with a value of default if key is not in the dictionary.\n" + "\n" + "Return the value for key if key is in the dictionary, else default."); + +PyDoc_STRVAR(dict_pop__doc__, + "pop($self, key, default=, /)\n" + "--\n" + "\n" + "D.pop(k[,d]) -> v, remove specified key and return the corresponding value.\n" + "\n" + "If the key is not found, return the default if given; otherwise,\n" + "raise a KeyError."); + +PyDoc_STRVAR(clear__doc__, + "D.clear() -> None. Remove all items from D."); + +PyDoc_STRVAR(copy__doc__, + "D.copy() -> a shallow copy of D"); + +PyDoc_STRVAR(keys__doc__, + "D.keys() -> a set-like object providing a view on D's keys"); +PyDoc_STRVAR(items__doc__, + "D.items() -> a set-like object providing a view on D's items"); +PyDoc_STRVAR(values__doc__, + "D.values() -> an object providing a view on D's values"); + +PyDoc_STRVAR(update__doc__, + "D.update([E, ]**F) -> None. Update D from dict/iterable E and F.\n\ +If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]\n\ +If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v\n\ +In either case, this is followed by: for k in F: D[k] = F[k]"); + +PyDoc_STRVAR(dict_keys__doc__, + "D.keys() -> a set-like object providing a view on D's keys"); +PyDoc_STRVAR(dict_items__doc__, + "D.items() -> a set-like object providing a view on D's items"); +PyDoc_STRVAR(dict_values__doc__, + "D.values() -> an object providing a view on D's values"); + /** * @brief Struct for the methods that define the Mapping protocol * */ static PyMappingMethods JSObjectProxy_mapping_methods = { .mp_length = (lenfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_length, - .mp_subscript = (binaryfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_get, + .mp_subscript = (binaryfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_get_subscript, .mp_ass_subscript = (objobjargproc)JSObjectProxyMethodDefinitions::JSObjectProxy_assign }; +/** + * @brief Struct for the methods that define the Sequence protocol + * + */ +static PySequenceMethods JSObjectProxy_sequence_methods = { + .sq_contains = (objobjproc)JSObjectProxyMethodDefinitions::JSObjectProxy_contains +}; + +static PyNumberMethods JSObjectProxy_number_methods = { + .nb_or = (binaryfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_or, + .nb_inplace_or = (binaryfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_ior +}; + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSObjectProxy_methods[] = { + {"setdefault", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_setdefault_method, METH_FASTCALL, dict_setdefault__doc__}, + {"__getitem__", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_get, METH_O | METH_COEXIST, getitem__doc__}, + {"get", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_get_method, METH_FASTCALL, dict_get__doc__}, + {"pop", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_pop_method, METH_FASTCALL, dict_pop__doc__}, + {"clear", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_clear_method, METH_NOARGS, clear__doc__}, + {"copy", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_copy_method, METH_NOARGS, copy__doc__}, + {"update", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_update_method, METH_VARARGS | METH_KEYWORDS, update__doc__}, + {"keys", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_keys_method, METH_NOARGS, dict_keys__doc__}, + {"items", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_items_method, METH_NOARGS, dict_items__doc__}, + {"values", (PyCFunction)JSObjectProxyMethodDefinitions::JSObjectProxy_values_method, METH_NOARGS, dict_values__doc__}, + {NULL, NULL} /* sentinel */ +}; + /** * @brief Struct for the JSObjectProxyType, used by all JSObjectProxy objects */ extern PyTypeObject JSObjectProxyType; + +#endif \ No newline at end of file diff --git a/include/JSObjectValuesProxy.hh b/include/JSObjectValuesProxy.hh new file mode 100644 index 00000000..17c2af23 --- /dev/null +++ b/include/JSObjectValuesProxy.hh @@ -0,0 +1,141 @@ +/** + * @file JSObjectValuesProxy.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectValuesProxy is a custom C-implemented python type that derives from dict values + * @date 2024-01-17 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSObjectValuesProxy_ +#define PythonMonkey_JSObjectValuesProxy_ + +#include + +#include +#include "include/pyshim.hh" + + +/** + * @brief The typedef for the backing store that will be used by JSObjectValuesProxy objects + * + */ +typedef struct { + _PyDictViewObject dv; +} JSObjectValuesProxy; + +/** + * @brief This struct is a bundle of methods used by the JSObjectValuesProxy type + * + */ +struct JSObjectValuesProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSObject before freeing the JSObjectValuesProxy + * + * @param self - The JSObjectValuesProxy to be free'd + */ + static void JSObjectValuesProxy_dealloc(JSObjectValuesProxy *self); + + /** + * @brief .tp_traverse method + * + * @param self - The JSObjectValuesProxy + * @param visit - The function to be applied on each element of the list + * @param arg - The argument to the visit function + * @return 0 on success + */ + static int JSObjectValuesProxy_traverse(JSObjectValuesProxy *self, visitproc visit, void *arg); + + /** + * @brief .tp_clear method + * + * @param self - The JSObjectValuesProxy + * @return 0 on success + */ + static int JSObjectValuesProxy_clear(JSObjectValuesProxy *self); + + /** + * @brief Length method (.sq_length), returns the number of key-value pairs in the JSObject, used by the python len() method + * + * @param self - The JSObjectProxy + * @return Py_ssize_t The length of the JSObjectProxy + */ + static Py_ssize_t JSObjectValuesProxy_length(JSObjectValuesProxy *self); + + /** + * @brief Test method (.sq_contains), returns whether a key exists, used by the in operator + * + * @param self - The JSObjectValuesProxy + * @param key - The key for the value in the JSObjectValuesProxy + * @return int 1 if `key` is in dict, 0 if not, and -1 on error + */ + static int JSObjectValuesProxy_contains(JSObjectValuesProxy *self, PyObject *key); + + /** + * @brief Return an iterator object to make JSObjectValuesProxy iterable, emitting (key, value) tuples + * + * @param self - The JSObjectValuesProxy + * @return PyObject* - iterator object + */ + static PyObject *JSObjectValuesProxy_iter(JSObjectValuesProxy *self); + + /** + * @brief Compute a string representation of the JSObjectValuesProxy + * + * @param self - The JSObjectValuesProxy + * @return the string representation (a PyUnicodeObject) on success, NULL on failure + */ + static PyObject *JSObjectValuesProxy_repr(JSObjectValuesProxy *self); + + /** + * @brief reverse iterator method + * + * @param self - The JSObjectValuesProxy + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectValuesProxy_iter_reverse(JSObjectValuesProxy *self); + + /** + * @brief mapping method + * + * @param self - The JSObjectValuesProxy + * @param Py_UNUSED + * @return PyObject* The resulting new dict + */ + static PyObject *JSObjectValuesProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)); +}; + +/** + * @brief Struct for the methods that define the Sequence protocol + * + */ +static PySequenceMethods JSObjectValuesProxy_sequence_methods = { + .sq_length = (lenfunc)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_length, + .sq_contains = (objobjproc)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_contains +}; + +PyDoc_STRVAR(reversed_values_doc, + "Return a reverse iterator over the dict values."); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSObjectValuesProxy_methods[] = { + {"__reversed__", (PyCFunction)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_iter_reverse, METH_NOARGS, reversed_values_doc}, + {NULL, NULL} /* sentinel */ +}; + +static PyGetSetDef JSObjectValuesProxy_getset[] = { + {"mapping", JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_mapping, (setter)NULL, "dictionary that this view refers to", NULL}, + {0} +}; + +/** + * @brief Struct for the JSObjectValuesProxyType, used by all JSObjectValuesProxy objects + */ +extern PyTypeObject JSObjectValuesProxyType; + +#endif \ No newline at end of file diff --git a/include/JSStringProxy.hh b/include/JSStringProxy.hh new file mode 100644 index 00000000..ed696c19 --- /dev/null +++ b/include/JSStringProxy.hh @@ -0,0 +1,79 @@ +/** + * @file JSStringProxy.hh + * @author Caleb Aikens (caleb@distributive.network) + * @brief JSStringProxy is a custom C-implemented python type that derives from str. It acts as a proxy for JSStrings from Spidermonkey, and behaves like a str would. + * @date 2024-01-03 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_JSStringProxy_ +#define PythonMonkey_JSStringProxy_ + +#include + +#include + +#include + +/** + * @brief The typedef for the backing store that will be used by JSStringProxy objects. All it contains is a pointer to the JSString + * + */ +typedef struct { + PyUnicodeObject str; + JS::PersistentRootedValue *jsString; +} JSStringProxy; + +extern std::unordered_set jsStringProxies; // a collection of all JSStringProxy objects, used during a GCCallback to ensure they continue to point to the correct char buffer + +/** + * @brief This struct is a bundle of methods used by the JSStringProxy type + * + */ +struct JSStringProxyMethodDefinitions { +public: + /** + * @brief Deallocation method (.tp_dealloc), removes the reference to the underlying JSString before freeing the JSStringProxy + * + * @param self - The JSStringProxy to be free'd + */ + static void JSStringProxy_dealloc(JSStringProxy *self); + + /** + * @brief copy protocol method for both copy and deepcopy + * + * @param self - The JSObjectProxy + * @return a copy of the string + */ + static PyObject *JSStringProxy_copy_method(JSStringProxy *self); +}; + +// docs for methods, copied from cpython +PyDoc_STRVAR(stringproxy_deepcopy__doc__, + "__deepcopy__($self, memo, /)\n" + "--\n" + "\n"); + +PyDoc_STRVAR(stringproxy_copy__doc__, + "__copy__($self, /)\n" + "--\n" + "\n"); + +/** + * @brief Struct for the other methods + * + */ +static PyMethodDef JSStringProxy_methods[] = { + {"__deepcopy__", (PyCFunction)JSStringProxyMethodDefinitions::JSStringProxy_copy_method, METH_O, stringproxy_deepcopy__doc__}, // ignores any memo argument + {"__copy__", (PyCFunction)JSStringProxyMethodDefinitions::JSStringProxy_copy_method, METH_NOARGS, stringproxy_copy__doc__}, + {NULL, NULL} /* sentinel */ +}; + +/** + * @brief Struct for the JSStringProxyType, used by all JSStringProxy objects + */ +extern PyTypeObject JSStringProxyType; + +#endif \ No newline at end of file diff --git a/include/JobQueue.hh b/include/JobQueue.hh index 5d10e9a7..36734f92 100644 --- a/include/JobQueue.hh +++ b/include/JobQueue.hh @@ -1,11 +1,10 @@ /** * @file JobQueue.hh * @author Tom Tang (xmader@distributive.network) - * @brief Implement the ECMAScript Job Queue - * @version 0.1 + * @brief Implements the ECMAScript Job Queue * @date 2023-04-03 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023 Distributive Corp. * */ @@ -23,20 +22,34 @@ * @see https://hg.mozilla.org/releases/mozilla-esr102/file/5741ffa/js/public/Promise.h#l22 */ class JobQueue : public JS::JobQueue { -// -// JS::JobQueue methods. -// + public: +explicit JobQueue(JSContext *cx); ~JobQueue() = default; /** - * @brief Ask the embedding for the incumbent global. + * @brief Initialize PythonMonkey's event-loop job queue + * @param cx - javascript context pointer + * @return success + */ +bool init(JSContext *cx); + +/** + * @brief Ask the embedding for the host defined data. * - * SpiderMonkey doesn't itself have a notion of incumbent globals as defined + * SpiderMonkey doesn't itself have a notion of host defined data as defined * by the HTML spec, so we need the embedding to provide this. See * dom/script/ScriptSettings.h for details. + * + * If the embedding has the host defined data, this method should return the + * host defined data via the `data` out parameter and return `true`. + * The object in the `data` out parameter can belong to any compartment. + * If the embedding doesn't need the host defined data, this method should + * set the `data` out parameter to `nullptr` and return `true`. + * If any error happens while generating the host defined data, this method + * should set a pending exception to `cx` and return `false`. */ -JSObject *getIncumbentGlobal(JSContext *cx) override; +bool getHostDefinedData(JSContext *cx, JS::MutableHandle data) const override; /** * @brief Enqueue a reaction job `job` for `promise`, which was allocated at @@ -73,7 +86,32 @@ void runJobs(JSContext *cx) override; */ bool empty() const override; +/** + * @return true if the job queue stopped draining, which results in `empty()` being false after `runJobs()`. + */ +bool isDrainingStopped() const override; + +/** + * @brief Appends a callback to the queue of FinalizationRegistry callbacks + * + * @param callback - the callback to be queue'd + */ +void queueFinalizationRegistryCallback(JSFunction *callback); + +/** + * @brief Runs the accumulated queue of FinalizationRegistry callbacks + * + * @param cx - Pointer to the JSContext + * @return true - at least 1 callback was called + * @return false - no callbacks were called + */ +bool runFinalizationRegistryCallbacks(JSContext *cx); + private: + +using FunctionVector = JS::GCVector; +JS::PersistentRooted *finalizationRegistryCallbacks; + /** * @brief Capture this JobQueue's current job queue as a SavedJobQueue and return it, * leaving the JobQueue's job queue empty. Destroying the returned object @@ -85,18 +123,6 @@ private: */ js::UniquePtr saveJobQueue(JSContext *) override; -// -// Custom methods -// -public: -/** - * @brief Initialize PythonMonkey's event-loop job queue - * @param cx - javascript context pointer - * @return success - */ -bool init(JSContext *cx); - -private: /** * @brief The callback for dispatching an off-thread promise to the event loop * see https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/public/Promise.h#l580 @@ -106,7 +132,22 @@ private: * @return not shutting down */ static bool dispatchToEventLoop(void *closure, JS::Dispatchable *dispatchable); -}; + +/** + * @brief The callback that gets invoked whenever a Promise is rejected without a rejection handler (uncaught/unhandled exception) + * see https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/public/Promise.h#l268 + * https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/Runtime.cpp#l600 + * @param promise - The Promise object + * @param state - Is the Promise unhandled? + * @param mutedErrors - When the `mutedErrors` option in `pm.eval` is set to true, unhandled rejections are ignored ("muted"). + * See also https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/public/CompileOptions.h#l129 + * @param privateData - unused + */ +static void promiseRejectionTracker(JSContext *cx, bool mutedErrors, + JS::HandleObject promise, JS::PromiseRejectionHandlingState state, + void *privateData); + +}; // class /** * @brief Send job to the Python event-loop on main thread diff --git a/include/ListType.hh b/include/ListType.hh index 061212a8..88d27032 100644 --- a/include/ListType.hh +++ b/include/ListType.hh @@ -1,65 +1,28 @@ /** * @file ListType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing python lists - * @version 0.1 * @date 2022-08-18 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ #ifndef PythonMonkey_ListType_ #define PythonMonkey_ListType_ -#include "PyType.hh" -#include "TypeEnum.hh" +#include #include + /** - * @brief This struct represents a list in python. It derives from the PyType struct + * @brief This struct represents a list in python * * @author Giovanni */ -struct ListType : public PyType { +struct ListType { public: - ListType(); - ListType(PyObject *object); - const TYPE returnType = TYPE::LIST; -/** - * @brief - * - * - * @param index The index of the list item - * @param value The value of the list item - */ - void set(int index, PyType *value); - -/** - * @brief Gets the list item at the given index - * - * @param index The index of the item in question - * @return PyType* Returns a pointer to the appropriate PyType object - */ - PyType *get(int index) const; - -/** - * @brief Appends the given value to the list - * - * @param value The item to be appended - */ - void append(PyType *value); - -/** - * @brief - * - * - * - * @returns int length of the list - */ - int len() const; - - void sort(); + static PyObject *getPyObject(JSContext *cx, JS::HandleObject arrayObj); }; #endif \ No newline at end of file diff --git a/include/NoneType.hh b/include/NoneType.hh index a2f7a82d..84aa66a3 100644 --- a/include/NoneType.hh +++ b/include/NoneType.hh @@ -1,27 +1,25 @@ /** * @file NoneType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing None - * @version 0.1 * @date 2023-02-22 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #ifndef PythonMonkey_NoneType_ #define PythonMonkey_NoneType_ -#include "PyType.hh" -#include "TypeEnum.hh" +#include /** - * @brief This struct represents the 'None' type in Python. It inherits from the PyType struct + * @brief This struct represents the 'None' type in Python */ -struct NoneType : public PyType { +struct NoneType { public: - NoneType(); - TYPE returnType = TYPE::NONE; + static PyObject *getPyObject(); + }; #endif \ No newline at end of file diff --git a/include/NullType.hh b/include/NullType.hh index 80b91e20..5ce810b5 100644 --- a/include/NullType.hh +++ b/include/NullType.hh @@ -1,27 +1,24 @@ /** * @file NullType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing JS null in a python object - * @version 0.1 * @date 2023-02-22 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #ifndef PythonMonkey_NullType_ #define PythonMonkey_NullType_ -#include "PyType.hh" -#include "TypeEnum.hh" +#include /** - * @brief This struct represents the JS null type in Python using a singleton object on the pythonmonkey module. It inherits from the PyType struct + * @brief This struct represents the JS null type in Python using a singleton object on the pythonmonkey module */ -struct NullType : public PyType { +struct NullType { public: - NullType(); - const TYPE returnType = TYPE::PYTHONMONKEY_NULL; + static PyObject *getPyObject(); }; #endif \ No newline at end of file diff --git a/include/PromiseType.hh b/include/PromiseType.hh index cc8381f1..880c2b3a 100644 --- a/include/PromiseType.hh +++ b/include/PromiseType.hh @@ -1,48 +1,43 @@ /** * @file PromiseType.hh - * @author Tom Tang (xmader@distributive.network) + * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing Promises - * @version 0.1 * @date 2023-03-29 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #ifndef PythonMonkey_PromiseType_ #define PythonMonkey_PromiseType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include #include /** - * @brief This struct represents the JS Promise type in Python using our custom pythonmonkey.promise type. It inherits from the PyType struct + * @brief This struct represents the JS Promise type in Python using our custom pythonmonkey.promise type */ -struct PromiseType : public PyType { +struct PromiseType { public: - PromiseType(PyObject *object); - /** * @brief Construct a new PromiseType object from a JS::PromiseObject. * * @param cx - javascript context pointer * @param promise - JS::PromiseObject to be coerced + * + * @returns PyObject* pointer to the resulting PyObject */ - PromiseType(JSContext *cx, JS::HandleObject promise); - - const TYPE returnType = TYPE::PYTHONMONKEY_PROMISE; + static PyObject *getPyObject(JSContext *cx, JS::HandleObject promise); /** * @brief Convert a Python [awaitable](https://docs.python.org/3/library/asyncio-task.html#awaitables) object to JS Promise * * @param cx - javascript context pointer + * @param pyObject - the python awaitable to be converted */ - JSObject *toJsPromise(JSContext *cx); + static JSObject *toJsPromise(JSContext *cx, PyObject *pyObject); }; /** diff --git a/include/PyBaseProxyHandler.hh b/include/PyBaseProxyHandler.hh new file mode 100644 index 00000000..637a0300 --- /dev/null +++ b/include/PyBaseProxyHandler.hh @@ -0,0 +1,53 @@ +/** + * @file PyBaseProxyHandler.hh + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Structs for creating JS proxy objects. + * @date 2023-04-20 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_PyBaseProxy_ +#define PythonMonkey_PyBaseProxy_ + +#include +#include +#include +#include + +#include + +/** + * @brief base class for PyDictProxyHandler and PyListProxyHandler + */ +struct PyBaseProxyHandler : public js::BaseProxyHandler { +public: + PyBaseProxyHandler(const void *family) : js::BaseProxyHandler(family) {}; + + bool getPrototypeIfOrdinary(JSContext *cx, JS::HandleObject proxy, bool *isOrdinary, JS::MutableHandleObject protop) const override final; + bool preventExtensions(JSContext *cx, JS::HandleObject proxy, JS::ObjectOpResult &result) const override final; + bool isExtensible(JSContext *cx, JS::HandleObject proxy, bool *extensible) const override final; +}; + +enum ProxySlots {PyObjectSlot, OtherSlot}; + +typedef struct { + const char *name; /* The name of the method */ + JSNative call; /* The C function that implements it */ + uint16_t nargs; /* The argument count for the method */ +} JSMethodDef; + +/** + * @brief Convert jsid to a PyObject to be used as dict keys + */ +PyObject *idToKey(JSContext *cx, JS::HandleId id); + +/** + * @brief Convert Python dict key to jsid + */ +bool keyToId(PyObject *key, JS::MutableHandleId idp); + +bool idToIndex(JSContext *cx, JS::HandleId id, Py_ssize_t *index); + +#endif \ No newline at end of file diff --git a/include/PyBytesProxyHandler.hh b/include/PyBytesProxyHandler.hh new file mode 100644 index 00000000..1c7126f9 --- /dev/null +++ b/include/PyBytesProxyHandler.hh @@ -0,0 +1,57 @@ +/** + * @file PyBytesProxyHandler.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS Uint8Array-like proxy objects for immutable bytes objects + * @date 2024-07-23 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_PyBytesProxy_ +#define PythonMonkey_PyBytesProxy_ + + +#include "include/PyObjectProxyHandler.hh" + + +/** + * @brief This struct is the ProxyHandler for JS Proxy Iterable pythonmonkey creates to handle coercion from python iterables to JS Objects + * + */ +struct PyBytesProxyHandler : public PyObjectProxyHandler { +public: + PyBytesProxyHandler() : PyObjectProxyHandler(&family) {}; + static const char family; + + /** + * @brief [[Set]] + * + * @param cx pointer to JSContext + * @param proxy The proxy object who's property we wish to set + * @param id Key of the property we wish to set + * @param v Value that we wish to set the property to + * @param receiver The `this` value to use when executing any code + * @param result whether or not the call succeeded + * @return true call succeed + * @return false call failed and an exception has been raised + */ + bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::HandleValue v, JS::HandleValue receiver, + JS::ObjectOpResult &result) const override; + + bool getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc + ) const override; + + /** + * @brief Handles python object reference count when JS Proxy object is finalized + * + * @param gcx pointer to JS::GCContext + * @param proxy the proxy object being finalized + */ + void finalize(JS::GCContext *gcx, JSObject *proxy) const override; +}; + +#endif \ No newline at end of file diff --git a/include/PyDictProxyHandler.hh b/include/PyDictProxyHandler.hh new file mode 100644 index 00000000..cb608660 --- /dev/null +++ b/include/PyDictProxyHandler.hh @@ -0,0 +1,126 @@ +/** + * @file PyDictProxyHandler.hh + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects for Dicts + * @date 2023-04-20 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_PyDictProxy_ +#define PythonMonkey_PyDictProxy_ + +#include "include/PyObjectProxyHandler.hh" + + +/** + * @brief This struct is the ProxyHandler for JS Proxy Objects pythonmonkey creates to handle coercion from python dicts to JS Objects + * + */ +struct PyDictProxyHandler : public PyObjectProxyHandler { +public: + PyDictProxyHandler() : PyObjectProxyHandler(&family) {}; + static const char family; + + /** + * @brief [[OwnPropertyKeys]] + * + * @param cx - pointer to JSContext + * @param proxy - The proxy object who's keys we output + * @param props - out-parameter of object IDs + * @return true - call succeeded + * @return false - call failed and an exception has been raised + */ + bool ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const override; + /** + * @brief [[Delete]] + * + * @param cx - pointer to JSContext + * @param proxy - The proxy object who's property we wish to delete + * @param id - The key we wish to delete + * @param result - whether the call succeeded or not + * @return true - call succeeded + * @return false - call failed and an exception has been raised + */ + bool delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::ObjectOpResult &result) const override; + /** + * @brief [[HasProperty]] + * @param cx - pointer to JSContext + * @param proxy - The proxy object who's property we wish to check + * @param id - key value of the property to check + * @param bp - out-paramter: true if object has property, false if not + * @return true - call succeeded + * @return false - call failed and an exception has been raised + */ + bool has(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + bool *bp) const override; + /** + * @brief [[Set]] + * + * @param cx pointer to JSContext + * @param proxy The proxy object who's property we wish to set + * @param id Key of the property we wish to set + * @param v Value that we wish to set the property to + * @param receiver The `this` value to use when executing any code + * @param result whether or not the call succeeded + * @return true call succeed + * @return false call failed and an exception has been raised + */ + bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::HandleValue v, JS::HandleValue receiver, + JS::ObjectOpResult &result) const override; + /** + * @brief [[Enumerate]] + * + * @param cx - pointer to JSContext + * @param proxy - The proxy object who's keys we output + * @param props - out-parameter of object IDs + * @return true - call succeeded + * @return false - call failed and an exception has been raised + */ + bool enumerate(JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const override; + + /** + * @brief Returns true if `id` is in `proxy`, false otherwise + * + * @param cx pointer to JSContext + * @param proxy The proxy object who's property we wish to check + * @param id Key of the property we wish to check + * @param bp out-paramter: true if object has property, false if not + * @return true call succeeded + * @return false call failed and an exception has been raised + */ + bool hasOwn(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + bool *bp) const override; + + /** + * @brief Returns vector of proxy's own keys + * + * @param cx - Pointer to the JSContext + * @param proxy - the proxy object + * @param props - out parameter, the vector of proxy's own keys + * @return true - the call succeeded + * @return false - the call failed and an exception has been raised + */ + bool getOwnEnumerablePropertyKeys( + JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const override; + + bool getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc + ) const override; + + bool defineProperty(JSContext *cx, JS::HandleObject proxy, + JS::HandleId id, + JS::Handle desc, + JS::ObjectOpResult &result) const override; + + bool getBuiltinClass(JSContext *cx, JS::HandleObject proxy, js::ESClass *cls) const override; +}; + +#endif diff --git a/include/PyEventLoop.hh b/include/PyEventLoop.hh index 36fdb03c..a4c643f2 100644 --- a/include/PyEventLoop.hh +++ b/include/PyEventLoop.hh @@ -2,10 +2,9 @@ * @file PyEventLoop.hh * @author Tom Tang (xmader@distributive.network) * @brief Send jobs to the Python event-loop - * @version 0.1 * @date 2023-04-05 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023 Distributive Corp. * */ @@ -13,6 +12,7 @@ #define PythonMonkey_PyEventLoop_ #include +#include #include #include #include @@ -32,32 +32,50 @@ public: * @see https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Handle */ struct AsyncHandle { + using id_t = uint32_t; public: explicit AsyncHandle(PyObject *handle) : _handle(handle) {}; AsyncHandle(const AsyncHandle &old) = delete; // forbid copy-initialization - AsyncHandle(AsyncHandle &&old) : _handle(std::exchange(old._handle, nullptr)) {}; // clear the moved-from object + AsyncHandle(AsyncHandle &&old) : _handle(std::exchange(old._handle, nullptr)), _refed(old._refed.exchange(false)), _debugInfo(std::exchange(old._debugInfo, nullptr)) {}; // clear the moved-from object ~AsyncHandle() { if (Py_IsInitialized()) { // the Python runtime has already been finalized when `_timeoutIdMap` is cleared at exit Py_XDECREF(_handle); } } + /** + * @brief Create a new `AsyncHandle` without an associated `asyncio.Handle` Python object + * @return the timeoutId + */ + static inline id_t newEmpty() { + auto handle = AsyncHandle(Py_None); + return AsyncHandle::getUniqueId(std::move(handle)); + } + /** * @brief Cancel the scheduled event-loop job. * If the job has already been canceled or executed, this method has no effect. */ void cancel(); + /** + * @return true if the job has been cancelled. + */ + bool cancelled(); + /** + * @return true if the job function has already been executed or cancelled. + */ + bool _finishedOrCancelled(); /** * @brief Get the unique `timeoutID` for JS `setTimeout`/`clearTimeout` methods * @see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#return_value */ - static inline uint32_t getUniqueId(AsyncHandle &&handle) { + static inline id_t getUniqueId(AsyncHandle &&handle) { // TODO (Tom Tang): mutex lock _timeoutIdMap.push_back(std::move(handle)); return _timeoutIdMap.size() - 1; // the index in `_timeoutIdMap` } - static inline AsyncHandle *fromId(uint32_t timeoutID) { + static inline AsyncHandle *fromId(id_t timeoutID) { try { return &_timeoutIdMap.at(timeoutID); } catch (...) { // std::out_of_range& @@ -65,6 +83,12 @@ public: } } + /** + * @brief Cancel all pending event-loop jobs. + * @return success + */ + static bool cancelAll(); + /** * @brief Get the underlying `asyncio.Handle` Python object */ @@ -72,8 +96,64 @@ public: Py_INCREF(_handle); // otherwise the object would be GC-ed as the AsyncHandle destructor decreases the reference count return _handle; } + + /** + * @brief Replace the underlying `asyncio.Handle` Python object with the provided value + * @return the old `asyncio.Handle` object + */ + inline PyObject *swap(PyObject *newHandleObject) { + return std::exchange(_handle, newHandleObject); + } + + /** + * @brief Getter for if the timer has been ref'ed + */ + inline bool hasRef() { + return _refed; + } + + /** + * @brief Ref the timer so that the event-loop won't exit as long as the timer is active + */ + inline void addRef() { + if (!_refed) { + _refed = true; + if (!_finishedOrCancelled()) { // noop if the timer is finished or canceled + PyEventLoop::_locker->incCounter(); + } + } + } + + /** + * @brief Unref the timer so that the event-loop can exit + */ + inline void removeRef() { + if (_refed) { + _refed = false; + PyEventLoop::_locker->decCounter(); + } + } + + /** + * @brief Set the debug info object for WTFPythonMonkey tool + */ + inline void setDebugInfo(PyObject *obj) { + _debugInfo = obj; + } + inline PyObject *getDebugInfo() { + return _debugInfo; + } + + /** + * @brief Get an iterator for the `AsyncHandle`s of all timers + */ + static inline auto &getAllTimers() { + return _timeoutIdMap; + } protected: PyObject *_handle; + std::atomic_bool _refed = false; + PyObject *_debugInfo = nullptr; }; /** @@ -86,9 +166,10 @@ public: * @brief Schedule a job to the Python event-loop, with the given delay * @param jobFn - The JS event-loop job converted to a Python function * @param delaySeconds - The job function will be called after the given number of seconds - * @return a AsyncHandle, the value can be safely ignored + * @param repeat - If true, the job will be executed repeatedly on a fixed interval + * @return the timeoutId */ - AsyncHandle enqueueWithDelay(PyObject *jobFn, double delaySeconds); + [[nodiscard]] AsyncHandle::id_t enqueueWithDelay(PyObject *jobFn, double delaySeconds, bool repeat); /** * @brief C++ wrapper for Python `asyncio.Future` class @@ -216,7 +297,7 @@ public: } /** - * @brief An `asyncio.Event` instance to notify that there are no queueing asynchronous jobs + * @brief An `asyncio.Event` instance to notify that there are no queued asynchronous jobs * @see https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event */ PyObject *_queueIsEmpty = nullptr; diff --git a/include/PyIterableProxyHandler.hh b/include/PyIterableProxyHandler.hh new file mode 100644 index 00000000..8a11706f --- /dev/null +++ b/include/PyIterableProxyHandler.hh @@ -0,0 +1,33 @@ +/** + * @file PyIterableProxyHandler.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects for iterables + * @date 2024-04-08 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_PyIterableProxy_ +#define PythonMonkey_PyIterableProxy_ + + +#include "include/PyObjectProxyHandler.hh" + + +/** + * @brief This struct is the ProxyHandler for JS Proxy Iterable pythonmonkey creates to handle coercion from python iterables to JS Objects + * + */ +struct PyIterableProxyHandler : public PyObjectProxyHandler { +public: + PyIterableProxyHandler() : PyObjectProxyHandler(&family) {}; + static const char family; + + bool getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc + ) const override; +}; + +#endif \ No newline at end of file diff --git a/include/PyListProxyHandler.hh b/include/PyListProxyHandler.hh new file mode 100644 index 00000000..bd5e118c --- /dev/null +++ b/include/PyListProxyHandler.hh @@ -0,0 +1,50 @@ +/** + * @file PyListProxyHandler.hh + * @author Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects for Lists + * @date 2023-12-01 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_PyListProxy_ +#define PythonMonkey_PyListProxy_ + +#include "include/PyBaseProxyHandler.hh" + + +/** + * @brief This struct is the ProxyHandler for JS Proxy Objects pythonmonkey creates + * to handle coercion from python lists to JS Array objects + */ +struct PyListProxyHandler : public PyBaseProxyHandler { +public: + PyListProxyHandler() : PyBaseProxyHandler(&family) {}; + static const char family; + + /** + * @brief Handles python object reference count when JS Proxy object is finalized + * + * @param gcx pointer to JS::GCContext + * @param proxy the proxy object being finalized + */ + void finalize(JS::GCContext *gcx, JSObject *proxy) const override; + + bool getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc + ) const override; + + bool defineProperty( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::Handle desc, JS::ObjectOpResult &result + ) const override; + + bool ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const override; + bool delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult &result) const override; + bool isArray(JSContext *cx, JS::HandleObject proxy, JS::IsArrayAnswer *answer) const override; + bool getBuiltinClass(JSContext *cx, JS::HandleObject proxy, js::ESClass *cls) const override; +}; + +#endif diff --git a/include/PyProxyHandler.hh b/include/PyObjectProxyHandler.hh similarity index 51% rename from include/PyProxyHandler.hh rename to include/PyObjectProxyHandler.hh index 21188e26..38f6f970 100644 --- a/include/PyProxyHandler.hh +++ b/include/PyObjectProxyHandler.hh @@ -1,44 +1,96 @@ /** - * @file PyProxy.hh + * @file PyObjectProxyHandler.hh * @author Caleb Aikens (caleb@distributive.network) - * @brief Struct for creating JS proxy objects. Used by DictType for object coercion - * @version 0.1 - * @date 2023-04-20 + * @brief Struct for creating JS proxy objects for all objects + * @date 2024-01-25 * - * Copyright (c) 2023 Distributive Corp. + * @copyright Copyright (c) 2023 Distributive Corp. * */ -#ifndef PythonMonkey_PyProxy_ -#define PythonMonkey_PyProxy_ +#ifndef PythonMonkey_PyObjectProxy_ +#define PythonMonkey_PyObjectProxy_ +#include "include/PyBaseProxyHandler.hh" #include #include #include /** - * @brief base class for PyProxyHandler and PyListProxyHandler - */ -struct PyBaseProxyHandler : public js::BaseProxyHandler { -public: - PyBaseProxyHandler(PyObject *pyObj, const void *family) : js::BaseProxyHandler(family), pyObject(pyObj) {}; - PyObject *pyObject; // @TODO (Caleb Aikens) Consider putting this in a private slot - - bool getPrototypeIfOrdinary(JSContext *cx, JS::HandleObject proxy, bool *isOrdinary, JS::MutableHandleObject protop) const override final; - bool preventExtensions(JSContext *cx, JS::HandleObject proxy, JS::ObjectOpResult &result) const override final; - bool isExtensible(JSContext *cx, JS::HandleObject proxy, bool *extensible) const override final; -}; - -/** - * @brief This struct is the ProxyHandler for JS Proxy Objects pythonmonkey creates to handle coercion from python dicts to JS Objects + * @brief This struct is the ProxyHandler for JS Proxy Objects pythonmonkey creates to handle coercion from python objects to JS Objects * */ -struct PyProxyHandler : public PyBaseProxyHandler { +struct PyObjectProxyHandler : public PyBaseProxyHandler { public: - PyProxyHandler(PyObject *pyObj) : PyBaseProxyHandler(pyObj, &family) {}; + PyObjectProxyHandler() : PyBaseProxyHandler(&family) {}; + PyObjectProxyHandler(const void *childFamily) : PyBaseProxyHandler(childFamily) {}; static const char family; + /** + * @brief Helper function used by dicts and objects for ownPropertyKeys + * + * @param cx - pointer to the JSContext + * @param keys - PyListObject containing the keys of the proxy'd dict/object + * @param length - the length of keys param + * @param props - out-param, will be a JS vector of the keys converted to JS Ids + * @return true - the function succeeded + * @return false - the function failed (an Exception should be raised) + */ + static bool handleOwnPropertyKeys(JSContext *cx, PyObject *keys, size_t length, JS::MutableHandleIdVector props); + + /** + * @brief Helper function used by dicts and objects for get OwnPropertyDescriptor + * + * @param cx - pointer to the JSContext + * @param id - id of the prop to get + * @param desc - out-param, the property descriptor + * @param item - the python object to be converted to a JS prop + * @return true - the function succeeded + * @return false - the function has failed and an exception has been raised + */ + static bool handleGetOwnPropertyDescriptor(JSContext *cx, JS::HandleId id, + JS::MutableHandle> desc, PyObject *item); + + /** + * @brief Handles python object reference count when JS Proxy object is finalized + * + * @param gcx pointer to JS::GCContext + * @param proxy the proxy object being finalized + */ + void finalize(JS::GCContext *gcx, JSObject *proxy) const override; + + /** + * @brief Helper function used by dicts and objects to convert dict/object to String + * + * @param cx - pointer to the JSContext + * @param argc - unused + * @param vp - unused + * @return true - this function always returns true + */ + static bool object_toString(JSContext *cx, unsigned argc, JS::Value *vp); + + /** + * @brief Helper function used by dicts and objects to convert dict/object to LocaleString + * + * @param cx - pointer to the JSContext + * @param argc - unused + * @param vp - unused + * @return true - this function always returns true + */ + static bool object_toLocaleString(JSContext *cx, unsigned argc, JS::Value *vp); + + /** + * @brief Helper function used by dicts and objects to get valueOf, just returns a new reference to `self` + * + * @param cx - pointer to the JSContext + * @param argc - unused + * @param vp - unused + * @return true - the function succeeded + * @return false - the function failed and an exception has been raised + */ + static bool object_valueOf(JSContext *cx, unsigned argc, JS::Value *vp); + /** * @brief [[OwnPropertyKeys]] * @@ -56,7 +108,7 @@ public: * @param cx - pointer to JSContext * @param proxy - The proxy object who's property we wish to delete * @param id - The key we wish to delete - * @param result - @TODO (Caleb Aikens) read up on JS::ObjectOpResult + * @param result - whether the call succeeded or not * @return true - call succeeded * @return false - call failed and an exception has been raised */ @@ -74,20 +126,6 @@ public: */ bool has(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, bool *bp) const override; - /** - * @brief [[Get]] - * - * @param cx pointer to JSContext - * @param proxy - The proxy object who's property we wish to check - * @param receiver @TODO (Caleb Aikens) read ECMAScript docs about this - * @param id - Key of the property we wish to get - * @param vp - out-paramter for the gotten property - * @return true - call succeeded - * @return false - call failed and an exception has been raised - */ - bool get(JSContext *cx, JS::HandleObject proxy, - JS::HandleValue receiver, JS::HandleId id, - JS::MutableHandleValue vp) const override; /** * @brief [[Set]] * @@ -95,8 +133,8 @@ public: * @param proxy The proxy object who's property we wish to set * @param id Key of the property we wish to set * @param v Value that we wish to set the property to - * @param receiver @TODO (Caleb Aikens) read ECMAScript docs about this - * @param result @TODO (Caleb Aikens) read ECMAScript docs about this + * @param receiver The `this` value to use when executing any code + * @param result whether or not the call succeeded * @return true call succeed * @return false call failed and an exception has been raised */ @@ -115,9 +153,8 @@ public: bool enumerate(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const override; - // @TODO (Caleb Aikens) The following are Spidermonkey-unique extensions, need to read into them more /** - * @brief @TODO (Caleb Aikens) read up on what this trap does exactly + * @brief Returns true if `id` is in `proxy`, false otherwise * * @param cx pointer to JSContext * @param proxy The proxy object who's property we wish to check @@ -128,25 +165,19 @@ public: */ bool hasOwn(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, bool *bp) const override; + /** - * @brief @TODO (Caleb Aikens) read up on what this trap does exactly + * @brief Returns vector of proxy's own keys * - * @param cx - pointer to JSContext - * @param proxy - The proxy object who's keys we output - * @param props - out-parameter of object IDs - * @return true - call succeeded - * @return false - call failed and an exception has been raised + * @param cx - Pointer to the JSContext + * @param proxy - the proxy object + * @param props - out parameter, the vector of proxy's own keys + * @return true - the call succeeded + * @return false - the call failed and an exception has been raised */ bool getOwnEnumerablePropertyKeys( JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const override; - /** - * @brief Handles python object reference count when JS Proxy object is finalized - * - * @param gcx pointer to JS::GCContext - * @param proxy the proxy object being finalized - */ - void finalize(JS::GCContext *gcx, JSObject *proxy) const override; bool getOwnPropertyDescriptor( JSContext *cx, JS::HandleObject proxy, JS::HandleId id, @@ -157,39 +188,8 @@ public: JS::HandleId id, JS::Handle desc, JS::ObjectOpResult &result) const override; -}; - -/** - * @brief This struct is the ProxyHandler for JS Proxy Objects pythonmonkey creates - * to handle coercion from python lists to JS Array-like objects - */ -struct PyListProxyHandler : public PyBaseProxyHandler { -public: - PyListProxyHandler(PyObject *pyObj) : PyBaseProxyHandler(pyObj, &family) {}; - static const char family; - - bool getOwnPropertyDescriptor( - JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::MutableHandle> desc - ) const override; - bool defineProperty( - JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::Handle desc, JS::ObjectOpResult &result - ) const override; - - bool ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const override; - bool delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult &result) const override; + bool getBuiltinClass(JSContext *cx, JS::HandleObject proxy, js::ESClass *cls) const override; }; -/** - * @brief Convert jsid to a PyObject to be used as dict keys - */ -PyObject *idToKey(JSContext *cx, JS::HandleId id); - -/** - * @brief Convert Python dict key to jsid - */ -bool keyToId(PyObject *key, JS::MutableHandleId idp); - -#endif +#endif \ No newline at end of file diff --git a/include/PyType.hh b/include/PyType.hh deleted file mode 100644 index b75d7f1b..00000000 --- a/include/PyType.hh +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @file PyType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) - * @brief Struct representing python types - * @version 0.1 - * @date 2022-07-27 - * - * @copyright Copyright (c) 2022 - * - */ - -#ifndef PythonMonkey_PyType_ -#define PythonMonkey_PyType_ - -#include "TypeEnum.hh" - -#include - -/** - * @brief Abstract struct that serves as a base for the different type relations in C++/Python - */ -struct PyType { -public: - PyType(); - PyType(PyObject *object); - const TYPE returnType = TYPE::DEFAULT; - PyObject *getPyObject(); - ~PyType(); - -protected: - PyObject *pyObject = nullptr; -}; -#endif \ No newline at end of file diff --git a/include/StrType.hh b/include/StrType.hh index b6f5e16c..d8199bc4 100644 --- a/include/StrType.hh +++ b/include/StrType.hh @@ -1,56 +1,42 @@ /** * @file StrType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing python strings - * @version 0.1 * @date 2022-08-08 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022,2024 Distributive Corp. * */ #ifndef PythonMonkey_StrType_ #define PythonMonkey_StrType_ -#include "PyType.hh" -#include "TypeEnum.hh" - #include #include /** - * @brief This struct represents the 'string' type in Python, which is represented as a 'char*' in C++. It inherits from the PyType struct + * @brief This struct represents the 'string' type in Python, which is represented as a 'char*' in C++ */ -struct StrType : public PyType { +struct StrType { public: - StrType(PyObject *object); - StrType(char *string); - /** - * @brief Construct a new StrType object from a JSString. Automatically handles encoding conversion for latin1 & UCS2: + * @brief Construct a new unicode PyObject from a JSString. Automatically handles encoding conversion for latin1 & UCS2: * codepoint | Python | Spidermonkey | identical representation? * 000000-0000FF | latin1 | latin1 | Yes * 000100-00D7FF | UCS2 | UTF16 | Yes * 00D800-00DFFF | UCS2 (unpaired) | UTF16 (unpaired) | Yes * 00E000-00FFFF | UCS2 | UTF16 | Yes - * 010000-10FFFF | UCS4 | UTF16 | No, conversion and new backing store required, user must explicitly call asUCS4() + * 010000-10FFFF | UCS4 | UTF16 | No, conversion and new backing store required, user must explicitly call asUCS4() -> static in code * * @param cx - javascript context pointer * @param str - JSString pointer - */ - StrType(JSContext *cx, JSString *str); - - const TYPE returnType = TYPE::STRING; - const char *getValue() const; - - /** - * @brief creates new UCS4-encoded pyObject string. This must be called by the user if the original JSString contains any surrogate pairs - * - * @return PyObject* - the UCS4-encoding of the pyObject string * + * @returns PyObject* pointer to the resulting PyObject */ - static PyObject *asUCS4(PyObject *pyObject); + static PyObject *getPyObject(JSContext *cx, JS::HandleValue str); + + static PyObject *proxifyString(JSContext *cx, JS::HandleValue str); }; #endif \ No newline at end of file diff --git a/include/TupleType.hh b/include/TupleType.hh deleted file mode 100644 index 5dd84a04..00000000 --- a/include/TupleType.hh +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file TupleType.hh - * @author Giovanni Tedesco (giovanni@distributive.network) - * @brief Struct for representing python tuples - * @version 0.1 - * @date 2022-08-19 - * - * @copyright Copyright (c) 2022 - * - */ - -#ifndef PythonMonkey_TupleType_ -#define PythonMonkey_TupleType_ - -#include "include/PyType.hh" -#include "include/TypeEnum.hh" - -#include - -/** - * @brief A struct to represent the tuple type in python - * - */ -struct TupleType : public PyType { - -public: - TupleType(PyObject *obj); - const TYPE returnType = TYPE::TUPLE; - -/** - * @brief Gets the tuple item at the given index - * - * @param index The index of the item in question - * @return PyType* Returns a pointer to the appropriate PyType object - */ - PyType *get(int index) const; - -/** - * @brief - * - * @returns int length of the tuple - */ - int len() const; -}; -#endif \ No newline at end of file diff --git a/include/TypeEnum.hh b/include/TypeEnum.hh deleted file mode 100644 index a2d2c875..00000000 --- a/include/TypeEnum.hh +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @file TypeEnum.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) & Tom Tang (xmader@distributive.network) - * @brief Enum for every PyType - * @version 0.1 - * @date 2022-08-08 - * - * @copyright Copyright (c) 2023 Distributive Corp. - * - */ - -#ifndef PythonMonkey_TypeEnum_ -#define PythonMonkey_TypeEnum_ - -enum class TYPE { - DEFAULT, - BOOL, - INT, - FLOAT, - STRING, - FUNC, - DICT, - LIST, - TUPLE, - DATE, - PYTHONMONKEY_PROMISE, - BUFFER, - EXCEPTION, - NONE, - PYTHONMONKEY_NULL, -}; - -#endif \ No newline at end of file diff --git a/include/internalBinding.hh b/include/internalBinding.hh index 24c00082..ca5907db 100644 --- a/include/internalBinding.hh +++ b/include/internalBinding.hh @@ -2,7 +2,6 @@ * @file internalBinding.hh * @author Tom Tang (xmader@distributive.network) * @brief - * @version 0.1 * @date 2023-05-16 * * @copyright Copyright (c) 2023 Distributive Corp. diff --git a/include/jsTypeFactory.hh b/include/jsTypeFactory.hh index 6ec68412..479ef938 100644 --- a/include/jsTypeFactory.hh +++ b/include/jsTypeFactory.hh @@ -2,22 +2,43 @@ * @file jsTypeFactory.hh * @author Caleb Aikens (caleb@distributive.network) * @brief - * @version 0.1 * @date 2023-02-15 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023 Distributive Corp. * */ #ifndef PythonMonkey_JsTypeFactory_ #define PythonMonkey_JsTypeFactory_ -#include "include/PyType.hh" - #include +#include + + +struct PythonExternalString : public JSExternalStringCallbacks { +public: + /** + * @brief Get the PyObject using the given char buffer + * + * @param chars - the char buffer of the PyObject + * @return PyObject* - the PyObject string + */ + static PyObject *getPyString(const char16_t *chars); + static PyObject *getPyString(const JS::Latin1Char *chars); + + /** + * @brief decrefs the underlying PyObject string when the JSString is finalized + * + * @param chars - The char buffer of the string + */ + void finalize(char16_t *chars) const override; + void finalize(JS::Latin1Char *chars) const override; -struct PythonExternalString; + size_t sizeOfBuffer(const char16_t *chars, mozilla::MallocSizeOf mallocSizeOf) const override; + size_t sizeOfBuffer(const JS::Latin1Char *chars, mozilla::MallocSizeOf mallocSizeOf) const override; +}; +extern PythonExternalString PythonExternalStringCallbacks; /** * @brief Function that makes a UTF16-encoded copy of a UCS4 string diff --git a/include/modules/pythonmonkey/pythonmonkey.hh b/include/modules/pythonmonkey/pythonmonkey.hh index 22e08e5d..443cc174 100644 --- a/include/modules/pythonmonkey/pythonmonkey.hh +++ b/include/modules/pythonmonkey/pythonmonkey.hh @@ -2,16 +2,14 @@ * @file pythonmonkey.hh * @author Caleb Aikens (caleb@kingsds.network) * @brief This file defines the pythonmonkey module, along with its various functions. - * @version 0.1 * @date 2022-09-06 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022-2024 Distributive Corp. * */ #ifndef PythonMonkey_Module_PythonMonkey #define PythonMonkey_Module_PythonMonkey -#include "include/PyType.hh" #include "include/JobQueue.hh" #include @@ -20,41 +18,23 @@ #include -#define PythonMonkey_Null PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "null") /**< macro for pythonmonkey.null object*/ -#define PythonMonkey_BigInt PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "bigint") /**< macro for pythonmonkey.bigint class object */ extern JSContext *GLOBAL_CX; /**< pointer to PythonMonkey's JSContext */ +extern JS::PersistentRootedObject jsFunctionRegistry; /** *global; /**< pointer to the global object of PythonMonkey's JSContext */ static JSAutoRealm *autoRealm; /**< pointer to PythonMonkey's AutoRealm */ static JobQueue *JOB_QUEUE; /**< pointer to PythonMonkey's event-loop job queue */ +// Get handle on global object +PyObject *getPythonMonkeyNull(); +PyObject *getPythonMonkeyBigInt(); + /** * @brief Destroys the JSContext and deletes associated memory. Called when python quits or faces a fatal exception. * */ static void cleanup(); -/** - * @brief This function is used to memoize PyTypes and GCThings that use the same backing store for their data, - * so that the JS garbage collector doesn't collect memory still in use by Python. It does this by storing the - * pointers in an unordered_map, with the key being the PyType pointer, and the value being a vector of GCThing - * pointers. - * - * @param pyType - Pointer to the PyType to be memoized - * @param GCThing - Pointer to the GCThing to be memoized - */ -void memoizePyTypeAndGCThing(PyType *pyType, JS::Handle GCThing); - -/** - * @brief Callback function passed to JS_SetGCCallback to handle PythonMonkey shared memory - * - * @param cx - Pointer to the JS Context (not used) - * @param status - enum specifying whether the Callback triggered at the beginning or end of the GC Cycle - * @param reason - reason for the GC Cycle - * @param data - - */ -void handleSharedPythonMonkeyMemory(JSContext *cx, JSGCStatus status, JS::GCReason reason, void *data); - /** * @brief Function exposed by the python module that calls the spidermonkey garbage collector * diff --git a/include/pyTypeFactory.hh b/include/pyTypeFactory.hh index 68e87b74..0fc262e8 100644 --- a/include/pyTypeFactory.hh +++ b/include/pyTypeFactory.hh @@ -1,54 +1,28 @@ /** * @file pyTypeFactory.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Function for wrapping arbitrary PyObjects into the appropriate PyType class, and coercing JS types to python types - * @version 0.1 * @date 2022-08-08 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2022, 2023, 2024 Distributive Corp. * */ #ifndef PythonMonkey_PyTypeFactory_ #define PythonMonkey_PyTypeFactory_ -#include "PyType.hh" - #include #include -/** @brief Function that takes an arbitrary PyObject* and returns a corresponding PyType* object - - @author Caleb Aikens - @date August 2022 - - @param object - Pointer to the PyObject who's type and value we wish to encapsulate - */ -PyType *pyTypeFactory(PyObject *object); /** - * @brief Function that takes a JS::Value and returns a corresponding PyType* object, doing shared memory management when necessary + * @brief Function that takes a JS::Value and returns a corresponding PyObject* object, doing shared memory management when necessary * * @param cx - Pointer to the javascript context of the JS::Value - * @param thisObj - Pointer to the JS `this` object for the value's scope - * @param rval - Pointer to the JS::Value who's type and value we wish to encapsulate - * @return PyType* - Pointer to a PyType object corresponding to the JS::Value - */ -PyType *pyTypeFactory(JSContext *cx, JS::Rooted *thisObj, JS::Rooted *rval); -/** - * @brief same to pyTypeFactory, but it's guaranteed that no error would be set on the Python error stack, instead - * return `pythonmonkey.null` on error - */ -PyType *pyTypeFactorySafe(JSContext *cx, JS::Rooted *thisObj, JS::Rooted *rval); - -/** - * @brief Helper function for pyTypeFactory to create FuncTypes through PyCFunction_New - * - * @param JSFuncAddress - Pointer to a PyLongObject containing the memory address of JS::Value containing the JSFunction* - * @param args - Pointer to a PyTupleObject containing the arguments to the python function - * @return PyObject* - The result of the JSFunction called with args coerced to JS types, coerced back to a PyObject type, or NULL if coercion wasn't possible + * @param rval - The JS::Value who's type and value we wish to encapsulate + * @return PyObject* - Pointer to the object corresponding to the JS::Value */ -PyObject *callJSFunc(PyObject *JSFuncAddress, PyObject *args); +PyObject *pyTypeFactory(JSContext *cx, JS::HandleValue rval); #endif \ No newline at end of file diff --git a/include/pyshim.hh b/include/pyshim.hh new file mode 100644 index 00000000..1e43d86f --- /dev/null +++ b/include/pyshim.hh @@ -0,0 +1,145 @@ +/** + * @file pyshim.hh + * @author Tom Tang (xmader@distributive.network) + * @brief Python's C APIs are constantly changing in different versions of CPython. + * PythonMonkey has a wide variety of CPython versions' support. (Currently Python 3.8-3.13) + * This file helps our Python API calls work with different Python versions in the same code base. + * @date 2024-09-20 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#ifndef PythonMonkey_py_version_shim_ +#define PythonMonkey_py_version_shim_ + +#include + +/** + * @brief `_Py_IsFinalizing` becomes a stable API in Python 3.13, + * and renames to `Py_IsFinalizing` + */ +#if PY_VERSION_HEX >= 0x030d0000 // Python version is greater than 3.13 + #define Py_IsFinalizing Py_IsFinalizing +#else + #define Py_IsFinalizing _Py_IsFinalizing +#endif + +/** + * @brief `_PyDictViewObject` type definition moved from Python's public API + * to the **internal** header file `internal/pycore_dict.h` in Python 3.13. + * + * @see https://github.com/python/cpython/blob/v3.13.0rc1/Include/internal/pycore_dict.h#L64-L72 + */ +#if PY_VERSION_HEX >= 0x030d0000 // Python version is greater than 3.13 +typedef struct { + PyObject_HEAD + PyDictObject *dv_dict; +} _PyDictViewObject; +#endif + +/** + * @brief Shim for `_PyArg_CheckPositional`. + * Since Python 3.13, `_PyArg_CheckPositional` function became an internal API. + * @see Modified from https://github.com/python/cpython/blob/v3.13.0rc1/Python/getargs.c#L2738-L2780 + */ +#if PY_VERSION_HEX >= 0x030d0000 // Python version is greater than 3.13 +inline int _PyArg_CheckPositional(const char *name, Py_ssize_t nargs, Py_ssize_t min, Py_ssize_t max) { + if (!name) { // _PyArg_CheckPositional may also be when unpacking a tuple + name = "unpacked tuple"; // https://github.com/python/cpython/blob/v3.13.0rc1/Python/getargs.c#L2746 + } + + if (nargs < min) { + PyErr_Format( + PyExc_TypeError, + "%.200s expected %s%zd argument%s, got %zd", + name, (min == max ? "" : "at least "), min, min == 1 ? "" : "s", nargs); + return 0; + } + + if (nargs == 0) { + return 1; + } + + if (nargs > max) { + PyErr_Format( + PyExc_TypeError, + "%.200s expected %s%zd argument%s, got %zd", + name, (min == max ? "" : "at most "), max, max == 1 ? "" : "s", nargs); + return 0; + } + + return 1; +} +#endif + +/** + * @brief Shim for `_PyDictView_New`. + * Since Python 3.13, `_PyDictView_New` function became an internal API. + * @see Modified from https://github.com/python/cpython/blob/v3.13.0rc1/Objects/dictobject.c#L5806-L5827 + */ +inline PyObject *PyDictView_New(PyObject *dict, PyTypeObject *type) { +#if PY_VERSION_HEX < 0x030d0000 // Python version is lower than 3.13 + return _PyDictView_New(dict, type); +#else + _PyDictViewObject *dv; + dv = PyObject_GC_New(_PyDictViewObject, type); + if (dv == NULL) + return NULL; + Py_INCREF(dict); + dv->dv_dict = (PyDictObject *)dict; + PyObject_GC_Track(dv); + return (PyObject *)dv; +#endif +} + +/** + * @brief Shim for `_PyErr_SetKeyError`. + * Since Python 3.13, `_PyErr_SetKeyError` function became an internal API. + */ +inline void PyErr_SetKeyError(PyObject *key) { + // Use the provided API when possible, as `PyErr_SetObject`'s behaviour is more complex than originally thought + // see also: https://github.com/python/cpython/issues/101578 +#if PY_VERSION_HEX < 0x030d0000 // Python version is lower than 3.13 + return _PyErr_SetKeyError(key); +#else + return PyErr_SetObject(PyExc_KeyError, key); +#endif +} + +/** + * @brief Shim for `Py_SET_SIZE`. + * `Py_SET_SIZE` is not available in Python < 3.9 + */ +#ifndef Py_SET_SIZE +static inline void _Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) { + ob->ob_size = size; +} +#define Py_SET_SIZE(ob, size) _Py_SET_SIZE((PyVarObject *)(ob), size) +#endif + +/** + * @brief Shim for `PyObject_CallOneArg`. + * `PyObject_CallOneArg` is not available in Python < 3.9 + */ +#if PY_VERSION_HEX < 0x03090000 // Python version is less than 3.9 +inline PyObject *PyObject_CallOneArg(PyObject *func, PyObject *arg) { + return PyObject_CallFunction(func, "O", arg); +} +#endif + +/** + * @brief Shim for `_PyLong_AsByteArray`. + * Python 3.13.0a4 added a new public API `PyLong_AsNativeBytes()` to replace the private `_PyLong_AsByteArray()`. + * But this change also modified the function signature of `_PyLong_AsByteArray()`. + * @see https://github.com/python/cpython/issues/111140 + */ +inline int PyLong_AsByteArray(PyLongObject *v, unsigned char *bytes, size_t n, bool little_endian, bool is_signed) { +#if PY_VERSION_HEX >= 0x030d0000 // Python version is 3.13 or higher + return _PyLong_AsByteArray(v, bytes, n, little_endian, is_signed, /*with_exceptions*/ false); +#else + return _PyLong_AsByteArray(v, bytes, n, little_endian, is_signed); +#endif +} + +#endif // #ifndef PythonMonkey_py_version_shim_ diff --git a/include/setSpiderMonkeyException.hh b/include/setSpiderMonkeyException.hh index f838e5e4..dc7a8794 100644 --- a/include/setSpiderMonkeyException.hh +++ b/include/setSpiderMonkeyException.hh @@ -2,10 +2,9 @@ * @file setSpiderMonkeyException.hh * @author Caleb Aikens (caleb@distributive.network) * @brief Call this function whenever a JS_* function call fails in order to set an appropriate python exception (remember to also return NULL) - * @version 0.1 * @date 2023-02-28 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023 Distributive Corp. * */ @@ -19,8 +18,9 @@ * * @param cx - pointer to the JS context * @param exceptionStack - reference to the SpiderMonkey exception stack + * @param printStack - whether or not to print the JS stack */ -PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack); +PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack, bool printStack); /** * @brief This function sets a python error under the assumption that a JS_* function call has failed. Do not call this function if that is not the case. diff --git a/mozcentral.version b/mozcentral.version new file mode 100644 index 00000000..55aeecbf --- /dev/null +++ b/mozcentral.version @@ -0,0 +1 @@ +6bca861985ba51920c1cacc21986af01c51bd690 diff --git a/peter-jr b/peter-jr index 873a85f6..aba5f924 100755 --- a/peter-jr +++ b/peter-jr @@ -142,6 +142,7 @@ findTests \ | (shuf 2>/dev/null || cat) \ | while read file do + trap 'echo -e "\nAborted." >&2; exit 130' 2 printf "Testing %-${longestFilenameLen}s ... " "${file}" ext="${file##*.}" testName="${file%.failing}" @@ -168,7 +169,7 @@ findTests \ exitCode="$?" ;; "bash") - eval timeout -s9 "${timeout:-${defaultTimeout}}" bash \"$file\" + eval timeout -s9 --foreground "${timeout:-${defaultTimeout}}" bash \"$file\" exitCode="$?" ;; *) diff --git a/poetry.lock b/poetry.lock index 62d1a77f..6bf2a192 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,399 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "aiodns" +version = "3.2.0" +description = "Simple DNS resolver for asyncio" +optional = false +python-versions = "*" +files = [ + {file = "aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5"}, + {file = "aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72"}, +] + +[package.dependencies] +pycares = ">=4.0.0" + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.10.11" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"}, + {file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"}, + {file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"}, + {file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"}, + {file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"}, + {file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"}, + {file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"}, + {file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"}, + {file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"}, + {file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"}, + {file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"}, + {file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"}, + {file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"}, + {file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"}, +] + +[package.dependencies] +aiodns = {version = ">=3.2.0", optional = true, markers = "(sys_platform == \"linux\" or sys_platform == \"darwin\") and extra == \"speedups\""} +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +Brotli = {version = "*", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"speedups\""} +brotlicffi = {version = "*", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"speedups\""} +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.12.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "colorama" version = "0.4.6" @@ -13,29 +407,251 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "numpy" version = "1.24.4" @@ -75,99 +691,449 @@ files = [ [[package]] name = "numpy" -version = "2.0.0.dev0" +version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-2.0.0.dev0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d9ee815dd72c00bf2b842f00eca5174b65e8357467561ce3a3711ea666217ebd"}, - {file = "numpy-2.0.0.dev0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b07c082a32afacf20c72f3e7cfb84b97ac01dd7eabc11a6f339e6d0859cc73c"}, - {file = "numpy-2.0.0.dev0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e5df60daeb76746476a02d85907a2e65661ad0180a5516dd04d6efb77202bbc"}, - {file = "numpy-2.0.0.dev0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f7f7b2e458ca033a8c28718f57bf2c6bc72b6e091d7a98b4ed0926cce475d7e"}, - {file = "numpy-2.0.0.dev0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:26fef32b250ebbbabd176d4c6f7f74b35461cd035cb7d7575141c42e96a162cb"}, - {file = "numpy-2.0.0.dev0-cp310-cp310-win_amd64.whl", hash = "sha256:95f6254daf1f067759660cfe98dc80615e68c6811130cc6edb4650be82eb3a94"}, - {file = "numpy-2.0.0.dev0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e5bb824a54c6ecedd262cedeaf148f0a20d18352bf01648d6495fad6bf90fad5"}, - {file = "numpy-2.0.0.dev0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e2edb7eb45409a3822e8d5e5a4e84ff797f90a7ba79192f33f0c8c7fc20db29"}, - {file = "numpy-2.0.0.dev0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a2871d629cd39048c3e75e29f80e5166dafd5eb08f0ef7292c10fd44523a376"}, - {file = "numpy-2.0.0.dev0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d0331bc691ffbbbee176808bc3383abe9f99034d7aaba8cf2ccba62e60064c1"}, - {file = "numpy-2.0.0.dev0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3eaa3ad03c472e6d0d2d7a2c51cc8d013a0ff02219ef04c1a699da534587d924"}, - {file = "numpy-2.0.0.dev0-cp311-cp311-win_amd64.whl", hash = "sha256:b67f2d97cd5cb4839cd3c881d31c639b8ee99c3773fbf39440a086eaef6c08f0"}, - {file = "numpy-2.0.0.dev0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4a5c13f25048bb00c6241e6f274e7882c446c9810f4897f86a79fb20a10dca57"}, - {file = "numpy-2.0.0.dev0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d5d890b2054ce93e7930f5b77fa45023a805de9670715fad2f874beabaf3e90"}, - {file = "numpy-2.0.0.dev0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b489b60da67d6e97c233982156eb6ee512a091d48868aa6e72ab5cd8eeaf629"}, - {file = "numpy-2.0.0.dev0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a9b6d9543618a0b4aba4a29b12853042adccfc416809398121a143751c0da14"}, - {file = "numpy-2.0.0.dev0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1741a0b350a21d5aae34105760b80b1fb597082e8a8dc9c2d903a2244e0d9dfb"}, - {file = "numpy-2.0.0.dev0-cp312-cp312-win_amd64.whl", hash = "sha256:4cb78b51a2c0900e827e32b86bbf2d35e5da77034d13583eb613baef13733e5b"}, - {file = "numpy-2.0.0.dev0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d054fc187f28c68896c3d7de1120287fa3e26f905825686ef1ab99089060ef14"}, - {file = "numpy-2.0.0.dev0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:45b6e9e8a641d9ae72112bfa9abac37a0a135d522e6fb6be56b8ca10957fc737"}, - {file = "numpy-2.0.0.dev0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:022fa4ee86825096ce654cf90bd1ec57538c5a7627b28bc30ce2a83b00580aa6"}, - {file = "numpy-2.0.0.dev0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37565dd074af4d359ace61c2c65ab6684763f2e7549c1dc9cd5a096c0be6b8e0"}, - {file = "numpy-2.0.0.dev0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7504b42d17ed110b48363f899c1a1babc8fb8249743c9cfb85e57dbadbce02e0"}, - {file = "numpy-2.0.0.dev0-cp39-cp39-win_amd64.whl", hash = "sha256:e34d5b3b969a13355b9ee1d55e98d4264119815c19928d011b353cb87f548cc1"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.anaconda.org/pythonmonkey/simple" -reference = "anaconda" + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, +] + +[[package]] +name = "numpy" +version = "2.2.6" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, + {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, + {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, + {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, + {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, + {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, + {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, + {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, + {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, + {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, + {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, + {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, +] + +[[package]] +name = "numpy" +version = "2.3.3" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +files = [ + {file = "numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30"}, + {file = "numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57"}, + {file = "numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa"}, + {file = "numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}, + {file = "numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}, + {file = "numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}, + {file = "numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}, + {file = "numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}, + {file = "numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}, + {file = "numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}, + {file = "numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}, + {file = "numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}, + {file = "numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}, + {file = "numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}, + {file = "numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}, + {file = "numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}, + {file = "numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}, + {file = "numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}, + {file = "numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc"}, + {file = "numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}, +] [[package]] name = "packaging" -version = "23.1" +version = "25.0" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pip" -version = "23.1.2" +version = "23.3.2" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.7" files = [ - {file = "pip-23.1.2-py3-none-any.whl", hash = "sha256:3ef6ac33239e4027d9a5598a381b9d30880a1477e50039db2eac6e8a8f6d1b18"}, - {file = "pip-23.1.2.tar.gz", hash = "sha256:0e7c86f486935893c708287b30bd050a36ac827ec7fe5e43fe7cb198dd835fba"}, + {file = "pip-23.3.2-py3-none-any.whl", hash = "sha256:5052d7889c1f9d05224cd41741acb7c5d6fa735ab34e339624a614eaaa7e7d76"}, + {file = "pip-23.3.2.tar.gz", hash = "sha256:7fd9972f96db22c8077a1ee2691b172c8089b17a5652a44494a9ecb0d78f9149"}, ] [[package]] name = "pluggy" -version = "1.2.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "propcache" +version = "0.2.0" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.8" +files = [ + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, + {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, + {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, + {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, + {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, + {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, + {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, + {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, + {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, + {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, + {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, + {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, + {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, + {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, + {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, +] + +[[package]] +name = "pycares" +version = "4.4.0" +description = "Python interface for c-ares" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, + {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, + {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff"}, + {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd"}, + {file = "pycares-4.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677"}, + {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3"}, + {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153"}, + {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9"}, + {file = "pycares-4.4.0-cp310-cp310-win32.whl", hash = "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0"}, + {file = "pycares-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c"}, + {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490"}, + {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a"}, + {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817"}, + {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff"}, + {file = "pycares-4.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710"}, + {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6"}, + {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c"}, + {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f"}, + {file = "pycares-4.4.0-cp311-cp311-win32.whl", hash = "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f"}, + {file = "pycares-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502"}, + {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6"}, + {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4"}, + {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93"}, + {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a"}, + {file = "pycares-4.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8"}, + {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88"}, + {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540"}, + {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4"}, + {file = "pycares-4.4.0-cp312-cp312-win32.whl", hash = "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0"}, + {file = "pycares-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977"}, + {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95"}, + {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273"}, + {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee"}, + {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8"}, + {file = "pycares-4.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084"}, + {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69"}, + {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912"}, + {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4"}, + {file = "pycares-4.4.0-cp38-cp38-win32.whl", hash = "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f"}, + {file = "pycares-4.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51"}, + {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741"}, + {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641"}, + {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80"}, + {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002"}, + {file = "pycares-4.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b"}, + {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02"}, + {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec"}, + {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b"}, + {file = "pycares-4.4.0-cp39-cp39-win32.whl", hash = "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47"}, + {file = "pycares-4.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2"}, + {file = "pycares-4.4.0.tar.gz", hash = "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2"}, +] + +[package.dependencies] +cffi = ">=1.5.0" + +[package.extras] +idna = ["idna (>=2.1)"] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + [[package]] name = "pyreadline3" -version = "3.4.1" +version = "3.5.4" description = "A python implementation of GNU readline." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, - {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, ] +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -183,16 +1149,169 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "yarl" +version = "1.15.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8"}, + {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172"}, + {file = "yarl-1.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43ebdcc120e2ca679dba01a779333a8ea76b50547b55e812b8b92818d604662c"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3433da95b51a75692dcf6cc8117a31410447c75a9a8187888f02ad45c0a86c50"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38d0124fa992dbacd0c48b1b755d3ee0a9f924f427f95b0ef376556a24debf01"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ded1b1803151dd0f20a8945508786d57c2f97a50289b16f2629f85433e546d47"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace4cad790f3bf872c082366c9edd7f8f8f77afe3992b134cfc810332206884f"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c77494a2f2282d9bbbbcab7c227a4d1b4bb829875c96251f66fb5f3bae4fb053"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b7f227ca6db5a9fda0a2b935a2ea34a7267589ffc63c8045f0e4edb8d8dcf956"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:31561a5b4d8dbef1559b3600b045607cf804bae040f64b5f5bca77da38084a8a"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3e52474256a7db9dcf3c5f4ca0b300fdea6c21cca0148c8891d03a025649d935"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1af74a9529a1137c67c887ed9cde62cff53aa4d84a3adbec329f9ec47a3936"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:15c87339490100c63472a76d87fe7097a0835c705eb5ae79fd96e343473629ed"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:74abb8709ea54cc483c4fb57fb17bb66f8e0f04438cff6ded322074dbd17c7ec"}, + {file = "yarl-1.15.2-cp310-cp310-win32.whl", hash = "sha256:ffd591e22b22f9cb48e472529db6a47203c41c2c5911ff0a52e85723196c0d75"}, + {file = "yarl-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:1695497bb2a02a6de60064c9f077a4ae9c25c73624e0d43e3aa9d16d983073c2"}, + {file = "yarl-1.15.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5"}, + {file = "yarl-1.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e"}, + {file = "yarl-1.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5"}, + {file = "yarl-1.15.2-cp311-cp311-win32.whl", hash = "sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d"}, + {file = "yarl-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179"}, + {file = "yarl-1.15.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94"}, + {file = "yarl-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e"}, + {file = "yarl-1.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b"}, + {file = "yarl-1.15.2-cp312-cp312-win32.whl", hash = "sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8"}, + {file = "yarl-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d"}, + {file = "yarl-1.15.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84"}, + {file = "yarl-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33"}, + {file = "yarl-1.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe"}, + {file = "yarl-1.15.2-cp313-cp313-win32.whl", hash = "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9"}, + {file = "yarl-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad"}, + {file = "yarl-1.15.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16"}, + {file = "yarl-1.15.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b"}, + {file = "yarl-1.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04"}, + {file = "yarl-1.15.2-cp38-cp38-win32.whl", hash = "sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea"}, + {file = "yarl-1.15.2-cp38-cp38-win_amd64.whl", hash = "sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9"}, + {file = "yarl-1.15.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc"}, + {file = "yarl-1.15.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627"}, + {file = "yarl-1.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7"}, + {file = "yarl-1.15.2-cp39-cp39-win32.whl", hash = "sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d"}, + {file = "yarl-1.15.2-cp39-cp39-win_amd64.whl", hash = "sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810"}, + {file = "yarl-1.15.2-py3-none-any.whl", hash = "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a"}, + {file = "yarl-1.15.2.tar.gz", hash = "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "f4de4ae4242c925c62044ba107e732a45941c0568a3c434e3daa446da24956de" +content-hash = "e4f32f4286c2558ea80fcba84acb74bb0a6d10ff33f02edaa7a36330ea25e07b" diff --git a/pyproject.toml b/pyproject.toml index f72afb69..78edc0ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,24 @@ [tool.poetry] name = "pythonmonkey" version = "0" # automatically set by poetry-dynamic-versioning -description = "" -authors = ["Caleb Aikens ", "Tom Tang ", "Wes Garland ", "Hamada Gasmallah "] +description = "Seamless interop between Python and JavaScript." +authors = ["Distributive Corp. "] +license = "MIT" +homepage = "https://pythonmonkey.io/" +documentation = "https://docs.pythonmonkey.io/" +repository = "https://github.com/Distributive-Network/PythonMonkey" readme = "README.md" packages = [ { include = "pythonmonkey", from = "python" }, ] include = [ # Linux and macOS - "python/pythonmonkey/pythonmonkey.so", - "python/pythonmonkey/libmozjs*", + { path = "python/pythonmonkey/pythonmonkey.so", format = ["sdist", "wheel"] }, + { path = "python/pythonmonkey/libmozjs*", format = ["sdist", "wheel"] }, # Windows - "python/pythonmonkey/pythonmonkey.pyd", - "python/pythonmonkey/mozjs-*.dll", + { path = "python/pythonmonkey/pythonmonkey.pyd", format = ["sdist", "wheel"] }, + { path = "python/pythonmonkey/mozjs-*.dll", format = ["sdist", "wheel"] }, # include all files for source distribution { path = "src", format = "sdist" }, @@ -23,13 +27,18 @@ include = [ { path = "tests", format = "sdist" }, { path = "CMakeLists.txt", format = "sdist" }, { path = "*.sh", format = "sdist" }, + { path = "mozcentral.version", format = "sdist" }, + + # Add marker file for pep561 + { path = "python/pythonmonkey/py.typed", format = ["sdist", "wheel"] }, ] [tool.poetry.dependencies] python = "^3.8" pyreadline3 = { version = "^3.4.1", platform = "win32" } -pminit = { version = "*", allow-prereleases = true } +aiohttp = { version = "^3.9.5" } +pminit = { version = ">=0.4.0", allow-prereleases = true } [tool.poetry-dynamic-versioning] @@ -56,19 +65,22 @@ pmjs = "pythonmonkey.cli.pmjs:main" pytest = "^7.3.1" pip = "^23.1.2" numpy = [ - # numpy hasn't released for Python 3.12 yet on PyPI - # TODO: use the PyPI build once the wheels are released for Python 3.12 - {version = "^2.0.0.dev0", allow-prereleases = true, source = "anaconda", python = "3.12.*"}, - {version = "^1.24.3", python = "<3.12"}, + {version = "^2.3.0", python = ">=3.11"}, + {version = "^2.1.0", python = ">=3.10,<3.11"}, # NumPy 2.3.0 drops support for Python 3.10 + {version = "^2.0.1", python = ">=3.9,<3.10"}, # NumPy 2.1.0 drops support for Python 3.9 + {version = "^1.24.3", python = ">=3.8,<3.9"}, # NumPy 1.25.0 drops support for Python 3.8 ] pminit = { path = "./python/pminit", develop = true } -[[tool.poetry.source]] -name = "anaconda" -url = "https://pypi.anaconda.org/pythonmonkey/simple" -priority = "explicit" - [build-system] -requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning==0.24.0"] +requires = ["poetry-core>=1.1.1", "poetry-dynamic-versioning==1.1.1"] build-backend = "poetry_dynamic_versioning.backend" + +[tool.autopep8] +max_line_length=120 +ignore="E111,E114,E121" # allow 2-space indents +verbose=true +indent-size=2 +aggressive=3 +exit-code=true diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/python/pminit/pminit/cli.py b/python/pminit/pminit/cli.py index 700ba00c..f7a005a2 100644 --- a/python/pminit/pminit/cli.py +++ b/python/pminit/pminit/cli.py @@ -11,8 +11,8 @@ def execute(cmd: str, cwd: str): popen.stdout.close() return_code = popen.wait() - if return_code: - raise subprocess.CalledProcessError(return_code, cmd) + if return_code != 0: + sys.exit(return_code) def commandType(value: str): if value != "npm": @@ -34,7 +34,3 @@ def main(): ) execute(' '.join( args.executable + args.args ), pythonmonkey_path) - - - - diff --git a/python/pminit/pyproject.toml b/python/pminit/pyproject.toml index 4f6e6946..09d28174 100644 --- a/python/pminit/pyproject.toml +++ b/python/pminit/pyproject.toml @@ -3,11 +3,13 @@ name = "pminit" version = "0" description = "Post-install hook for PythonMonkey" authors = [ - "Tom Tang ", - "Caleb Aikens ", - "Wes Garland ", - "Hamada Gasmallah " + "Distributive Corp. " ] +license = "MIT" +homepage = "https://pythonmonkey.io/" +documentation = "https://docs.pythonmonkey.io/" +repository = "https://github.com/Distributive-Network/PythonMonkey" + include = [ # Install extra files into the pythonmonkey package "pythonmonkey/package*.json", @@ -33,6 +35,6 @@ generate-setup-file = false pminit = "pminit.cli:main" [build-system] -requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning==0.24.0"] +requires = ["poetry-core>=1.1.1", "poetry-dynamic-versioning==1.1.1"] build-backend = "poetry_dynamic_versioning.backend" diff --git a/python/pminit/pythonmonkey/package-lock.json b/python/pminit/pythonmonkey/package-lock.json index 5aa5ddaf..6533b355 100644 --- a/python/pminit/pythonmonkey/package-lock.json +++ b/python/pminit/pythonmonkey/package-lock.json @@ -1,47 +1,73 @@ { "name": "pythonmonkey", "version": "0.0.1", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "requires": { + "packages": { + "": { + "name": "pythonmonkey", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "core-js": "^3.35.1", + "ctx-module": "^1.0.15", + "events": "^3.3.0" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } + "minimalistic-assert": "^1.0.0" } }, - "base64-js": { + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, + "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "bn.js": { + "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "license": "MIT" }, - "brorand": { + "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" }, - "browserify-aes": { + "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { + "license": "MIT", + "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", @@ -50,96 +76,144 @@ "safe-buffer": "^5.0.1" } }, - "browserify-cipher": { + "node_modules/browserify-cipher": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { + "license": "MIT", + "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, - "browserify-des": { + "node_modules/browserify-des": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { + "license": "MIT", + "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.5", + "hash-base": "~3.0", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" } }, - "buffer": { + "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, - "buffer-xor": { + "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "license": "MIT" + }, + "node_modules/cipher-base": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "node_modules/core-js": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "create-ecdh": { + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "requires": { + "license": "MIT", + "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } } }, - "create-hash": { + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, + "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { + "license": "MIT", + "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", @@ -147,11 +221,12 @@ "sha.js": "^2.4.0" } }, - "create-hmac": { + "node_modules/create-hmac": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { + "license": "MIT", + "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", @@ -160,64 +235,93 @@ "sha.js": "^2.4.8" } }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "ctx-module": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/ctx-module/-/ctx-module-1.0.14.tgz", - "integrity": "sha512-eH4h/bv64YuzCHMUZs93j57/4zNJHyQWOIz5CPAs1gJ/4yznPD9HoCLCXjQlST2AxOLOKNvV67n+A8dALtLA+Q==", - "requires": { + "node_modules/ctx-module": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/ctx-module/-/ctx-module-1.0.15.tgz", + "integrity": "sha512-+/L9CMH/5cjNW+ddPmqGoxF4/xOns3jdTZFbvi683zjxB/FEACGKpBLfVVWvLsyJsdQCKLVibUlM7Ui/BtMS/A==", + "license": "MIT", + "dependencies": { "buffer": "^6.0.3", - "crypto-browserify": "^3.12.0" + "crypto-browserify": "^3.12.1", + "debug": "^4.3.4" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "des.js": { + "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "requires": { + "license": "MIT", + "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, - "diffie-hellman": { + "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { + "license": "MIT", + "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } } }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", @@ -225,210 +329,304 @@ "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } } }, - "evp_bytestokey": { + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { + "license": "MIT", + "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "hash.js": { + "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { + "license": "MIT", + "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, - "hmac-drbg": { + "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "requires": { + "license": "MIT", + "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, - "ieee754": { + "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, - "md5.js": { + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { + "license": "MIT", + "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, - "miller-rabin": { + "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { + "license": "MIT", + "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } + "bin": { + "miller-rabin": "bin/miller-rabin" } }, - "minimalistic-assert": { + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, + "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" }, - "minimalistic-crypto-utils": { + "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "pbkdf2": { + "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "requires": { + "license": "MIT", + "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", "ripemd160": "^2.0.1", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" } }, - "public-encrypt": { + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "requires": { + "license": "MIT", + "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } } }, - "randombytes": { + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, + "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { + "license": "MIT", + "dependencies": { "safe-buffer": "^5.1.0" } }, - "randomfill": { + "node_modules/randomfill": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { + "license": "MIT", + "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "ripemd160": { + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { + "license": "MIT", + "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, - "safe-buffer": { + "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "sha.js": { + "node_modules/sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "util-deprecate": { + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" } } } diff --git a/python/pminit/pythonmonkey/package.json b/python/pminit/pythonmonkey/package.json index 2e5f0241..af323dd0 100644 --- a/python/pminit/pythonmonkey/package.json +++ b/python/pminit/pythonmonkey/package.json @@ -21,6 +21,8 @@ }, "homepage": "https://github.com/Distributive-Network/PythonMonkey#readme", "dependencies": { - "ctx-module": "^1.0.14" + "core-js": "^3.35.1", + "ctx-module": "^1.0.15", + "events": "^3.3.0" } } diff --git a/python/pythonmonkey/__init__.py b/python/pythonmonkey/__init__.py index c779f0a7..957713ff 100644 --- a/python/pythonmonkey/__init__.py +++ b/python/pythonmonkey/__init__.py @@ -5,25 +5,13 @@ # Expose the package version import importlib.metadata -__version__= importlib.metadata.version(__name__) +__version__ = importlib.metadata.version(__name__) del importlib # Load the module by default to expose global APIs +# builtin_modules require("console") require("base64") require("timers") - -# Add the `.keys()` method on `Object.prototype` to get JSObjectProxy dict() conversion working -# Conversion from a dict-subclass to a strict dict by `dict(subclass)` internally calls the .keys() method to read the dictionary keys, -# but .keys on a JSObjectProxy can only come from the JS side -pythonmonkey.eval(""" -(makeList) => { - const keysMethod = { - get() { - return () => makeList(...Object.keys(this)) - } - } - Object.defineProperty(Object.prototype, "keys", keysMethod) - Object.defineProperty(Array.prototype, "keys", keysMethod) -} -""", { 'filename': __file__, 'fromPythonFrame': True })(lambda *args: list(args)) +require("url") +require("XMLHttpRequest") diff --git a/python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.d.ts b/python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.d.ts new file mode 100644 index 00000000..2d8c4495 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.d.ts @@ -0,0 +1,57 @@ +/** + * @file XMLHttpRequest-internal.d.ts + * @brief TypeScript type declarations for the internal XMLHttpRequest helpers + * @author Tom Tang + * @date August 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. + */ + +/** + * `processResponse` callback's argument type + */ +export declare interface XHRResponse { + /** Response URL */ + url: string; + /** HTTP status */ + status: number; + /** HTTP status message */ + statusText: string; + /** The `Content-Type` header value */ + contentLength: number; + /** Implementation of the `xhr.getResponseHeader` method */ + getResponseHeader(name: string): string | undefined; + /** Implementation of the `xhr.getAllResponseHeaders` method */ + getAllResponseHeaders(): string; + /** Implementation of the `xhr.abort` method */ + abort(): void; +} + +/** + * Send request + */ +export declare function request( + method: string, + url: string, + headers: Record, + body: string | Uint8Array, + timeoutMs: number, + // callbacks for request body progress + processRequestBodyChunkLength: (bytesLength: number) => void, + processRequestEndOfBody: () => void, + // callbacks for response progress + processResponse: (response: XHRResponse) => void, + processBodyChunk: (bytes: Uint8Array) => void, + processEndOfBody: () => void, + // callbacks for known exceptions + onTimeoutError: (err: Error) => void, + onNetworkError: (err: Error) => void, + // the debug logging function + /** See `pm.bootstrap.require("debug")` */ + debug: (selector: string) => ((...args: string[]) => void), +): Promise; + +/** + * Decode data using the codec registered for encoding. + */ +export declare function decodeStr(data: Uint8Array, encoding?: string): string; diff --git a/python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.py b/python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.py new file mode 100644 index 00000000..ca716d88 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.py @@ -0,0 +1,140 @@ +# @file XMLHttpRequest-internal.py +# @brief internal helper functions for XMLHttpRequest +# @author Tom Tang +# @date August 2023 +# @copyright Copyright (c) 2023 Distributive Corp. + +import asyncio +import aiohttp +import yarl +import io +import platform +import pythonmonkey as pm +from typing import Union, ByteString, Callable, TypedDict + +keepAliveConnector: Union[aiohttp.TCPConnector, None] = None + + +class XHRResponse(TypedDict, total=True): + """ + See definitions in `XMLHttpRequest-internal.d.ts` + """ + url: str + status: int + statusText: str + contentLength: int + getResponseHeader: Callable[[str], Union[str, None]] + getAllResponseHeaders: Callable[[], str] + abort: Callable[[], None] + + +async def request( + method: str, + url: str, + headers: dict, + body: Union[str, ByteString], + timeoutMs: float, + # callbacks for request body progress + processRequestBodyChunkLength: Callable[[int], None], + processRequestEndOfBody: Callable[[], None], + # callbacks for response progress + processResponse: Callable[[XHRResponse], None], + processBodyChunk: Callable[[bytearray], None], + processEndOfBody: Callable[[], None], + # callbacks for known exceptions + onTimeoutError: Callable[[asyncio.TimeoutError], None], + onNetworkError: Callable[[aiohttp.ClientError], None], + # the debug logging function, see `pm.bootstrap.require("debug")` + debug: Callable[[str], Callable[..., None]], + / +): + + # to support HTTP-Keep-Alive + global keepAliveConnector + if (not keepAliveConnector): + # seconds before closing Keep-Alive connection. + keepAliveConnector = aiohttp.TCPConnector(keepalive_timeout=5) # 5s is the default for Node.js's `http.globalAgent` + + class BytesPayloadWithProgress(aiohttp.BytesPayload): + _chunkMaxLength = 2**16 # aiohttp default + + async def write(self, writer) -> None: + debug('xhr:io')('begin chunked write') + buf = io.BytesIO(self._value) + chunk = buf.read(self._chunkMaxLength) + while chunk: + debug('xhr:io')(' writing', len(chunk), 'bytes') + await writer.write(chunk) + processRequestBodyChunkLength(len(chunk)) + chunk = buf.read(self._chunkMaxLength) + processRequestEndOfBody() + debug('xhr:io')('finish chunked write') + + if isinstance(body, str): + body = bytes(body, "utf-8") + + # set default headers + headers.setdefault("user-agent", f"Python/{platform.python_version()} PythonMonkey/{pm.__version__}") + debug('xhr:headers')('after set default\n', headers) + + if timeoutMs > 0: + timeoutOptions = aiohttp.ClientTimeout(total=timeoutMs / 1000) # convert to seconds + else: + timeoutOptions = aiohttp.ClientTimeout() # default timeout + + try: + debug('xhr:aiohttp')('creating request for', url) + async with aiohttp.request(method=method, + url=yarl.URL(url, encoded=True), + headers=headers, + data=BytesPayloadWithProgress(body) if body else None, + timeout=timeoutOptions, + connector=keepAliveConnector, + ) as res: + debug('xhr:aiohttp')('got', res.content_type, 'result') + + def getResponseHeader(name: str): + return res.headers.get(name) + + def getAllResponseHeaders(): + headers = [] + for name, value in res.headers.items(): + headers.append(f"{name.lower()}: {value}") + headers.sort() + return "\r\n".join(headers) + + def abort(): + debug('xhr:io')('abort') + res.close() + + # readyState HEADERS_RECEIVED + processResponse({ + 'url': str(res.real_url), + 'status': res.status, + 'statusText': str(res.reason or ''), + + 'getResponseHeader': getResponseHeader, + 'getAllResponseHeaders': getAllResponseHeaders, + 'abort': abort, + 'contentLength': res.content_length or 0, + }) + + async for data in res.content.iter_any(): + processBodyChunk(bytearray(data)) # PythonMonkey only accepts the mutable bytearray type + # readyState DONE + processEndOfBody() + except asyncio.TimeoutError as e: + onTimeoutError(e) + raise # rethrow + except aiohttp.ClientError as e: + onNetworkError(e) + raise # rethrow + + +def decodeStr(data: bytes, encoding='utf-8'): # XXX: Remove this once we get proper TextDecoder support + return str(data, encoding=encoding) + + +# Module exports +exports['request'] = request # type: ignore +exports['decodeStr'] = decodeStr # type: ignore diff --git a/python/pythonmonkey/builtin_modules/XMLHttpRequest.js b/python/pythonmonkey/builtin_modules/XMLHttpRequest.js new file mode 100644 index 00000000..3cbacfc1 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/XMLHttpRequest.js @@ -0,0 +1,742 @@ +/** + * @file XMLHttpRequest.js + * Implement the XMLHttpRequest (XHR) API + * + * @author Tom Tang + * @date August 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. + */ +'use strict'; + +const { EventTarget, Event } = require('event-target'); +const { DOMException } = require('dom-exception'); +const { URL, URLSearchParams } = require('url'); +const { request, decodeStr } = require('XMLHttpRequest-internal'); +const debug = globalThis.python.eval('__import__("pythonmonkey").bootstrap.require')('debug'); + +/** + * Truncate a string-like thing for display purposes, returning a string. + * @param {any} what The thing to truncate; must have a slice method and index property. + * Works with string, array, typedarray, etc. + * @param {number} maxlen The maximum length for truncation + * @param {boolean=} coerce Not false = coerce to printable character codes + * @returns {string} + */ +function trunc(what, maxlen, coerce) +{ + if (coerce !== false && typeof what !== 'string') + { + what = Array.from(what).map(x => { + if (x > 31 && x < 127) + return String.fromCharCode(x); + else if (x < 32) + return String.fromCharCode(0x2400 + Number(x)); + else if (x === 127) + return '\u2421'; + else + return '\u2423'; + }).join(''); + } + return `${what.slice(0, maxlen)}${what.length > maxlen ? '\u2026' : ''}`; +} + +// exposed +/** + * Events using the ProgressEvent interface indicate some kind of progression. + */ +class ProgressEvent extends Event +{ + /** + * @param {string} type + * @param {{ lengthComputable?: boolean; loaded?: number; total?: number; error?: Error; }} eventInitDict + */ + constructor (type, eventInitDict = {}) + { + super(type); + this.lengthComputable = eventInitDict.lengthComputable ?? false; + this.loaded = eventInitDict.loaded ?? 0; + this.total = eventInitDict.total ?? 0; + this.error = eventInitDict.error ?? null; + this.debugTag = 'xhr:'; + } +} + +// exposed +class XMLHttpRequestEventTarget extends EventTarget +{ + // event handlers + /** @typedef {import('event-target').EventListenerFn} EventListenerFn */ + /** @type {EventListenerFn} */ + onloadstart; + /** @type {EventListenerFn} */ + onprogress; + /** @type {EventListenerFn} */ + onabort; + /** @type {EventListenerFn} */ + onerror; + /** @type {EventListenerFn} */ + onload; + /** @type {EventListenerFn} */ + ontimeout; + /** @type {EventListenerFn} */ + onloadend; +} + +// exposed +class XMLHttpRequestUpload extends XMLHttpRequestEventTarget +{} + +const FORBIDDEN_REQUEST_METHODS = [ + 'TRACE', + 'TRACK', + 'CONNECT' +]; + +// exposed +/** + * Implement the `XMLHttpRequest` API (`XHR` for short) according to the spec. + * @see https://xhr.spec.whatwg.org/ + */ +class XMLHttpRequest extends XMLHttpRequestEventTarget +{ + // event handler + /** @type {EventListenerFn} */ + onreadystatechange = null; + + // + // debugging + // + /** The unique connection id to identify each XHR connection when debugging */ + #connectionId = Math.random().toString(16).slice(2, 9); // random 7-character hex string + + /** + * Wrapper to print debug logs with connection id information + * @param {string} selector + */ + #debug(selector) + { + return (...args) => debug(selector)(`Conn<${this.#connectionId}>:`, ...args); + } + + /** + * Allowing others to inspect the internal properties + */ + get _requestMetadata() + { + return { + method: this.#requestMethod, + url: this.#requestURL.toString(), + headers: this.#requestHeaders, + body: this.#requestBody, + }; + } + + // + // states + // + /** @readonly */ static UNSENT = 0; + /** @readonly */ static OPENED = 1; + /** @readonly */ static HEADERS_RECEIVED = 2; + /** @readonly */ static LOADING = 3; + /** @readonly */ static DONE = 4; + + /** @readonly */ UNSENT = 0; + /** @readonly */ OPENED = 1; + /** @readonly */ HEADERS_RECEIVED = 2; + /** @readonly */ LOADING = 3; + /** @readonly */ DONE = 4; + + /** + * Returns client's state. + */ + get readyState() + { + return this.#state; + } + + // + // request + // + /** + * Sets the request method, request URL, and synchronous flag. + * @typedef {'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT'} Method + * @param {Method} method + * @param {string | URL} url + * @param {boolean} async + * @param {string} username + * @param {string} password + * @see https://xhr.spec.whatwg.org/#the-open()-method + */ + open(method, url, async = true, username = null, password = null) + { + this.#debug('xhr:open')('open start, method=' + method); + // Normalize the method. + // @ts-expect-error + method = method.toString().toUpperCase(); + + // Check for valid request method + if (!method || FORBIDDEN_REQUEST_METHODS.includes(method)) + throw new DOMException('Request method not allowed', 'SecurityError'); + + const parsedURL = new URL(url); + if (username) + parsedURL.username = username; + if (password) + parsedURL.password = password; + this.#debug('xhr:open')('url is ' + parsedURL.href); + + // step 11 + this.#sendFlag = false; + this.#uploadListenerFlag = false; + this.#requestMethod = method; + this.#requestURL = parsedURL; + if (async === false) + this.#synchronousFlag = true; + this.#requestHeaders = {}; // clear + this.#response = null; + this.#receivedBytes = []; + this.#responseObject = null; + + // step 12 + if (this.#state !== XMLHttpRequest.OPENED) + { + this.#state = XMLHttpRequest.OPENED; + this.dispatchEvent(new Event('readystatechange')); + } + this.#debug('xhr:open')('finished open, state is ' + this.#state); + } + + /** + * Combines a header in author request headers. + * @param {string} name + * @param {string} value + */ + setRequestHeader(name, value) + { + this.#debug('xhr:headers')(`set header ${name}=${value}`); + if (this.#state !== XMLHttpRequest.OPENED) + throw new DOMException('setRequestHeader can only be called when state is OPEN', 'InvalidStateError'); + if (this.#sendFlag) + throw new DOMException('send flag is true', 'InvalidStateError'); + + // Normalize value + value = value.toString().trim(); + name = name.toString().trim().toLowerCase(); + + // TODO: do we need to throw for forbidden request-headers here? + // see https://fetch.spec.whatwg.org/#forbidden-request-header + + // Combine header values + if (this.#requestHeaders[name]) + this.#requestHeaders[name] += ', ' + value; + else + this.#requestHeaders[name] = value; + } + + /** + * Timeout time in **milliseconds**. + * When set to a non-zero value will cause fetching to terminate after the given time has passed. + */ + timeout = 0; + + /** + * A boolean value that indicates whether or not cross-site `Access-Control` requests should be made using credentials such as cookies, authorization headers or TLS client certificates. + * Setting withCredentials has no effect on same-origin requests. + * @see https://xhr.spec.whatwg.org/#the-withcredentials-attribute + */ + get withCredentials() + { + return this.#crossOriginCredentials; + } + set withCredentials(flag) + { + // step 1 + if (this.#state !== XMLHttpRequest.UNSENT && this.#state !== XMLHttpRequest.OPENED) + // The XHR internal state should be UNSENT or OPENED. + throw new DOMException('XMLHttpRequest must not be sending.', 'InvalidStateError'); + // step 2 + if (this.#sendFlag) + throw new DOMException('send() has already been called', 'InvalidStateError'); + // step 3 + this.#crossOriginCredentials = flag; + // TODO: figure out what cross-origin means in PythonMonkey. Is it always same-origin request? What to send? + } + + /** + * Returns the associated XMLHttpRequestUpload object. + * It can be used to gather transmission information when data is transferred to a server. + */ + get upload() + { + return this.#uploadObject; + } + + /** + * Initiates the request. + * @typedef {TypedArray | DataView | ArrayBuffer | URLSearchParams | string} XMLHttpRequestBodyInit + * @param {XMLHttpRequestBodyInit | null} body + * @see https://xhr.spec.whatwg.org/#dom-xmlhttprequest-send + */ + send(body = null) + { + this.#debug('xhr:send')(`sending; body length=${body?.length} «${body ? trunc(body, 100) : ''}»`); + if (this.#state !== XMLHttpRequest.OPENED) // step 1 + throw new DOMException('connection must be opened before send() is called', 'InvalidStateError'); + if (this.#sendFlag) // step 2 + throw new DOMException('send has already been called', 'InvalidStateError'); + + if (['GET', 'HEAD'].includes(this.#requestMethod)) // step 3 + body = null; + + // step 4 + this.#requestBody = null; + if (body !== null) + { + let extractedContentType = null; + if (body instanceof URLSearchParams) + { + this.#requestBody = body.toString(); + extractedContentType = 'application/x-www-form-urlencoded;charset=UTF-8'; + } + else if (typeof body === 'string') + { + this.#requestBody = body; + extractedContentType = 'text/plain;charset=UTF-8'; + } + else // BufferSource + { + this.#requestBody = body instanceof ArrayBuffer ? new Uint8Array(body) : new Uint8Array(body.buffer); // make a copy + } + + const originalAuthorContentType = this.#requestHeaders['content-type']; + if (!originalAuthorContentType && extractedContentType) + this.#requestHeaders['content-type'] = extractedContentType; + } + this.#debug('xhr:send')(`content-type=${this.#requestHeaders['content-type']}`); + + // step 5 + if (this.#uploadObject._hasAnyListeners()) + this.#uploadListenerFlag = true; + + // FIXME: do we have to initiate request here instead of in #sendAsync? (step 6) + + this.#uploadCompleteFlag = false; // step 7 + this.#timedOutFlag = false; // step 8 + if (this.#requestBody === null) // step 9 + this.#uploadCompleteFlag = true; + this.#sendFlag = true; // step 10 + + if (!this.#synchronousFlag) // step 11 + this.#sendAsync(); + else // step 12 + this.#sendSync(); + } + + /** + * @see https://xhr.spec.whatwg.org/#dom-xmlhttprequest-send step 11 + */ + #sendAsync() + { + this.#debug('xhr:send')('sending in async mode'); + this.dispatchEvent(new ProgressEvent('loadstart', { loaded:0, total:0 })); // step 11.1 + + let requestBodyTransmitted = 0; // step 11.2 + let requestBodyLength = this.#requestBody ? this.#requestBody.length : 0; // step 11.3 + if (!this.#uploadCompleteFlag && this.#uploadListenerFlag) // step 11.5 + this.#uploadObject.dispatchEvent(new ProgressEvent('loadstart', { loaded:requestBodyTransmitted, total:requestBodyLength })); + + if (this.#state !== XMLHttpRequest.OPENED || !this.#sendFlag) // step 11.6 + return; + + // step 11.7 + const processRequestBodyChunkLength = (/** @type {number} */ bytesLength) => + { + requestBodyTransmitted += bytesLength; + if (this.#uploadListenerFlag) + this.#uploadObject.dispatchEvent(new ProgressEvent('progress', { loaded:requestBodyTransmitted, total:requestBodyLength })); + }; + + // step 11.8 + const processRequestEndOfBody = () => + { + this.#uploadCompleteFlag = true; + if (!this.#uploadListenerFlag) + return; + for (const eventType of ['progress', 'load', 'loadend']) + this.#uploadObject.dispatchEvent(new ProgressEvent(eventType, { loaded:requestBodyTransmitted, total:requestBodyLength })); + }; + + // step 11.9 + let responseLength = 0; + const processResponse = (response) => + { + this.#debug('xhr:response')(`response headers ----\n${response.getAllResponseHeaders()}`); + this.#response = response; // step 11.9.1 + this.#state = XMLHttpRequest.HEADERS_RECEIVED; // step 11.9.4 + this.dispatchEvent(new Event('readystatechange')); // step 11.9.5 + if (this.#state !== XMLHttpRequest.HEADERS_RECEIVED) // step 11.9.6 + return; + responseLength = this.#response.contentLength; // step 11.9.8 + }; + + const processBodyChunk = (/** @type {Uint8Array} */ bytes) => + { + this.#debug('xhr:response')(`recv chunk, ${bytes.length} bytes «${trunc(bytes, 100)}»`); + this.#receivedBytes.push(bytes); + if (this.#state === XMLHttpRequest.HEADERS_RECEIVED) + this.#state = XMLHttpRequest.LOADING; + this.dispatchEvent(new Event('readystatechange')); + this.dispatchEvent(new ProgressEvent('progress', { loaded:this.#receivedLength, total:responseLength })); + }; + + /** + * @see https://xhr.spec.whatwg.org/#handle-response-end-of-body + */ + const processEndOfBody = () => + { + this.#debug('xhr:response')(`end of body, received ${this.#receivedLength} bytes`); + const transmitted = this.#receivedLength; // step 3 + const length = responseLength || 0; // step 4 + + this.dispatchEvent(new ProgressEvent('progress', { loaded:transmitted, total:length })); // step 6 + this.#state = XMLHttpRequest.DONE; // step 7 + this.#sendFlag = false; // step 8 + + this.dispatchEvent(new Event('readystatechange')); // step 9 + for (const eventType of ['load', 'loadend']) // step 10, step 11 + this.dispatchEvent(new ProgressEvent(eventType, { loaded:transmitted, total:length })); + }; + + this.#debug('xhr:send')(`${this.#requestMethod} ${this.#requestURL.href}`); + this.#debug('xhr:headers')('headers=' + Object.entries(this.#requestHeaders)); + + // send() step 6 + request( + this.#requestMethod, + this.#requestURL.toString(), + this.#requestHeaders, + this.#requestBody ?? '', + this.timeout, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processBodyChunk, + processEndOfBody, + () => (this.#timedOutFlag = true), // onTimeoutError + () => (this.#response = null /* network error */), // onNetworkError + this.#debug.bind(this), + ).catch((e) => this.#handleErrors(e)); + } + + /** + * @see https://xhr.spec.whatwg.org/#dom-xmlhttprequest-send step 12 + */ + #sendSync() + { + /* Synchronous XHR deprecated. /wg march 2024 */ + throw new DOMException('synchronous XHR is not supported', 'NotSupportedError'); + } + + /** + * @see https://xhr.spec.whatwg.org/#handle-errors + * @param {Error} e + */ + #handleErrors(e) + { + if (!this.#sendFlag) // step 1 + return; + if (this.#timedOutFlag) // step 2 + return this.#reportRequestError('timeout', new DOMException(e.toString(), 'TimeoutError')); + if (this.#response === null /* network error */) // step 4 + return this.#reportRequestError('error', new DOMException(e.toString(), 'NetworkError')); + else // unknown errors + return this.#reportRequestError('error', new DOMException(e.toString(), 'InvalidStateError')); + } + + /** + * @see https://xhr.spec.whatwg.org/#request-error-steps + * @param {string} event event type + * @param {DOMException} exception + */ + #reportRequestError(event, exception) + { + this.#state = XMLHttpRequest.DONE; // step 1 + this.#sendFlag = false; // step 2 + + this.#response = null/* network error */; // step 3 + + if (this.#synchronousFlag) // step 4 + throw exception; + + this.dispatchEvent(new Event('readystatechange')); // step 5 + + if (!this.#uploadCompleteFlag) // step 6 + { + this.#uploadCompleteFlag = true; + if (this.#uploadListenerFlag) + { + this.#uploadObject.dispatchEvent(new ProgressEvent(event, { loaded:0, total:0, error: exception })); + this.#uploadObject.dispatchEvent(new ProgressEvent('loadend', { loaded:0, total:0 })); + } + } + + this.dispatchEvent(new ProgressEvent(event, { loaded:0, total:0, error: exception })); // step 7 + this.dispatchEvent(new ProgressEvent('loadend', { loaded:0, total:0 })); // step 8 + } + + /** + * Cancels any network activity. + * @see https://xhr.spec.whatwg.org/#the-abort()-method + */ + abort() + { + if (this.#response) + this.#response.abort(); // step 1 + + if ( + (this.#state === XMLHttpRequest.OPENED && this.#sendFlag) + || this.#state === XMLHttpRequest.HEADERS_RECEIVED + || this.#state === XMLHttpRequest.LOADING + ) // step 2 + return this.#reportRequestError('abort', new DOMException('Aborted.', 'AbortError')); + + if (this.#state === XMLHttpRequest.DONE) // step 3 + { + this.#state = XMLHttpRequest.UNSENT; + this.#response = null; /* network error */ + } + } + + // + // response + // + /** + * @return {string} + */ + get responseURL() + { + if (!this.#response) + return ''; + else + return this.#response.url; + } + + /** + * @return {number} HTTP status code + */ + get status() + { + if (!this.#response) + return 0; + else + return this.#response.status; + } + + /** + * @return {string} HTTP status message + */ + get statusText() + { + if (!this.#response) + return ''; + else + return this.#response.statusText; + } + + /** + * @param {string} name + * @return {string} the text of a particular header's value + */ + getResponseHeader(name) + { + if (!this.#response) + return null; + else + return this.#response.getResponseHeader(name) ?? null; + } + + /** + * @return {string} all the response headers, separated by CRLF, as a string, or returns null if no response has been received. + */ + getAllResponseHeaders() + { + if (!this.#response) + return ''; + else + return this.#response.getAllResponseHeaders(); + } + + /** + * Acts as if the `Content-Type` header value for a response is mime. + * (It does not change the header.) + * @param {string} mime + */ + overrideMimeType(mime) + { + // TODO + } + + /** + * @typedef {"" | "arraybuffer" | "blob" | "document" | "json" | "text"} ResponseType + */ + get responseType() + { + return this.#responseType; + } + set responseType(t) + { + if (this.#state === XMLHttpRequest.LOADING || this.#state === XMLHttpRequest.DONE) + throw new DOMException('responseType can only be set before send()', 'InvalidStateError'); + if (!['', 'text', 'arraybuffer', 'json'].includes(t)) + throw new DOMException('only responseType "text" or "arraybuffer" or "json" is supported', 'NotSupportedError'); + this.#responseType = t; + } + + /** + * @see https://xhr.spec.whatwg.org/#text-response + * @return {string} + */ + #getTextResponse() + { + // TODO: refactor using proper TextDecoder API + // TODO: handle encodings other than utf-8 + this.#responseObject = decodeStr(this.#mergeReceivedBytes(), 'utf-8'); + return this.#responseObject; + } + + /** + * Returns the response body. + * @see https://xhr.spec.whatwg.org/#the-response-attribute + */ + get response() + { + if (this.#responseType === '' || this.#responseType === 'text') // step 1 + return this.responseText; + if (this.#state !== XMLHttpRequest.DONE) // step 2 + return null; + + if (this.#responseObject) // step 4 + return this.#responseObject; + if (this.#responseType === 'arraybuffer') // step 5 + { + this.#responseObject = this.#mergeReceivedBytes().buffer; + return this.#responseObject; + } + + if (this.#responseType === 'json') // step 8 + { + // step 8.2 + if (this.#receivedLength === 0) // response’s body is null + return null; + // step 8.3 + let jsonObject = null; + try + { + // TODO: use proper TextDecoder API + const str = decodeStr(this.#mergeReceivedBytes(), 'utf-8'); // only supports utf-8, see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value + jsonObject = JSON.parse(str); + } + catch (exception) + { + return null; + } + // step 8.4 + this.#responseObject = jsonObject; + } + + // step 6 and step 7 ("blob" or "document") are not supported + throw new DOMException(`unsupported responseType "${this.#responseType}"`, 'InvalidStateError'); + } + + /** + * Returns response as text. + */ + get responseText() + { + if (!['text', ''].includes(this.#responseType)) + throw new DOMException('responseType must be "text" or an empty string', 'InvalidStateError'); + if (![XMLHttpRequest.LOADING, XMLHttpRequest.DONE].includes(this.#state)) + return ''; + else + return this.#getTextResponse(); + } + + /** + * Returns the response as document. + */ + get responseXML() + { + throw new DOMException('responseXML is not supported', 'NotSupportedError'); + } + + // internal properties + #uploadObject = new XMLHttpRequestUpload(); + #state = XMLHttpRequest.UNSENT; // One of unsent, opened, headers received, loading, and done; initially unsent. + #sendFlag = false; // A flag, initially unset. + #crossOriginCredentials = false; // A boolean, initially false. + /** @type {Method} */ + #requestMethod = null; + /** @type {URL} */ + #requestURL = null; + /** @type {{ [name: string]: string; }} */ + #requestHeaders = {}; + /** @type {string | Uint8Array | null} */ + #requestBody = null; + #synchronousFlag = false; // A flag, initially unset. + #uploadCompleteFlag = false; // A flag, initially unset. + #uploadListenerFlag = false; // A flag, initially unset. + #timedOutFlag = false; // A flag, initially unset. + /** @type {import('./XMLHttpRequest-internal').XHRResponse} */ + #response = null; + /** @type {Uint8Array[]} */ + #receivedBytes = []; + /** @type {ResponseType} */ + #responseType = ''; + /** + * cache for converting receivedBytes to the desired response type + * @type {ArrayBuffer | string | Record} + */ + #responseObject = null; + + /** + * Get received bytes’s total length + */ + get #receivedLength() + { + return this.#receivedBytes.reduce((sum, chunk) => sum + chunk.length, 0); + } + + /** + * Concatenate received bytes into one single Uint8Array + */ + #mergeReceivedBytes() + { + let offset = 0; + const merged = new Uint8Array(this.#receivedLength); + for (const chunk of this.#receivedBytes) + { + merged.set(chunk, offset); + offset += chunk.length; + } + return merged; + } +} + +/* A side-effect of loading this module is to add the XMLHttpRequest and related symbols to the global + * object. This makes them accessible in the "normal" way (like in a browser) even in PythonMonkey JS + * host environments which don't include a require() symbol. + */ +if (!globalThis.XMLHttpRequestEventTarget) + globalThis.XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; +if (!globalThis.XMLHttpRequestUpload) + globalThis.XMLHttpRequestUpload = XMLHttpRequestUpload; +if (!globalThis.XMLHttpRequest) + globalThis.XMLHttpRequest = XMLHttpRequest; +if (!globalThis.ProgressEvent) + globalThis.ProgressEvent = ProgressEvent; + +exports.XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; +exports.XMLHttpRequestUpload = XMLHttpRequestUpload; +exports.XMLHttpRequest = XMLHttpRequest; +exports.ProgressEvent = ProgressEvent; diff --git a/python/pythonmonkey/builtin_modules/base64.d.ts b/python/pythonmonkey/builtin_modules/base64.d.ts index a1a3c38a..6575386f 100644 --- a/python/pythonmonkey/builtin_modules/base64.d.ts +++ b/python/pythonmonkey/builtin_modules/base64.d.ts @@ -3,6 +3,8 @@ * @brief TypeScript type declarations for base64.py * @author Tom Tang * @date July 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ /** diff --git a/python/pythonmonkey/builtin_modules/base64.py b/python/pythonmonkey/builtin_modules/base64.py index 196f48ff..97e29ade 100644 --- a/python/pythonmonkey/builtin_modules/base64.py +++ b/python/pythonmonkey/builtin_modules/base64.py @@ -1,14 +1,22 @@ # @file base64.py # @author Tom Tang , Hamada Gasmallah # @date July 2023 +# @copyright Copyright (c) 2023 Distributive Corp. +import pythonmonkey as pm import base64 -atob = lambda b64: str(base64.standard_b64decode(b64), 'latin1') -btoa = lambda data: str(base64.standard_b64encode(bytes(data, 'latin1')), 'latin1') + +def atob(b64): + padding = '=' * (4 - (len(b64) & 3)) + return str(base64.standard_b64decode(b64 + padding), 'latin1') + + +def btoa(data): + return str(base64.standard_b64encode(bytes(data, 'latin1')), 'latin1') + # Make `atob`/`btoa` globally available -import pythonmonkey as pm pm.eval(r"""(atob, btoa) => { if (!globalThis.atob) { globalThis.atob = atob; @@ -19,5 +27,5 @@ }""")(atob, btoa) # Module exports -exports['atob'] = atob # type: ignore -exports['btoa'] = btoa # type: ignore +exports['atob'] = atob # type: ignore +exports['btoa'] = btoa # type: ignore diff --git a/python/pythonmonkey/builtin_modules/console.js b/python/pythonmonkey/builtin_modules/console.js index 7bf6e2b4..a2024d8c 100644 --- a/python/pythonmonkey/builtin_modules/console.js +++ b/python/pythonmonkey/builtin_modules/console.js @@ -2,22 +2,39 @@ * @file console.js * @author Tom Tang * @date June 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ -const { customInspectSymbol, format } = require("util"); +const { customInspectSymbol, format } = require('util'); /** @typedef {(str: string) => void} WriteFn */ /** @typedef {{ write: WriteFn }} IOWriter */ /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Console_API + * @see https://console.spec.whatwg.org/ */ -// TODO (Tom Tang): adhere https://console.spec.whatwg.org/ -class Console { +class Console +{ /** @type {WriteFn} */ #writeToStdout; /** @type {WriteFn} */ #writeToStderr; + /** + * @type {{ [label: string]: number; }} + * @see https://console.spec.whatwg.org/#counting + */ + #countMap = {}; + + #groupLevel = 0; + + /** + * @type {{ [label: string]: number; }} + * @see https://console.spec.whatwg.org/#timing + */ + #timerTable = {}; + /** * Console constructor, form 1 * @overload @@ -51,27 +68,138 @@ class Console { this.#writeToStdout = options.stdout.write; this.#writeToStderr = options.stderr.write; - this.log = (...args) => this.#writeToStdout(this.#formatToStr(...args)); - this.debug = (...args) => this.#writeToStdout(this.#formatToStr(...args)); - this.info = (...args) => this.#writeToStdout(this.#formatToStr(...args)); - this.warn = (...args) => this.#writeToStderr(this.#formatToStr(...args)); - this.error = (...args) => this.#writeToStderr(this.#formatToStr(...args)); + this.log = (...args) => void this.#writeToStdout(this.#formatToStr(...args)); + this.debug = (...args) => void this.#writeToStdout(this.#formatToStr(...args)); + this.info = (...args) => void this.#writeToStdout(this.#formatToStr(...args)); + this.warn = (...args) => void this.#writeToStderr(this.#formatToStr(...args)); + this.error = (...args) => void this.#writeToStderr(this.#formatToStr(...args)); + + this.clear = () => void this.#writeToStdout('\x1bc'); // clear the terminal, see https://stackoverflow.com/questions/47503734 + + this.assert = (condition, ...data) => // See https://console.spec.whatwg.org/#assert + { + if (condition) return; // step 1 + + const message = 'Assertion failed'; // step 2 + if (data.length === 0) // step 3 + data.push(message); + else // step 4 + { + const first = data[0]; // step 4.1 + if (typeof first !== 'string') data.unshift(message+':'); // step 4.2 + else data[0] = `${message}: ${first}`; // step 4.3 + } + + return this.error(...data); // print out + }; + + this.trace = (...args) => // implement console.trace using new Error().stack + { + const header = args.length > 0 + ? `Trace: ${format(...args)}\n` + : 'Trace\n'; + + let stacks = new Error().stack + .split('\n') + stacks.shift(); // skip the first line which is this.trace itself + stacks = stacks + .filter(s => s !== '') // filter out empty lines + .map(s => ' '+s) // add indent + .join('\n'); + return this.debug(header + stacks); + }; + + // TODO (Tom Tang): implement those properly instead of aliases to console.log + this.dir = this.log; + this.dirxml = this.log; + this.table = this.log; + + // Counting functions + // @see https://console.spec.whatwg.org/#count + this.count = (label = 'default') => + { + if (this.#countMap[label]) + this.#countMap[label] += 1; + else + this.#countMap[label] = 1; + this.info(`${label}: ${this.#countMap[label]}`); + }; + + this.countReset = (label = 'default') => + { + if (this.#countMap[label]) + this.#countMap[label] = 0; + else + this.warn(`Counter for '${label}' does not exist.`); + }; + + // Grouping functions + // @see https://console.spec.whatwg.org/#grouping + this.group = (...data) => + { + if (data.length > 0) + this.log(...data); + this.#groupLevel++; + }; + this.groupCollapsed = this.group; + + this.groupEnd = () => + { + this.#groupLevel--; + if (this.#groupLevel < 0) + this.#groupLevel = 0; + }; + + // Timing functions + // @see https://console.spec.whatwg.org/#timing + this.time = (label = 'default') => + { + if (this.#timerTable[label]) + this.warn(`Label '${label}' already exists for console.time()`); + else + this.#timerTable[label] = Date.now(); + }; + + this.timeLog = (label = 'default', ...data) => + { + if (!this.#timerTable[label]) + return this.warn(`No such label '${label}' for console.timeLog()`); + + const duration = Date.now() - this.#timerTable[label]; + data.unshift(`${label}: ${duration}ms`); + + return this.log(...data); + }; + + this.timeEnd = (label = 'default') => + { + if (!this.#timerTable[label]) + return this.warn(`No such label '${label}' for console.timeEnd()`); + + const startTime = this.#timerTable[label]; + delete this.#timerTable[label]; + + const duration = Date.now() - startTime; + return this.info(`${label}: ${duration}ms`); + }; } /** + * Format with appropriate grouping level * @return {string} */ - #formatToStr(...args) { - return format(...args) + "\n" + #formatToStr(...args) + { + const msg = format(...args) + '\n'; + return msg.split('\n').map(s => '│ '.repeat(this.#groupLevel) + s).join('\n'); } - // TODO (Tom Tang): implement more methods - /** * Re-export the `Console` constructor as global `console.Console`, like in Node.js */ - get Console() { - return Console + get Console() + { + return Console; } /** @@ -80,7 +208,8 @@ class Console { static customInspectSymbol = customInspectSymbol; } -if (!globalThis.console) { +if (!globalThis.console) +{ globalThis.console = new Console( python.stdout /* sys.stdout */, python.stderr /* sys.stderr */ diff --git a/python/pythonmonkey/builtin_modules/dom-exception.d.ts b/python/pythonmonkey/builtin_modules/dom-exception.d.ts new file mode 100644 index 00000000..e37f747b --- /dev/null +++ b/python/pythonmonkey/builtin_modules/dom-exception.d.ts @@ -0,0 +1,80 @@ +/** + * @file dom-exception.d.ts + * Type definitions for DOMException + * + * Copied from https://www.npmjs.com/package/@types/web + * Apache License 2.0 + */ + +/** + * An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) + */ +export interface DOMException extends Error { + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) + */ + readonly code: number; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */ + readonly message: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */ + readonly name: string; + readonly INDEX_SIZE_ERR: 1; + readonly DOMSTRING_SIZE_ERR: 2; + readonly HIERARCHY_REQUEST_ERR: 3; + readonly WRONG_DOCUMENT_ERR: 4; + readonly INVALID_CHARACTER_ERR: 5; + readonly NO_DATA_ALLOWED_ERR: 6; + readonly NO_MODIFICATION_ALLOWED_ERR: 7; + readonly NOT_FOUND_ERR: 8; + readonly NOT_SUPPORTED_ERR: 9; + readonly INUSE_ATTRIBUTE_ERR: 10; + readonly INVALID_STATE_ERR: 11; + readonly SYNTAX_ERR: 12; + readonly INVALID_MODIFICATION_ERR: 13; + readonly NAMESPACE_ERR: 14; + readonly INVALID_ACCESS_ERR: 15; + readonly VALIDATION_ERR: 16; + readonly TYPE_MISMATCH_ERR: 17; + readonly SECURITY_ERR: 18; + readonly NETWORK_ERR: 19; + readonly ABORT_ERR: 20; + readonly URL_MISMATCH_ERR: 21; + readonly QUOTA_EXCEEDED_ERR: 22; + readonly TIMEOUT_ERR: 23; + readonly INVALID_NODE_TYPE_ERR: 24; + readonly DATA_CLONE_ERR: 25; +} + +export declare var DOMException: { + prototype: DOMException; + new(message?: string, name?: string): DOMException; + readonly INDEX_SIZE_ERR: 1; + readonly DOMSTRING_SIZE_ERR: 2; + readonly HIERARCHY_REQUEST_ERR: 3; + readonly WRONG_DOCUMENT_ERR: 4; + readonly INVALID_CHARACTER_ERR: 5; + readonly NO_DATA_ALLOWED_ERR: 6; + readonly NO_MODIFICATION_ALLOWED_ERR: 7; + readonly NOT_FOUND_ERR: 8; + readonly NOT_SUPPORTED_ERR: 9; + readonly INUSE_ATTRIBUTE_ERR: 10; + readonly INVALID_STATE_ERR: 11; + readonly SYNTAX_ERR: 12; + readonly INVALID_MODIFICATION_ERR: 13; + readonly NAMESPACE_ERR: 14; + readonly INVALID_ACCESS_ERR: 15; + readonly VALIDATION_ERR: 16; + readonly TYPE_MISMATCH_ERR: 17; + readonly SECURITY_ERR: 18; + readonly NETWORK_ERR: 19; + readonly ABORT_ERR: 20; + readonly URL_MISMATCH_ERR: 21; + readonly QUOTA_EXCEEDED_ERR: 22; + readonly TIMEOUT_ERR: 23; + readonly INVALID_NODE_TYPE_ERR: 24; + readonly DATA_CLONE_ERR: 25; +}; diff --git a/python/pythonmonkey/builtin_modules/dom-exception.js b/python/pythonmonkey/builtin_modules/dom-exception.js new file mode 100644 index 00000000..40cf427d --- /dev/null +++ b/python/pythonmonkey/builtin_modules/dom-exception.js @@ -0,0 +1,14 @@ +/** + * @file dom-exception.js + * Polyfill the DOMException interface + * + * @author Tom Tang + * @date August 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. + */ + +// Apply polyfill from core-js +require('core-js/actual/dom-exception'); + +exports.DOMException = globalThis.DOMException; diff --git a/python/pythonmonkey/builtin_modules/event-target.js b/python/pythonmonkey/builtin_modules/event-target.js new file mode 100644 index 00000000..d4e6613e --- /dev/null +++ b/python/pythonmonkey/builtin_modules/event-target.js @@ -0,0 +1,189 @@ +/** + * @file event-target.js + * Implement browser-style EventTarget + * @see https://dom.spec.whatwg.org/#eventtarget + * @author Tom Tang + * @date August 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. + */ +'use strict'; +const debug = globalThis.python.eval('__import__("pythonmonkey").bootstrap.require')('debug'); + +/** + * The Event interface represents an event which takes place in the DOM. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Event + */ +class Event +{ + /** + * Indicates whether the event bubbles up through the DOM tree or not. + * @type {Boolean} + */ + bubbles = false; + + /** + * Indicates whether the event is cancelable. + * @type {Boolean} + */ + cancelable = true; + + /** + * Indicates whether the event can bubble across the boundary between the shadow DOM and the regular DOM. + * @type {Boolean} + */ + composed = false; + + /** + * The element to which the event handler has been attached. + * @type {EventTarget} + */ + currentTarget = null; + + /** + * Indicates whether the call to event.preventDefault() canceled the event. + * @type {Boolean} + */ + devaultPrevented = false; + + /** + * Indicates which phase of the event flow is currently being evaluated. + * @type {Number} + */ + eventPhase = Event.NONE; + static NONE = 0; // The event is not being processed + static CAPTURING_PHASE = 1; // The event is being propagated through the target's ancestor objects + static AT_TARGET = 2; // The event has arrived at the event's target + static BUBBLING_PHASE = 3; // The event is propagating back up through the target's ancestors in reverse order, starting with the parent + + /** + * Indicates whether the event was initiated by the browser or by a script. + * @type {Boolean} + */ + isTrusted = false; + + /** + * A reference to the object to which the event was originally dispatched. + * @type {EventTarget} + */ + target = null; + + /** + * The time at which the event was created (in milliseconds). By specification, this value is time since epoch. + * @type {Number} + */ + timeStamp = null; + + /** + * The name identifying the type of the event. + * @type {String} + */ + type = ''; + + /** + * @param {string} type A string with the name of the event. + */ + constructor(type) + { + this.type = type; + } + + // TODO: missing instance methods: Event.preventDefault(), Event.composedPath(), Event.stopImmediatePropagation(), Event.stopPropagation() + // See https://developer.mozilla.org/en-US/docs/Web/API/Event#instance_methods + // Also we need to figure out how we could handle event bubbling and capture in the PythonMonkey environment. +} + +/** + * @typedef {(ev: Event) => void} EventListenerFn + * @typedef {{handleEvent(ev: Event): void}} EventListenerObj + * @typedef {EventListenerFn | EventListenerObj} EventListener + */ + +class EventTarget +{ + /** + * @readonly + * @type {{ [type: string]: Set }} + */ + #listeners = Object.create(null); + + /** + * Add an event listener that will be called whenever the specified event is delivered to the target + * @param {string} type A case-sensitive string representing the event type to listen for + * @param {EventListener | null} listener The object that receives a notification when an event of the specified type occurs + */ + addEventListener(type, listener) + { + if (!listener) + return; + + if (!Object.hasOwn(this.#listeners, type)) + this.#listeners[type] = new Set(); + + this.#listeners[type].add(listener); + } + + /** + * Remove an event listener previously registered with EventTarget.addEventListener() from the target. + * @param {string} type A string which specifies the type of event for which to remove an event listener. + * @param {EventListener} listener The event listener function of the event handler to remove from the event target. + */ + removeEventListener(type, listener) + { + if (Object.hasOwn(this.#listeners, type)) + this.#listeners[type].delete(listener); + } + + /** + * Send an Event to the target, (synchronously) invoking the affected event listeners in the appropriate order. + * @param {Event} event The Event object to dispatch + */ + dispatchEvent(event) + { + debug((event.debugTag || '') + 'events:dispatch')(event.constructor.name, event.type); + // Set the Event.target property to the current EventTarget + event.target = this; + + const type = event.type; + if (!type) + return; + + // Call "on" methods + if (typeof this['on' + type] === 'function') + this['on' + type].call(this, event); + + // Call listeners registered with addEventListener() + if (!Object.hasOwn(this.#listeners, type)) + return; + for (const listener of this.#listeners[type].values()) + if (typeof listener === 'function') + listener.call(this, event); + else + listener.handleEvent.call(this, event); + } + + /** + * Determine whether the target has any kind of event listeners + */ + _hasAnyListeners() + { + return Object.values(this.#listeners).some(t => t.size > 0); + } + + /** + * Determine whether the target has listeners of the given event type + * @param {string} [type] + */ + _hasListeners(type) + { + return this.#listeners[type] && this.#listeners[type].size > 0; + } +} + +if (!globalThis.Event) + globalThis.Event = Event; +if (!globalThis.EventTarget) + globalThis.EventTarget = EventTarget; + +exports.Event = Event; +exports.EventTarget = EventTarget; diff --git a/python/pythonmonkey/builtin_modules/internal-binding.d.ts b/python/pythonmonkey/builtin_modules/internal-binding.d.ts index b7c3ac08..8caffe84 100644 --- a/python/pythonmonkey/builtin_modules/internal-binding.d.ts +++ b/python/pythonmonkey/builtin_modules/internal-binding.d.ts @@ -2,11 +2,13 @@ * @file internal-binding.d.ts * @author Tom Tang * @date June 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ /** * Note: `internalBinding` APIs are generally unsafe as they do not perform argument type checking, etc. - * Argument checking should be done in JavaScript side. + * Argument checking should be done on the JavaScript side. */ declare function internalBinding(namespace: string): any; // catch-all @@ -30,19 +32,48 @@ declare function internalBinding(namespace: "utils"): { getProxyDetails(proxy: T): undefined | [target: T, handler: ProxyHandler]; }; +declare type TimerDebugInfo = object; + declare function internalBinding(namespace: "timers"): { /** - * internal binding helper for the `setTimeout` global function + * internal binding helper for the `setTimeout`/`setInterval` global functions * * **UNSAFE**, does not perform argument type checks + * + * @param repeat The call is to `setInterval` if true * @return timeoutId */ - enqueueWithDelay(handler: Function, delaySeconds: number): number; + enqueueWithDelay(handler: Function, delaySeconds: number, repeat: boolean, debugInfo?: TimerDebugInfo): number; /** * internal binding helper for the `clearTimeout` global function */ cancelByTimeoutId(timeoutId: number): void; + + /** + * internal binding helper for if a timer object has been ref'ed + */ + timerHasRef(timeoutId: number): boolean; + + /** + * internal binding helper for ref'ing the timer + */ + timerAddRef(timeoutId: number): void; + + /** + * internal binding helper for unref'ing the timer + */ + timerRemoveRef(timeoutId: number): void; + + /** + * Retrieve debug info inside the timer for the WTFPythonMonkey tool + */ + getDebugInfo(timeoutId: number): TimerDebugInfo; + + /** + * Retrieve the debug info for all timers that are still ref'ed + */ + getAllRefedTimersDebugInfo(): TimerDebugInfo[]; }; export = internalBinding; diff --git a/python/pythonmonkey/builtin_modules/internal-binding.py b/python/pythonmonkey/builtin_modules/internal-binding.py index d9003662..b4231a2b 100644 --- a/python/pythonmonkey/builtin_modules/internal-binding.py +++ b/python/pythonmonkey/builtin_modules/internal-binding.py @@ -2,9 +2,9 @@ Re-export `internalBinding` to JS """ -import pythonmonkey as pm +import pythonmonkey as pm """ See function declarations in ./internal-binding.d.ts """ -exports = pm.internalBinding # type: ignore +exports = pm.internalBinding # type: ignore diff --git a/python/pythonmonkey/builtin_modules/timers.js b/python/pythonmonkey/builtin_modules/timers.js index 862bccc1..6fefc71c 100644 --- a/python/pythonmonkey/builtin_modules/timers.js +++ b/python/pythonmonkey/builtin_modules/timers.js @@ -2,24 +2,106 @@ * @file timers.js * @author Tom Tang * @date May 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ const internalBinding = require('internal-binding'); const { enqueueWithDelay, - cancelByTimeoutId + cancelByTimeoutId, + timerHasRef, + timerAddRef, + timerRemoveRef, } = internalBinding('timers'); +const { DOMException } = require('dom-exception'); + /** - * Implement the `setTimeout` global function - * @see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout and - * @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout + * Implement Node.js-style `timeoutId` class returned from setTimeout() and setInterval() + * @see https://nodejs.org/api/timers.html#class-timeout + */ +class Timeout +{ + /** @type {number} an integer */ + #numericId; + + /** + * @param {number} numericId + */ + constructor(numericId) + { + this.#numericId = numericId; + } + + /** + * If `true`, the `Timeout` object will keep the event-loop active. + * @returns {boolean} + */ + hasRef() + { + return timerHasRef(this.#numericId); + } + + /** + * When called, requests that the event-loop **not exit** so long as the `Timeout` is active. + * + * By default, all `Timeout` objects are "ref'ed", making it normally unnecessary to call `timeout.ref()` unless `timeout.unref()` had been called previously. + */ + ref() + { + timerAddRef(this.#numericId); + return this; // allow chaining + } + + /** + * When called, the active `Timeout` object will not require the event-loop to remain active. + * If there is no other activity keeping the event-loop running, the process may exit before the `Timeout` object's callback is invoked. + */ + unref() + { + timerRemoveRef(this.#numericId); + return this; // allow chaining + } + + /** + * Sets the timer's start time to the current time, + * and reschedules the timer to call its callback at the previously specified duration adjusted to the current time. + * + * Using this on a timer that has already called its callback will reactivate the timer. + */ + refresh() + { + throw new DOMException('Timeout.refresh() method is not supported by PythonMonkey.', 'NotSupportedError'); + // TODO: Do we really need to closely resemble the Node.js API? + // This one is not easy to implement since we need to memorize the callback function and delay parameters in every `setTimeout`/`setInterval` call. + } + + /** + * Cancels the timeout. + * @deprecated legacy Node.js API. Use `clearTimeout()` instead + */ + close() + { + return clearTimeout(this); + } + + /** + * @returns a number that can be used to reference this timeout + */ + [Symbol.toPrimitive]() + { + return this.#numericId; + } +} + +/** + * Normalize the arguments to `setTimeout`,`setImmediate` or `setInterval` * @param {Function | string} handler - * @param {number} delayMs timeout milliseconds, use value of 0 if this is omitted - * @param {any[]} args additional arguments to be passed to the `handler` - * @return {number} timeoutId + * @param {number} delayMs timeout milliseconds + * @param {any[]} additionalArgs additional arguments to be passed to the `handler` */ -function setTimeout(handler, delayMs = 0, ...args) +function _normalizeTimerArgs(handler, delayMs, additionalArgs) { // Ensure the first parameter is a function // We support passing a `code` string to `setTimeout` as the callback function @@ -32,7 +114,7 @@ function setTimeout(handler, delayMs = 0, ...args) // Wrap the job function into a bound function with the given additional arguments // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind /** @type {Function} */ - const boundHandler = handler.bind(thisArg, ...args); + const boundHandler = handler.bind(thisArg, ...additionalArgs); // Get the delay time in seconds // JS `setTimeout` takes milliseconds, but Python takes seconds @@ -41,25 +123,120 @@ function setTimeout(handler, delayMs = 0, ...args) delayMs = 0; // as spec-ed const delaySeconds = delayMs / 1000; // convert ms to s - return enqueueWithDelay(boundHandler, delaySeconds); + // Populate debug information for the WTFPythonMonkey tool + const stacks = new Error().stack.split('\n'); + const timerType = stacks[1]?.match(/^set(Timeout|Immediate|Interval)/)?.[0]; // `setTimeout@...`/`setImmediate@...`/`setInterval@...` is on the second line of stack trace + const debugInfo = { + type: timerType, // "setTimeout", "setImmediate", or "setInterval" + fn: handler, + args: additionalArgs, + startTime: new Date(), + delaySeconds, + stack: stacks.slice(2).join('\n'), // remove the first line `_normalizeTimerArgs@...` and the second line `setTimeout/setImmediate/setInterval@...` + }; + + return { boundHandler, delaySeconds, debugInfo }; +} + +/** + * Implement the `setTimeout` global function + * @see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout and + * @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout + * @param {Function | string} handler + * @param {number} delayMs timeout milliseconds, use value of 0 if this is omitted + * @param {any[]} args additional arguments to be passed to the `handler` + * @return {Timeout} timeoutId + */ +function setTimeout(handler, delayMs = 0, ...args) +{ + const { boundHandler, delaySeconds, debugInfo } = _normalizeTimerArgs(handler, delayMs, args); + return new Timeout(enqueueWithDelay(boundHandler, delaySeconds, false, debugInfo)); } /** * Implement the `clearTimeout` global function * @see https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout and * @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-cleartimeout - * @param {number} timeoutId + * @param {Timeout | number} timeoutId * @return {void} */ function clearTimeout(timeoutId) { - return cancelByTimeoutId(timeoutId); + // silently does nothing when an invalid timeoutId (should be a Timeout instance or an int32 value) is passed in + if (!(timeoutId instanceof Timeout) && !Number.isInteger(timeoutId)) + return; + + return cancelByTimeoutId(Number(timeoutId)); +} + +/** + * Implement the `setImmediate` global function + * **NON-STANDARD**, for Node.js compatibility only. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate and + * @see https://nodejs.org/en/learn/asynchronous-work/understanding-setimmediate + * @param {Function | string} handler + * @param {any[]} args additional arguments to be passed to the `handler` + */ +function setImmediate(handler, ...args) +{ + // setImmediate is equal to setTimeout with a 0ms delay + const { boundHandler, debugInfo } = _normalizeTimerArgs(handler, 0, args); + return new Timeout(enqueueWithDelay(boundHandler, 0, false, debugInfo)); +} + +/** + * Implement the `clearImmediate` global function + * **NON-STANDARD**, for Node.js compatibility only. + * @alias to `clearTimeout` + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/clearImmediate + */ +const clearImmediate = clearTimeout; + +/** + * Implement the `setInterval` global function + * @see https://developer.mozilla.org/en-US/docs/Web/API/setInterval and + * @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval + * @param {Function | string} handler + * @param {number} delayMs timeout milliseconds, use value of 0 if this is omitted + * @param {any[]} args additional arguments to be passed to the `handler` + * @return {Timeout} timeoutId + */ +function setInterval(handler, delayMs = 0, ...args) +{ + const { boundHandler, delaySeconds, debugInfo } = _normalizeTimerArgs(handler, delayMs, args); + return new Timeout(enqueueWithDelay(boundHandler, delaySeconds, true, debugInfo)); } +/** + * Implement the `clearInterval` global function + * @alias to `clearTimeout` + * @see https://developer.mozilla.org/en-US/docs/Web/API/clearInterval + */ +const clearInterval = clearTimeout; + +// expose the `Timeout` class +setTimeout.Timeout = Timeout; +setImmediate.Timeout = Timeout; +setInterval.Timeout = Timeout; + if (!globalThis.setTimeout) globalThis.setTimeout = setTimeout; if (!globalThis.clearTimeout) globalThis.clearTimeout = clearTimeout; +if (!globalThis.setImmediate) + globalThis.setImmediate = setImmediate; +if (!globalThis.clearImmediate) + globalThis.clearImmediate = clearImmediate; + +if (!globalThis.setInterval) + globalThis.setInterval = setInterval; +if (!globalThis.clearInterval) + globalThis.clearInterval = clearInterval; + exports.setTimeout = setTimeout; exports.clearTimeout = clearTimeout; +exports.setImmediate = setImmediate; +exports.clearImmediate = clearImmediate; +exports.setInterval = setInterval; +exports.clearInterval = clearInterval; diff --git a/python/pythonmonkey/builtin_modules/url.d.ts b/python/pythonmonkey/builtin_modules/url.d.ts new file mode 100644 index 00000000..8e4baa62 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/url.d.ts @@ -0,0 +1,111 @@ +/** + * @file url.d.ts + * Type definitions for URL/URLSearchParams + * + * @author Tom Tang + * @date August 2023 + */ + +/*! + * Modified from https://www.npmjs.com/package/@types/web + * Apache License 2.0 + */ + +/** + * The URL interface represents an object providing static methods used for creating object URLs. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL) + */ +export interface URL { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) */ + hash: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) */ + host: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) */ + hostname: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) */ + href: string; + toString(): string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin) */ + readonly origin: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) */ + password: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) */ + pathname: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) */ + port: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) */ + protocol: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) */ + search: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams) */ + readonly searchParams: URLSearchParams; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) */ + username: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON) */ + toJSON(): string; +} + +export declare var URL: { + prototype: URL; + new(url: string | URL, base?: string | URL): URL; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) */ + canParse(url: string | URL, base?: string): boolean; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static) @deprecated not implemented by polyfill */ + // @ts-expect-error types not defined + createObjectURL(obj: Blob | MediaSource): string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static) @deprecated not implemented by polyfill */ + revokeObjectURL(url: string): void; +}; + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams) */ +export interface URLSearchParams { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) */ + readonly size: number; + /** + * Appends a specified key/value pair as a new search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append) + */ + append(name: string, value: string): void; + /** + * Deletes the given search parameter, and its associated value, from the list of all search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete) + */ + delete(name: string, value?: string): void; + /** + * Returns the first value associated to the given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) + */ + get(name: string): string | null; + /** + * Returns all the values association with a given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) + */ + getAll(name: string): string[]; + /** + * Returns a Boolean indicating if such a search parameter exists. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) + */ + has(name: string, value?: string): boolean; + /** + * Sets the value associated to a given search parameter to the given value. If there were several values, delete the others. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) + */ + set(name: string, value: string): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) */ + sort(): void; + /** Returns a string containing a query string suitable for use in a URL. Does not include the question mark. */ + toString(): string; + forEach(callbackfn: (value: string, key: string, parent: URLSearchParams) => void, thisArg?: any): void; +} + +export declare var URLSearchParams: { + prototype: URLSearchParams; + new(init?: string[][] | Record | string | URLSearchParams): URLSearchParams; +}; diff --git a/python/pythonmonkey/builtin_modules/url.js b/python/pythonmonkey/builtin_modules/url.js new file mode 100644 index 00000000..1419b5b1 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/url.js @@ -0,0 +1,17 @@ +/** + * @file url.js + * Polyfill the URL and URLSearchParams interfaces + * + * @author Tom Tang + * @date August 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. + */ + +// Apply polyfills from core-js +require('./dom-exception'); +require('core-js/actual/url'); +require('core-js/actual/url-search-params'); + +exports.URL = globalThis.URL; +exports.URLSearchParams = globalThis.URLSearchParams; diff --git a/python/pythonmonkey/builtin_modules/util.js b/python/pythonmonkey/builtin_modules/util.js index dbe2fccb..358bfcf6 100644 --- a/python/pythonmonkey/builtin_modules/util.js +++ b/python/pythonmonkey/builtin_modules/util.js @@ -5,9 +5,11 @@ * * @author Tom Tang * @date June 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ -const internalBinding = require("internal-binding") +const internalBinding = require('internal-binding'); const { isAnyArrayBuffer, @@ -16,14 +18,15 @@ const { isTypedArray, getPromiseDetails, getProxyDetails, -} = internalBinding("utils") +} = internalBinding('utils'); -const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); // Keep this in sync with both SpiderMonkey and V8 /** @type {PromiseState.Pending} */ const kPending = 0; /** @type {PromiseState.Fulfilled} */ +// eslint-disable-next-line no-unused-vars const kFulfilled = 1; /** @type {PromiseState.Rejected} */ const kRejected = 2; @@ -32,42 +35,48 @@ const kRejected = 2; * @param {string[]} output * @param {string} separator */ -function join(output, separator) { - return output.join(separator) +function join(output, separator) +{ + return output.join(separator); } /** * @return {o is Error} */ -function isError(e) { +function isError(e) +{ return objectToString(e) === '[object Error]' || e instanceof Error; } /** * @return {o is Date} */ -function isDate(o) { +function isDate(o) +{ return objectToString(o) === '[object Date]' || o instanceof Date; } /** * @return {o is Set} */ -function isSet(o) { +function isSet(o) +{ return objectToString(o) === '[object Set]' || o instanceof Set; } /** * @return {o is Map} */ -function isMap(o) { +function isMap(o) +{ return objectToString(o) === '[object Map]' || o instanceof Map; } /** * @return {o is DataView} */ -function isDataView(o) { +function isDataView(o) +{ return objectToString(o) === '[object DataView]' || o instanceof DataView; } @@ -75,22 +84,26 @@ function isDataView(o) { * V8 IsJSExternalObject */ // TODO (Tom Tang): What's the equivalent in SpiderMonkey? -function isExternal(o) { +function isExternal(o) +{ return false; } -function objectToString(o) { +function objectToString(o) +{ return Object.prototype.toString.call(o); } // https://github.com/nodejs/node/blob/v8.17.0/lib/internal/util.js#L189-L202 -// MIT License -function getConstructorOf(obj) { - while (obj) { - var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); - if (descriptor !== undefined && - typeof descriptor.value === 'function' && - descriptor.value.name !== '') { +function getConstructorOf(obj) +{ + while (obj) + { + let descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); + if (descriptor !== undefined + && typeof descriptor.value === 'function' + && descriptor.value.name !== '') + { return descriptor.value; } @@ -100,10 +113,8 @@ function getConstructorOf(obj) { return null; } -/*! - * Modified from https://github.com/nodejs/node/blob/v8.17.0/lib/util.js#L59-L852 - * Node.js - * MIT License, Copyright Joyent, Inc. and other Node contributors. +/* ! Start verbatim Node.js + * https://github.com/nodejs/node/blob/v8.17.0/lib/util.js#L59-L852 */ const inspectDefaultOptions = Object.seal({ showHidden: false, @@ -120,20 +131,21 @@ const regExpToString = RegExp.prototype.toString; const dateToISOString = Date.prototype.toISOString; /** Return String(val) surrounded by appropriate ANSI escape codes to change the console text colour. */ +// eslint-disable-next-line no-unused-vars function colour(colourCode, val) { const esc=String.fromCharCode(27); - return `${esc}[${colourCode}m${val}${esc}[0m` + return `${esc}[${colourCode}m${val}${esc}[0m`; } -var CIRCULAR_ERROR_MESSAGE; +let CIRCULAR_ERROR_MESSAGE; /* eslint-disable */ const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; +const colorRegExp = /\u001b\[\d\d?m/g; /* eslint-enable */ const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; -const colorRegExp = /\u001b\[\d\d?m/g; const numberRegExp = /^(0|[1-9][0-9]*)$/; // Escaped special characters. Use empty strings to fill up unused entries. @@ -157,43 +169,61 @@ const escapeFn = (str) => meta[str.charCodeAt(0)]; // Escape control characters, single quotes and the backslash. // This is similar to JSON stringify escaping. -function strEscape(str) { +function strEscape(str) +{ // Some magic numbers that worked out fine while benchmarking with v8 6.0 if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) return `'${str}'`; if (str.length > 100) return `'${str.replace(strEscapeSequencesReplacer, escapeFn)}'`; - var result = ''; - var last = 0; - for (var i = 0; i < str.length; i++) { + let result = ''; + let last = 0; + let i; + for (i = 0; i < str.length; i++) + { const point = str.charCodeAt(i); - if (point === 39 || point === 92 || point < 32) { - if (last === i) { + if (point === 39 || point === 92 || point < 32) + { + if (last === i) + { result += meta[point]; - } else { + } + else + { result += `${str.slice(last, i)}${meta[point]}`; } last = i + 1; } } - if (last === 0) { + if (last === 0) + { result = str; - } else if (last !== i) { + } + else if (last !== i) + { result += str.slice(last); } return `'${result}'`; } -function tryStringify(arg) { - try { +function tryStringify(arg) +{ + try + { return JSON.stringify(arg); - } catch (err) { + } + catch (err) + { // Populate the circular error message lazily - if (!CIRCULAR_ERROR_MESSAGE) { - try { + if (!CIRCULAR_ERROR_MESSAGE) + { + try + { const a = {}; a.a = a; JSON.stringify(a); - } catch (err) { - CIRCULAR_ERROR_MESSAGE = err.message; + } + catch (err2) + { + CIRCULAR_ERROR_MESSAGE = err2.message; } } if (err.name === 'TypeError' && err.message === CIRCULAR_ERROR_MESSAGE) @@ -202,12 +232,15 @@ function tryStringify(arg) { } } -function format(f) { - var i, tempStr; - if (typeof f !== 'string') { +function format(f) +{ + let i, tempStr; + if (typeof f !== 'string') + { if (arguments.length === 0) return ''; - var res = ''; - for (i = 0; i < arguments.length - 1; i++) { + let res = ''; + for (i = 0; i < arguments.length - 1; i++) + { res += inspect(arguments[i]); res += ' '; } @@ -217,14 +250,18 @@ function format(f) { if (arguments.length === 1) return f; - var str = ''; - var a = 1; - var lastPos = 0; - for (i = 0; i < f.length - 1; i++) { - if (f.charCodeAt(i) === 37) { // '%' + let str = ''; + let a = 1; + let lastPos = 0; + for (i = 0; i < f.length - 1; i++) + { + if (f.charCodeAt(i) === 37) + { // '%' const nextChar = f.charCodeAt(++i); - if (a !== arguments.length) { - switch (nextChar) { + if (a !== arguments.length) + { + switch (nextChar) + { case 115: // 's' tempStr = String(arguments[a++]); break; @@ -242,7 +279,7 @@ function format(f) { { showHidden: true, depth: 4, showProxy: true }); break; case 105: // 'i' - tempStr = `${parseInt(arguments[a++])}`; + tempStr = `${parseInt(arguments[a++], 10)}`; break; case 102: // 'f' tempStr = `${parseFloat(arguments[a++])}`; @@ -258,7 +295,9 @@ function format(f) { str += f.slice(lastPos, i - 1); str += tempStr; lastPos = i + 1; - } else if (nextChar === 37) { + } + else if (nextChar === 37) + { str += f.slice(lastPos, i); lastPos = i + 1; } @@ -268,11 +307,15 @@ function format(f) { str = f; else if (lastPos < f.length) str += f.slice(lastPos); - while (a < arguments.length) { + while (a < arguments.length) + { const x = arguments[a++]; - if ((typeof x !== 'object' && typeof x !== 'symbol') || x === null) { + if ((typeof x !== 'object' && typeof x !== 'symbol') || x === null) + { str += ` ${x}`; - } else { + } + else + { str += ` ${inspect(x)}`; } } @@ -287,7 +330,8 @@ function format(f) { * @param {Object} opts Optional options object that alters the output. */ /* Legacy: obj, showHidden, depth, colors*/ -function inspect(obj, opts = undefined) { +function inspect(obj, opts = undefined) +{ // Default options const ctx = { seen: [], @@ -302,20 +346,27 @@ function inspect(obj, opts = undefined) { indentationLvl: 0 }; // Legacy... - if (arguments.length > 2) { - if (arguments[2] !== undefined) { + if (arguments.length > 2) + { + if (arguments[2] !== undefined) + { ctx.depth = arguments[2]; } - if (arguments.length > 3 && arguments[3] !== undefined) { + if (arguments.length > 3 && arguments[3] !== undefined) + { ctx.colors = arguments[3]; } } // Set user-specified options - if (typeof opts === 'boolean') { + if (typeof opts === 'boolean') + { ctx.showHidden = opts; - } else if (opts) { + } + else if (opts) + { const optKeys = Object.keys(opts); - for (var i = 0; i < optKeys.length; i++) { + for (let i = 0; i < optKeys.length; i++) + { ctx[optKeys[i]] = opts[optKeys[i]]; } } @@ -326,15 +377,17 @@ function inspect(obj, opts = undefined) { inspect.custom = customInspectSymbol; Object.defineProperty(inspect, 'defaultOptions', { - get() { + get() + { return inspectDefaultOptions; }, - set(options) { - if (options === null || typeof options !== 'object') { + set(options) + { + if (options === null || typeof options !== 'object') + { throw new TypeError('"options" must be an object'); } Object.assign(inspectDefaultOptions, options); - return inspectDefaultOptions; } }); @@ -370,32 +423,41 @@ inspect.styles = Object.assign(Object.create(null), { 'error2': 'grey', }); -function stylizeWithColor(str, styleType) { +function stylizeWithColor(str, styleType) +{ const style = inspect.styles[styleType]; - if (style !== undefined) { + if (style !== undefined) + { const color = inspect.colors[style]; return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`; } return str; } -function stylizeNoColor(str, styleType) { +function stylizeNoColor(str, styleType) +{ return str; } -function formatValue(ctx, value, recurseTimes, ln) { +function formatValue(ctx, value, recurseTimes, ln) +{ // Primitive types cannot have properties - if (typeof value !== 'object' && typeof value !== 'function') { + if (typeof value !== 'object' && typeof value !== 'function') + { return formatPrimitive(ctx.stylize, value); } - if (value === null) { + if (value === null) + { return ctx.stylize('null', 'null'); } - if (ctx.showProxy) { + if (ctx.showProxy) + { const proxy = getProxyDetails(value); - if (proxy !== undefined) { - if (recurseTimes != null) { + if (proxy !== undefined) + { + if (recurseTimes !== null) + { if (recurseTimes < 0) return ctx.stylize('Proxy [Array]', 'special'); recurseTimes -= 1; @@ -413,20 +475,24 @@ function formatValue(ctx, value, recurseTimes, ln) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it - if (ctx.customInspect) { + if (ctx.customInspect) + { const maybeCustomInspect = value[customInspectSymbol] || value.inspect; - if (typeof maybeCustomInspect === 'function' && + if (typeof maybeCustomInspect === 'function' // Filter out the util module, its inspect function is special - maybeCustomInspect !== inspect && + && maybeCustomInspect !== inspect // Also filter out any prototype objects using the circular check. - !(value.constructor && value.constructor.prototype === value)) { + && !(value.constructor && value.constructor.prototype === value)) + { const ret = maybeCustomInspect.call(value, recurseTimes, ctx); // If the custom inspection method returned `this`, don't go into // infinite recursion. - if (ret !== value) { - if (typeof ret !== 'string') { + if (ret !== value) + { + if (typeof ret !== 'string') + { return formatValue(ctx, ret, recurseTimes); } return ret; @@ -434,13 +500,16 @@ function formatValue(ctx, value, recurseTimes, ln) { } } - var keys; - var symbols = Object.getOwnPropertySymbols(value); + let keys; + let symbols = Object.getOwnPropertySymbols(value); // Look up the keys of the object. - if (ctx.showHidden) { + if (ctx.showHidden) + { keys = Object.getOwnPropertyNames(value); - } else { + } + else + { keys = Object.keys(value); if (symbols.length !== 0) symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key)); @@ -448,35 +517,43 @@ function formatValue(ctx, value, recurseTimes, ln) { const keyLength = keys.length + symbols.length; const constructor = getConstructorOf(value); - const ctorName = constructor && constructor.name ? - `${constructor.name} ` : ''; + const ctorName = constructor && constructor.name + ? `${constructor.name} ` : ''; - var base = ''; - var formatter = formatObject; - var braces; - var noIterator = true; - var raw; + let base = ''; + let formatter = formatObject; + let braces; + let noIterator = true; + let raw; // Iterators and the rest are split to reduce checks - if (value[Symbol.iterator]) { + if (value[Symbol.iterator]) + { noIterator = false; - if (Array.isArray(value)) { + if (Array.isArray(value)) + { // Only set the constructor for non ordinary ("Array [...]") arrays. braces = [`${ctorName === 'Array ' ? '' : ctorName}[`, ']']; if (value.length === 0 && keyLength === 0) return `${braces[0]}]`; formatter = formatArray; - } else if (isSet(value)) { + } + else if (isSet(value)) + { if (value.size === 0 && keyLength === 0) return `${ctorName}{}`; braces = [`${ctorName}{`, '}']; formatter = formatSet; - } else if (isMap(value)) { + } + else if (isMap(value)) + { if (value.size === 0 && keyLength === 0) return `${ctorName}{}`; braces = [`${ctorName}{`, '}']; formatter = formatMap; - } else if (isTypedArray(value)) { + } + else if (isTypedArray(value)) + { braces = [`${ctorName}[`, ']']; formatter = formatTypedArray; // } else if (isMapIterator(value)) { @@ -485,14 +562,20 @@ function formatValue(ctx, value, recurseTimes, ln) { // } else if (isSetIterator(value)) { // braces = ['SetIterator {', '}']; // formatter = formatCollectionIterator; - } else { + } + else + { // Check for boxed strings with valueOf() // The .valueOf() call can fail for a multitude of reasons - try { + try + { raw = value.valueOf(); - } catch (e) { /* ignore */ } + } + catch (e) + { /* ignore */ } - if (typeof raw === 'string') { + if (typeof raw === 'string') + { const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === raw.length) return ctx.stylize(`[String: ${formatted}]`, 'string'); @@ -502,85 +585,119 @@ function formatValue(ctx, value, recurseTimes, ln) { // Make boxed primitive Strings look like such keys = keys.slice(value.length); braces = ['{', '}']; - } else { + } + else + { noIterator = true; } } } - if (noIterator) { + if (noIterator) + { braces = ['{', '}']; - if (ctorName === 'Object ') { + if (ctorName === 'Object ') + { // Object fast path if (keyLength === 0) return '{}'; - } else if (typeof value === 'function') { + } + else if (typeof value === 'function') + { const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`; if (keyLength === 0) return ctx.stylize(`[${name}]`, 'special'); base = ` [${name}]`; - } else if (isRegExp(value)) { + } + else if (isRegExp(value)) + { // Make RegExps say that they are RegExps if (keyLength === 0 || recurseTimes < 0) return ctx.stylize(regExpToString.call(value), 'regexp'); base = ` ${regExpToString.call(value)}`; - } else if (isDate(value)) { - if (keyLength === 0) { + } + else if (isDate(value)) + { + if (keyLength === 0) + { if (Number.isNaN(value.getTime())) return ctx.stylize(value.toString(), 'date'); return ctx.stylize(dateToISOString.call(value), 'date'); } // Make dates with properties first say the date base = ` ${dateToISOString.call(value)}`; - } else if (isError(value)) { + } + else if (isError(value)) + { // Make error with message first say the error - if (keyLength === 0) + if (keyLength === 0 || keys.every(k => k === 'stack' || k === 'name')) // There's only a 'stack' or 'name' property return formatError(ctx, value); + keys = keys.filter(k => k !== 'stack' && k !== 'name'); // When changing the 'stack' or the 'name' property in SpiderMonkey, it becomes enumerable. base = ` ${formatError(ctx, value)}\n`; braces.length=0; - } else if (isAnyArrayBuffer(value)) { + } + else if (isAnyArrayBuffer(value)) + { // Fast path for ArrayBuffer and SharedArrayBuffer. // Can't do the same for DataView because it has a non-primitive // .buffer property that we need to recurse for. if (keyLength === 0) - return ctorName + - `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; + return ctorName + + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; braces[0] = `${ctorName}{`; keys.unshift('byteLength'); - } else if (isDataView(value)) { + } + else if (isDataView(value)) + { braces[0] = `${ctorName}{`; // .buffer goes last, it's not a primitive like the others. keys.unshift('byteLength', 'byteOffset', 'buffer'); - } else if (isPromise(value)) { + } + else if (isPromise(value)) + { braces[0] = `${ctorName}{`; formatter = formatPromise; - } else { + } + else + { // Check boxed primitives other than string with valueOf() // NOTE: `Date` has to be checked first! // The .valueOf() call can fail for a multitude of reasons - try { + try + { raw = value.valueOf(); - } catch (e) { /* ignore */ } + } + catch (e) + { /* ignore */ } - if (typeof raw === 'number') { + if (typeof raw === 'number') + { // Make boxed primitive Numbers look like such const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === 0) return ctx.stylize(`[Number: ${formatted}]`, 'number'); base = ` [Number: ${formatted}]`; - } else if (typeof raw === 'boolean') { + } + else if (typeof raw === 'boolean') + { // Make boxed primitive Booleans look like such const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === 0) return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); base = ` [Boolean: ${formatted}]`; - } else if (typeof raw === 'symbol') { + } + else if (typeof raw === 'symbol') + { const formatted = formatPrimitive(stylizeNoColor, raw); return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); - } else if (keyLength === 0) { + } + else if (keyLength === 0) + { if (isExternal(value)) return ctx.stylize('[External]', 'special'); return `${ctorName}{}`; - } else { + } + else + { braces[0] = `${ctorName}{`; } } @@ -591,8 +708,10 @@ function formatValue(ctx, value, recurseTimes, ln) { if (ctx.seen.indexOf(value) !== -1) return ctx.stylize('[Circular]', 'special'); - if (recurseTimes != null) { - if (recurseTimes < 0) { + if (recurseTimes !== null) + { + if (recurseTimes < 0) + { if (Array.isArray(value)) return ctx.stylize('[Array]', 'special'); return ctx.stylize('[Object]', 'special'); @@ -603,7 +722,8 @@ function formatValue(ctx, value, recurseTimes, ln) { ctx.seen.push(value); const output = formatter(ctx, value, recurseTimes, keys); - for (var i = 0; i < symbols.length; i++) { + for (let i = 0; i < symbols.length; i++) + { output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); } ctx.seen.pop(); @@ -611,14 +731,16 @@ function formatValue(ctx, value, recurseTimes, ln) { return reduceToSingleString(ctx, output, base, braces, ln); } -function formatNumber(fn, value) { +function formatNumber(fn, value) +{ // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. if (Object.is(value, -0)) return fn('-0', 'number'); return fn(`${value}`, 'number'); } -function formatPrimitive(fn, value) { +function formatPrimitive(fn, value) +{ if (typeof value === 'string') return fn(strEscape(value), 'string'); if (typeof value === 'number') @@ -648,39 +770,46 @@ function formatError(ctx, error) } const stackEls = error.stack - .split('\n') - .filter(a => a.length > 0) - .map(a => ` ${a}`); - const retstr = - `${error.name}: ${error.message}\n` - + stackEls[0] + '\n' - + style(stackEls.slice(1).join('\n')); + .split('\n') + .filter(a => a.length > 0) + .map(a => ` ${a}`); + let retstr = `${error.name}: ${error.message}\n`; + if (stackEls.length) + { + retstr += stackEls[0] + '\n'; + if (stackEls.length > 1) retstr += style(stackEls.slice(1).join('\n')); + } return retstr; } -function formatObject(ctx, value, recurseTimes, keys) { +function formatObject(ctx, value, recurseTimes, keys) +{ const len = keys.length; const output = new Array(len); - for (var i = 0; i < len; i++) + for (let i = 0; i < len; i++) output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); return output; } // The array is sparse and/or has extra keys -function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { +function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) +{ const output = []; const keyLen = keys.length; - var visibleLength = 0; - var i = 0; - if (keyLen !== 0 && numberRegExp.test(keys[0])) { - for (const key of keys) { + let visibleLength = 0; + let i = 0; + if (keyLen !== 0 && numberRegExp.test(keys[0])) + { + for (const key of keys) + { if (visibleLength === maxLength) break; - const index = +key; + const index = Number(key); // Arrays can only have up to 2^32 - 1 entries if (index > 2 ** 32 - 2) break; - if (i !== index) { + if (i !== index) + { if (!numberRegExp.test(key)) break; const emptyItems = index - i; @@ -696,7 +825,8 @@ function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { i++; } } - if (i < valLen && visibleLength !== maxLength) { + if (i < valLen && visibleLength !== maxLength) + { const len = valLen - i; const ending = len > 1 ? 's' : ''; const message = `<${len} empty item${ending}>`; @@ -706,24 +836,31 @@ function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { return output; } const remaining = valLen - i; - if (remaining > 0) { + if (remaining > 0) + { output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } - if (ctx.showHidden && keys[keyLen - 1] === 'length') { + if (ctx.showHidden && keys[keyLen - 1] === 'length') + { // No extra keys output.push(formatProperty(ctx, value, recurseTimes, 'length', 2)); - } else if (valLen === 0 || - keyLen > valLen && keys[valLen - 1] === `${valLen - 1}`) { + } + else if (valLen === 0 + || keyLen > valLen && keys[valLen - 1] === `${valLen - 1}`) + { // The array is not sparse for (i = valLen; i < keyLen; i++) output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); - } else if (keys[keyLen - 1] !== `${valLen - 1}`) { + } + else if (keys[keyLen - 1] !== `${valLen - 1}`) + { const extra = []; // Only handle special keys - var key; - for (i = keys.length - 1; i >= 0; i--) { + let key; + for (i = keys.length - 1; i >= 0; i--) + { key = keys[i]; - if (numberRegExp.test(key) && +key < 2 ** 32 - 1) + if (numberRegExp.test(key) && Number(key) < 2 ** 32 - 1) break; extra.push(formatProperty(ctx, value, recurseTimes, key, 2)); } @@ -733,7 +870,8 @@ function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { return output; } -function formatArray(ctx, value, recurseTimes, keys) { +function formatArray(ctx, value, recurseTimes, keys) +{ const len = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const hidden = ctx.showHidden ? 1 : 0; const valLen = value.length; @@ -743,7 +881,8 @@ function formatArray(ctx, value, recurseTimes, keys) { const remaining = valLen - len; const output = new Array(len + (remaining > 0 ? 1 : 0) + hidden); - for (var i = 0; i < len; i++) + let i; + for (i = 0; i < len; i++) output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 1); if (remaining > 0) output[i++] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; @@ -752,15 +891,18 @@ function formatArray(ctx, value, recurseTimes, keys) { return output; } -function formatTypedArray(ctx, value, recurseTimes, keys) { +function formatTypedArray(ctx, value, recurseTimes, keys) +{ const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const remaining = value.length - maxLength; const output = new Array(maxLength + (remaining > 0 ? 1 : 0)); - for (var i = 0; i < maxLength; ++i) + let i; + for (i = 0; i < maxLength; ++i) output[i] = formatNumber(ctx.stylize, value[i]); if (remaining > 0) output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; - if (ctx.showHidden) { + if (ctx.showHidden) + { // .buffer goes last, it's not a primitive like the others. const extraKeys = [ 'BYTES_PER_ELEMENT', @@ -769,22 +911,25 @@ function formatTypedArray(ctx, value, recurseTimes, keys) { 'byteOffset', 'buffer' ]; - for (i = 0; i < extraKeys.length; i++) { + for (i = 0; i < extraKeys.length; i++) + { const str = formatValue(ctx, value[extraKeys[i]], recurseTimes); output.push(`[${extraKeys[i]}]: ${str}`); } } // TypedArrays cannot have holes. Therefore it is safe to assume that all // extra keys are indexed after value.length. - for (i = value.length; i < keys.length; i++) { + for (i = value.length; i < keys.length; i++) + { output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); } return output; } -function formatSet(ctx, value, recurseTimes, keys) { +function formatSet(ctx, value, recurseTimes, keys) +{ const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); - var i = 0; + let i = 0; for (const v of value) output[i++] = formatValue(ctx, v, recurseTimes); // With `showHidden`, `length` will display as a hidden property for @@ -792,86 +937,119 @@ function formatSet(ctx, value, recurseTimes, keys) { // property isn't selected by Object.getOwnPropertyNames(). if (ctx.showHidden) output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; - for (var n = 0; n < keys.length; n++) { + for (let n = 0; n < keys.length; n++) + { output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); } return output; } -function formatMap(ctx, value, recurseTimes, keys) { +function formatMap(ctx, value, recurseTimes, keys) +{ const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); - var i = 0; + let i = 0; for (const [k, v] of value) - output[i++] = `${formatValue(ctx, k, recurseTimes)} => ` + - formatValue(ctx, v, recurseTimes); + output[i++] = `${formatValue(ctx, k, recurseTimes)} => ` + + formatValue(ctx, v, recurseTimes); // See comment in formatSet if (ctx.showHidden) output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; - for (var n = 0; n < keys.length; n++) { + for (let n = 0; n < keys.length; n++) + { output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); } return output; } -function formatPromise(ctx, value, recurseTimes, keys) { - var output; +function formatPromise(ctx, value, recurseTimes, keys) +{ + let output; const [state, result] = getPromiseDetails(value); - if (state === kPending) { + if (state === kPending) + { output = ['']; - } else { + } + else + { const str = formatValue(ctx, result, recurseTimes); output = [state === kRejected ? ` ${str}` : str]; } - for (var n = 0; n < keys.length; n++) { + for (let n = 0; n < keys.length; n++) + { output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); } return output; } -function formatProperty(ctx, value, recurseTimes, key, array) { - var name, str; - const desc = Object.getOwnPropertyDescriptor(value, key) || - { value: value[key], enumerable: true }; - if (desc.value !== undefined) { +function formatProperty(ctx, value, recurseTimes, key, array) +{ + let name, str; + const desc = Object.getOwnPropertyDescriptor(value, key) + || { value: value[key], enumerable: true }; + if (desc.value !== undefined) + { const diff = array === 0 ? 3 : 2; ctx.indentationLvl += diff; str = formatValue(ctx, desc.value, recurseTimes, array === 0); ctx.indentationLvl -= diff; - } else if (desc.get !== undefined) { - if (desc.set !== undefined) { + } + else if (desc.get !== undefined) + { + if (desc.set !== undefined) + { str = ctx.stylize('[Getter/Setter]', 'special'); - } else { + } + else + { str = ctx.stylize('[Getter]', 'special'); } - } else if (desc.set !== undefined) { + } + else if (desc.set !== undefined) + { str = ctx.stylize('[Setter]', 'special'); - } else { + } + else + { str = ctx.stylize('undefined', 'undefined'); } - if (array === 1) { + if (array === 1) + { return str; } - if (typeof key === 'symbol') { + if (typeof key === 'symbol') + { name = `[${ctx.stylize(key.toString(), 'symbol')}]`; - } else if (desc.enumerable === false) { + } + else if (desc.enumerable === false) + { name = `[${key}]`; - } else if (keyStrRegExp.test(key)) { + } + else if (keyStrRegExp.test(key)) + { name = ctx.stylize(key, 'name'); - } else { + } + else + { name = ctx.stylize(strEscape(key), 'string'); } return `${name}: ${str}`; } -function reduceToSingleString(ctx, output, base, braces, addLn) { +function reduceToSingleString(ctx, output, base, braces, addLn) +{ const breakLength = ctx.breakLength; - if (output.length * 2 <= breakLength) { - var length = 0; - for (var i = 0; i < output.length && length <= breakLength; i++) { - if (ctx.colors) { + if (output.length * 2 <= breakLength) + { + let length = 0; + for (let i = 0; i < output.length && length <= breakLength; i++) + { + if (ctx.colors) + { length += output[i].replace(colorRegExp, '').length + 1; - } else { + } + else + { length += output[i].length + 1; } } @@ -888,19 +1066,19 @@ function reduceToSingleString(ctx, output, base, braces, addLn) { // items will not line up correctly. const indentation = ' '.repeat(ctx.indentationLvl); const extraLn = addLn === true ? `\n${indentation}` : ''; - const ln = base === '' && braces[0].length === 1 ? - ' ' : `${base}\n${indentation} `; + const ln = base === '' && braces[0].length === 1 + ? ' ' : `${base}\n${indentation} `; const str = join(output, `,\n${indentation} `); return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`; } -/*! - * End of Node.js code +/* ! + * End of verbatim Node.js excerpt */ module.exports = exports = { customInspectSymbol, format, inspect, -}; +}; \ No newline at end of file diff --git a/python/pythonmonkey/cli/pmjs.py b/python/pythonmonkey/cli/pmjs.py index f1bd1ac9..1e48f83e 100755 --- a/python/pythonmonkey/cli/pmjs.py +++ b/python/pythonmonkey/cli/pmjs.py @@ -2,20 +2,24 @@ # @file pmjs - PythonMonkey REPL # @author Wes Garland, wes@distributive.network # @date June 2023 +# @copyright Copyright (c) 2023 Distributive Corp. -import sys, os, signal, getopt +import sys +import os +import signal +import getopt import readline import asyncio import pythonmonkey as pm -from pythonmonkey.lib import pmdb +from pythonmonkey.lib import pmdb, wtfpm globalThis = pm.eval("globalThis") -evalOpts = { 'filename': __file__, 'fromPythonFrame': True, 'strict': False } # type: pm.EvalOptions +evalOpts = {'filename': __file__, 'fromPythonFrame': True, 'strict': False} # type: pm.EvalOptions if (os.getenv('PMJS_PATH')): - requirePath = list(map(os.path.abspath, os.getenv('PMJS_PATH').split(','))) + requirePath = list(map(os.path.abspath, os.getenv('PMJS_PATH').split(','))) else: - requirePath = False; + requirePath = False pm.eval("""'use strict'; const cmds = {}; @@ -35,6 +39,7 @@ cmds.exit = python.exit; cmds.python = function pythonCmd(...args) { + var retval; const cmd = args.join(' ').trim(); if (cmd === 'reset') @@ -46,23 +51,20 @@ } if (cmd === '') - { return; - } - - try { + + try + { if (arguments[0] === 'from' || arguments[0] === 'import') - { return python.exec(cmd); - } - - const retval = python.eval(cmd); + retval = python.eval(cmd); } - catch(error) { + catch(error) + { globalThis._error = error; return util.inspect(error); } - + pythonCmd.serial = (pythonCmd.serial || 0) + 1; globalThis['$' + pythonCmd.serial] = retval; python.stdout.write('$' + pythonCmd.serial + ' = '); @@ -72,7 +74,7 @@ /** * Handle a .xyz repl command. Invokes function cmds[XXX], passing arguments that the user typed as the * function arguments. The function arguments are space-delimited arguments; arguments surrounded by - * quotes can include spaces, similar to how bash parses arguments. Argument parsing cribbed from + * quotes can include spaces, similar to how bash parses arguments. Argument parsing cribbed from * stackoverflow user Tsuneo Yoshioka, question 4031900. * * @param {string} cmdLine the command the user typed, without the leading . @@ -107,9 +109,9 @@ var result; var mightBeObjectLiteral = false; - /* A statement which starts with a { and does not end with a ; is treated as an object literal, + /* A statement which starts with a { and does not end with a ; is treated as an object literal, * and to get the parser in to Expression mode instead of Statement mode, we surround any expression - * like that which is also a valid compilation unit with parens, then if that is a syntax error, + * like that which is also a valid compilation unit with parens, then if that is a syntax error, * we re-evaluate without the parens. */ if (/^\\s*\\{.*[^;\\s]\\s*$/.test(statement)) @@ -145,140 +147,143 @@ return util.inspect(error); } } -""", evalOpts); +""", evalOpts) + + +async def repl(): + """ + Start a REPL to evaluate JavaScript code in the extra-module environment. Multi-line statements and + readline history are supported. ^C support is sketchy. Exit the REPL with ^D or ".quit". + """ + + print('Welcome to PythonMonkey v' + pm.__version__ + '.') + print('Type ".help" for more information.') + readline.parse_and_bind('set editing-mode emacs') + histfile = os.getenv('PMJS_REPL_HISTORY') or os.path.expanduser('~/.pmjs_history') + if (os.path.exists(histfile)): + try: + readline.read_history_file(histfile) + except BaseException: + pass + + got_sigint = 0 + statement = '' + readline_skip_chars = 0 + inner_loop = False + + def save_history(): + nonlocal histfile + readline.write_history_file(histfile) -def repl(): + import atexit + atexit.register(save_history) + + def quit(): """ - Start a REPL to evaluate JavaScript code in the extra-module environment. Multi-line statements and - readline history are supported. ^C support is sketchy. Exit the REPL with ^D or ".quit". + Quit the REPL. Repl saved by atexit handler. """ - - print('Welcome to PythonMonkey v' + pm.__version__ +'.') - print('Type ".help" for more information.') - readline.parse_and_bind('set editing-mode emacs') - histfile = os.getenv('PMJS_REPL_HISTORY') or os.path.expanduser('~/.pmjs_history') - if (os.path.exists(histfile)): - try: - readline.read_history_file(histfile) - except: - pass - - got_sigint = 0 - statement = '' - readline_skip_chars = 0 - inner_loop = False - - def save_history(): - nonlocal histfile - readline.write_history_file(histfile) - - import atexit - atexit.register(save_history) - - def quit(): - """ - Quit the REPL. Repl saved by atexit handler. - """ - sys.exit(0) - - def sigint_handler(signum, frame): - """ - Handle ^C by aborting the entry of the current statement and quitting when double-struck. - - Sometimes this happens in the main input() function. When that happens statement is "", because - we have not yet returned from input(). Sometimes it happens in the middle of the inner loop's - input() - in that case, statement is the beginning of a multiline expression. Hitting ^C in the - middle of a multiline express cancels its input, but readline's input() doesn't return, so we - have to print the extra > prompt and fake it by later getting rid of the first readline_skip_chars - characters from the input buffer. - """ - nonlocal got_sigint - nonlocal statement - nonlocal readline_skip_chars - nonlocal inner_loop - - got_sigint = got_sigint + 1 - if (got_sigint > 1): - raise EOFError - - if (inner_loop != True): - if (got_sigint == 1 and len(readline.get_line_buffer()) == readline_skip_chars): - # First ^C with nothing in the input buffer - sys.stdout.write("\n(To exit, press Ctrl+C again or Ctrl+D or type .exit)") - elif (got_sigint == 1 and readline.get_line_buffer() != ""): - # Input buffer has text - clear it - got_sigint = 0 - readline_skip_chars = len(readline.get_line_buffer()) - else: - if (got_sigint == 1 and statement == "" and len(readline.get_line_buffer()) == readline_skip_chars): - # statement == "" means that the inner loop has already seen ^C and is now faking the outer loop - sys.stdout.write("\n(To exit, press Ctrl+C again or Ctrl+D or type .exit)") - elif (got_sigint == 1 and statement != ""): - # ^C happened on inner loop while it was still thinking we were doing a multiline-expression; since - # we can't break the input() function, we set it up to return an outer expression and fake the outer loop - got_sigint = 0 - readline_skip_chars = len(readline.get_line_buffer()) - - sys.stdout.write("\n> ") + globalThis.python.exit() # need for python.exit.code in require.py + + def sigint_handler(signum, frame): + """ + Handle ^C by aborting the entry of the current statement and quitting when double-struck. + + Sometimes this happens in the main input() function. When that happens statement is "", because + we have not yet returned from input(). Sometimes it happens in the middle of the inner loop's + input() - in that case, statement is the beginning of a multiline expression. Hitting ^C in the + middle of a multiline express cancels its input, but readline's input() doesn't return, so we + have to print the extra > prompt and fake it by later getting rid of the first readline_skip_chars + characters from the input buffer. + """ + nonlocal got_sigint + nonlocal statement + nonlocal readline_skip_chars + nonlocal inner_loop + + got_sigint = got_sigint + 1 + if (got_sigint > 1): + raise EOFError + + if (not inner_loop): + if (got_sigint == 1 and len(readline.get_line_buffer()) == readline_skip_chars): + # First ^C with nothing in the input buffer + sys.stdout.write("\n(To exit, press Ctrl+C again or Ctrl+D or type .exit)") + elif (got_sigint == 1 and readline.get_line_buffer() != ""): + # Input buffer has text - clear it + got_sigint = 0 + readline_skip_chars = len(readline.get_line_buffer()) + else: + if (got_sigint == 1 and statement == "" and len(readline.get_line_buffer()) == readline_skip_chars): + # statement == "" means that the inner loop has already seen ^C and is now faking the outer loop + sys.stdout.write("\n(To exit, press Ctrl+C again or Ctrl+D or type .exit)") + elif (got_sigint == 1 and statement != ""): + # ^C happened on inner loop while it was still thinking we were doing a multiline-expression; since + # we can't break the input() function, we set it up to return an outer expression and fake the outer loop + got_sigint = 0 + readline_skip_chars = len(readline.get_line_buffer()) + + sys.stdout.write("\n> ") + statement = "" + signal.signal(signal.SIGINT, sigint_handler) + + # Main Loop + # + # Read lines entered by the user and collect them in a statement. Once the statement is a candiate + # for JavaScript evaluation (determined by pm.isCompilableUnit(), send it to replEval(). Statements + # beginning with a . are interpreted as REPL commands and sent to replCmd(). + # + # Beware - extremely tricky interplay between readline and the SIGINT handler. This is largely because we + # we can't clear the pending line buffer, so we have to fake it by re-displaying the prompt and subtracting + # characters. Another complicating factor is that the handler will suspend and resume readline, but there + # is no mechanism to force readline to return before the user presses enter. + # + while got_sigint < 2: + try: + await asyncio.sleep(0) + inner_loop = False + if (statement == ""): + statement = input('> ')[readline_skip_chars:] + readline_skip_chars = 0 + + if (len(statement) == 0): + continue + if (statement[0] == '.'): + cmd_output = globalThis.replCmd(statement[1:]) + if (cmd_output is not None): + print(cmd_output) statement = "" - signal.signal(signal.SIGINT, sigint_handler) - - # Main Loop - # - # Read lines entered by the user and collect them in a statement. Once the statement is a candiate - # for JavaScript evaluation (determined by pm.isCompilableUnit(), send it to replEval(). Statements - # beginning with a . are interpreted as REPL commands and sent to replCmd(). - # - # Beware - extremely tricky interplay between readline and the SIGINT handler. This is largely because we - # we can't clear the pending line buffer, so we have to fake it by re-displaying the prompt and subtracting - # characters. Another complicating factor is that the handler will suspend and resume readline, but there - # is no mechanism to force readline to return before the user presses enter. - # - while got_sigint < 2: - try: - inner_loop = False - if (statement == ""): - statement = input('> ')[readline_skip_chars:] - readline_skip_chars = 0 - - if (len(statement) == 0): - continue - if (statement[0] == '.'): - cmd_output = globalThis.replCmd(statement[1:]); - if (cmd_output != None): - print(cmd_output) - statement = "" - continue - if (pm.isCompilableUnit(statement)): - print(globalThis.replEval(statement)) - statement = "" - got_sigint = 0 - else: - got_sigint = 0 - # This loop builds a multi-line statement, but if the user hits ^C during this build, we - # abort the statement. The tricky part here is that the input('... ') doesn't quit when - # SIGINT is received, so we have to patch things up so that the next-entered line is - # treated as the input at the top of the loop. - while (got_sigint == 0): - inner_loop = True - lineBuffer = input('... ') - more = lineBuffer[readline_skip_chars:] - readline_skip_chars = 0 - if (got_sigint > 0): - statement = more - break - statement = statement + '\n' + more - if (pm.isCompilableUnit(statement)): - print(globalThis.replEval(statement)) - statement = "" - break - except EOFError: - print() - quit() + continue + if (pm.isCompilableUnit(statement)): + print(globalThis.replEval(statement)) + statement = "" + got_sigint = 0 + else: + got_sigint = 0 + # This loop builds a multi-line statement, but if the user hits ^C during this build, we + # abort the statement. The tricky part here is that the input('... ') doesn't quit when + # SIGINT is received, so we have to patch things up so that the next-entered line is + # treated as the input at the top of the loop. + while (got_sigint == 0): + await asyncio.sleep(0) + inner_loop = True + lineBuffer = input('... ') + more = lineBuffer[readline_skip_chars:] + readline_skip_chars = 0 + if (got_sigint > 0): + statement = more + break + statement = statement + '\n' + more + if (pm.isCompilableUnit(statement)): + print(globalThis.replEval(statement)) + statement = "" + break + except EOFError: + print() + quit() def usage(): - print("""Usage: pmjs [options] [ script.js ] [arguments] + print("""Usage: pmjs [options] [ script.js ] [arguments] Options: - script read from stdin (default if no file name is provided, interactive mode if a tty) @@ -291,80 +296,134 @@ def usage(): -v, --version print PythonMonkey version --use-strict evaluate -e, -p, and REPL code in strict mode --inspect enable pmdb, a gdb-like JavaScript debugger interface - + --wtf enable WTFPythonMonkey, a tool that can detect hanging timers when Ctrl-C is hit + Environment variables: TZ specify the timezone configuration PMJS_PATH ':'-separated list of directories prefixed to the module search path PMJS_REPL_HISTORY path to the persistent REPL history file""" - ) + ) + def initGlobalThis(): - """ - Initialize globalThis for for pmjs use in the extra-module context (eg -r, -e, -p). This context - needs a require function which resolve modules relative to the current working directory at pmjs - launch. The global require is to the JS function using a trick iinstead of a JS-wrapped-Python-wrapped function - """ - global requirePath + """ + Initialize globalThis for pmjs use in the extra-module context (eg -r, -e, -p). This context + needs a require function which resolves modules relative to the current working directory at pmjs + launch. The global require is to the JS function using a trick instead of a JS-wrapped-Python-wrapped function + """ + global requirePath + + require = pm.createRequire(os.path.abspath(os.getcwd() + '/__pmjs_virtual__'), requirePath) + globalThis.require = require + globalInitModule = require( + os.path.realpath( + os.path.dirname(__file__) + + "/../lib/pmjs/global-init")) # module load has side-effects + globalThis.arguments = sys.argv + return globalInitModule - require = pm.createRequire(os.path.abspath(os.getcwd() + '/__pmjs_virtual__'), requirePath) - globalThis.require = require - globalInitModule = require(os.path.realpath(os.path.dirname(__file__) + "/../lib/pmjs/global-init")) # module load has side-effects - argvBuilder = globalInitModule.makeArgvBuilder() - for arg in sys.argv: - argvBuilder(arg); # list=>Array not working yet - return globalInitModule def main(): - """ - Main program entry point - """ - enterRepl = sys.stdin.isatty() - forceRepl = False - globalInitModule = initGlobalThis() - global requirePath - + """ + Main program entry point + """ + enterRepl = sys.stdin.isatty() + forceRepl = False + globalInitModule = initGlobalThis() + global requirePath + + try: + opts, args = getopt.getopt(sys.argv[1:], "hie:p:r:v", ["help", "eval=", "print=", + "require=", "version", "interactive", "use-strict", "inspect", "wtf"]) + except getopt.GetoptError as err: + # print help information and exit: + print(err) # will print something like "option -a not recognized" + usage() + sys.exit(2) + output = None + verbose = False + enableWTF = False + for o, a in opts: + if o in ("-v", "--version"): + print(pm.__version__) + sys.exit() + elif o in ("--use-strict"): + evalOpts['strict'] = True + elif o in ("-h", "--help"): + usage() + sys.exit() + elif o in ("-i", "--interactive"): + forceRepl = True + elif o in ("-e", "--eval"): + async def runEval(): + pm.eval(a, evalOpts) + await pm.wait() + asyncio.run(runEval()) + enterRepl = False + elif o in ("-p", "--print"): + async def runEvalPrint(): + ret = pm.eval(a, evalOpts) + pm.eval("ret => console.log(ret)", evalOpts)(ret) + await pm.wait() + asyncio.run(runEvalPrint()) + enterRepl = False + elif o in ("-r", "--require"): + globalThis.require(a) + elif o in ("--inspect"): + pmdb.enable() + elif o in ("--wtf"): + enableWTF = True + else: + assert False, "unhandled option" + + if (len(args) > 0): + async def runJS(): + hasUncaughtException = False + loop = asyncio.get_running_loop() + + def exceptionHandler(loop, context): + "See https://docs.python.org/3.11/library/asyncio-eventloop.html#error-handling-api" + error = context["exception"] + try: + globalInitModule.uncaughtExceptionHandler(error) + except SystemExit: # the "exception" raised by `sys.exit()` call + pass + finally: + pm.stop() # unblock `await pm.wait()` to gracefully exit the program + nonlocal hasUncaughtException + hasUncaughtException = True + loop.set_exception_handler(exceptionHandler) + + def cleanupExit(code=0): + pm.stop() + realExit(code) + realExit = globalThis.python.exit + globalThis.python.exit = cleanupExit + + try: + globalInitModule.patchGlobalRequire() + pm.runProgramModule(args[0], args, requirePath) + await pm.wait() # blocks until all asynchronous calls finish + if hasUncaughtException: + sys.exit(1) + except Exception as error: + print(error, file=sys.stderr) + sys.exit(1) try: - opts, args = getopt.getopt(sys.argv[1:], "hie:p:r:v", ["help", "eval=", "print=", "require=", "version", "interactive", "use-strict", "inspect"]) - except getopt.GetoptError as err: - # print help information and exit: - print(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - output = None - verbose = False - for o, a in opts: - if o in ("-v", "--version"): - print(pm.__version__) - sys.exit() - elif o in ("--use-strict"): - evalOpts['strict'] = True - elif o in ("-h", "--help"): - usage() - sys.exit() - elif o in ("-i", "--interactive"): - forceRepl = True - elif o in ("-e", "--eval"): - pm.eval(a, evalOpts) - enterRepl = False - elif o in ("-p", "--print"): - print(pm.eval(a, evalOpts)) - enterRepl = False - elif o in ("-r", "--require"): - globalThis.require(a) - elif o in ("--inspect"): - pmdb.enable() - else: - assert False, "unhandled option" - - if (len(args) > 0): - async def runJS(): - globalInitModule.patchGlobalRequire() - pm.runProgramModule(args[0], args, requirePath) - await pm.wait() # blocks until all asynchronous calls finish - asyncio.run(runJS()) - elif (enterRepl or forceRepl): - globalInitModule.initReplLibs() - repl() + asyncio.run(runJS()) + except KeyboardInterrupt: + print() # silently going to end the program instead of printing out the Python traceback + if enableWTF: + wtfpm.printTimersDebugInfo() + elif (enterRepl or forceRepl): + async def runREPL(): + globalInitModule.initReplLibs() + await repl() + await pm.wait() + asyncio.run(runREPL()) + + globalThis.python.exit() # need for python.exit.code in require.py + if __name__ == "__main__": - main() + main() diff --git a/python/pythonmonkey/global.d.ts b/python/pythonmonkey/global.d.ts index 6b9371e5..6c350891 100644 --- a/python/pythonmonkey/global.d.ts +++ b/python/pythonmonkey/global.d.ts @@ -2,6 +2,8 @@ * @file global.d.ts * @author Tom Tang * @date May 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ declare const python: { @@ -54,6 +56,16 @@ declare var btoa: typeof import("base64").btoa; // Expose `setTimeout`/`clearTimeout` APIs declare var setTimeout: typeof import("timers").setTimeout; declare var clearTimeout: typeof import("timers").clearTimeout; +// Expose `setInterval`/`clearInterval` APIs +declare var setInterval: typeof import("timers").setInterval; +declare var clearInterval: typeof import("timers").clearInterval; + +// Expose `URL`/`URLSearchParams` APIs +declare var URL: typeof import("url").URL; +declare var URLSearchParams: typeof import("url").URLSearchParams; + +// Expose `XMLHttpRequest` (XHR) API +declare var XMLHttpRequest: typeof import("XMLHttpRequest").XMLHttpRequest; // Keep this in sync with both https://hg.mozilla.org/releases/mozilla-esr102/file/a03fde6/js/public/Promise.h#l331 // and https://github.com/nodejs/node/blob/v20.2.0/deps/v8/include/v8-promise.h#L30 diff --git a/python/pythonmonkey/helpers.py b/python/pythonmonkey/helpers.py index 8c526bb9..76970edc 100644 --- a/python/pythonmonkey/helpers.py +++ b/python/pythonmonkey/helpers.py @@ -5,30 +5,33 @@ # @author Wes Garland, wes@distributive.network # @date July 2023 # +# @copyright Copyright (c) 2023 Distributive Corp. + +from . import pythonmonkey as pm +evalOpts = {'filename': __file__, 'fromPythonFrame': True} -from . import pythonmonkey as pm -evalOpts = { 'filename': __file__, 'fromPythonFrame': True } def typeof(jsval): - """ - typeof function - wraps JS typeof operator - """ - return pm.eval("""'use strict'; ( -function pmTypeof(jsval) + """ + typeof function - wraps JS typeof operator + """ + return pm.eval("""'use strict'; ( +function pmTypeof(jsval) { return typeof jsval; } - )""", evalOpts)(jsval); + )""", evalOpts)(jsval) + def new(ctor): - """ - new function - emits function which wraps JS new operator, emitting a lambda which constructs a new - JS object upon invocation. - """ - if (typeof(ctor) == 'string'): - ctor = pm.eval(ctor) - - newCtor = pm.eval("""'use strict'; ( + """ + new function - emits function which wraps JS new operator, emitting a lambda which constructs a new + JS object upon invocation. + """ + if (typeof(ctor) == 'string'): + ctor = pm.eval(ctor) + + newCtor = pm.eval("""'use strict'; ( function pmNewFactory(ctor) { return function newCtor(args) { @@ -37,13 +40,25 @@ def new(ctor): }; } )""", evalOpts)(ctor) - return (lambda *args: newCtor(list(args))) + return (lambda *args: newCtor(list(args))) + + +def simpleUncaughtExceptionHandler(loop, context): + """ + A simple exception handler for uncaught JS Promise rejections sent to the Python event-loop + + See https://docs.python.org/3.11/library/asyncio-eventloop.html#error-handling-api + """ + error = context["exception"] + pm.eval("(err) => console.error('Uncaught', err)")(error) + pm.stop() # unblock `await pm.wait()` to gracefully exit the program + # List which symbols are exposed to the pythonmonkey module. -__all__ = [ "new", "typeof" ] +__all__ = ["new", "typeof", "simpleUncaughtExceptionHandler"] # Add the non-enumerable properties of globalThis which don't collide with pythonmonkey.so as exports: -globalThis = pm.eval('globalThis'); +globalThis = pm.eval('globalThis') pmGlobals = vars(pm) exports = pm.eval(""" @@ -51,8 +66,8 @@ def new(ctor): .filter(prop => Object.keys(globalThis).indexOf(prop) === -1); """, evalOpts) -for index in range(0, int(exports.length)): - name = exports[index] - if (pmGlobals.get(name) == None): - globals().update({name: globalThis[name]}) - __all__.append(name) +for index in range(0, len(exports)): + name = exports[index] + if (pmGlobals.get(name) is None): + globals().update({name: globalThis[name]}) + __all__.append(name) diff --git a/python/pythonmonkey/lib/pmdb.py b/python/pythonmonkey/lib/pmdb.py index 13a6565f..f7afdc30 100644 --- a/python/pythonmonkey/lib/pmdb.py +++ b/python/pythonmonkey/lib/pmdb.py @@ -1,25 +1,28 @@ # @file pmdb - A gdb-like JavaScript debugger interface # @author Tom Tang # @date July 2023 +# @copyright Copyright (c) 2023 Distributive Corp. import pythonmonkey as pm + def debuggerInput(prompt: str): try: - return input(prompt) # blocking + return input(prompt) # blocking except KeyboardInterrupt: - print("\b\bQuit") # to match the behaviour of gdb + print("\b\bQuit") # to match the behaviour of gdb return "" except Exception as e: print(e) return "" -def enable(debuggerGlobalObject = pm.eval("debuggerGlobal")): + +def enable(debuggerGlobalObject=pm.eval("debuggerGlobal")): if debuggerGlobalObject._pmdbEnabled: - return # already enabled, skipping + return # already enabled, skipping debuggerGlobalObject._pmdbEnabled = True - debuggerGlobalObject.eval("""(debuggerInput, _pythonPrint, _pythonExit) => { + pm.eval("debuggerGlobal.eval")("""(mainGlobal, debuggerInput, _pythonPrint, _pythonExit) => { const dbg = new Debugger() const mainDebuggee = dbg.addDebuggee(mainGlobal) dbg.uncaughtExceptionHook = (e) => { @@ -39,19 +42,19 @@ def enable(debuggerGlobalObject = pm.eval("debuggerGlobal")): const logger = makeDebuggeeValue(mainGlobal.console.log) logger.apply(logger, args.map(makeDebuggeeValue)) } - + function printErr (...args) { const logger = makeDebuggeeValue(mainGlobal.console.error) logger.apply(logger, args.map(makeDebuggeeValue)) } - + function printSource (frame, location) { const src = frame.script.source.text const line = src.split('\\n').slice(location.lineNumber-1, location.lineNumber).join('\\n') print(line) print(" ".repeat(location.columnNumber) + "^") // indicate column position } - + function getCommandInputs () { const input = debuggerInput("(pmdb) > ") // blocking const [_, command, rest] = input.match(/\\s*(\\w+)?(?:\\s+(.*))?/) @@ -64,7 +67,7 @@ def enable(debuggerGlobalObject = pm.eval("debuggerGlobal")): // This bytecode offset does not qualify as a breakpoint, skipping return } - + blockingLoop: while (true) { const { command, rest } = getCommandInputs() // blocking switch (command) { @@ -174,4 +177,4 @@ def enable(debuggerGlobalObject = pm.eval("debuggerGlobal")): // Enter debugger on `debugger;` statement dbg.onDebuggerStatement = (frame) => enterDebuggerLoop(frame) - }""")(debuggerInput, print, lambda status: exit(int(status))) + }""")(pm.globalThis, debuggerInput, print, lambda status: exit(int(status))) diff --git a/python/pythonmonkey/lib/pmjs/global-init.js b/python/pythonmonkey/lib/pmjs/global-init.js index db78892c..87f3a5be 100644 --- a/python/pythonmonkey/lib/pmjs/global-init.js +++ b/python/pythonmonkey/lib/pmjs/global-init.js @@ -5,6 +5,8 @@ * * @author Wes Garland, wes@distributive.network * @date June 2023 + * + * @copyright Copyright (c) 2023 Distributive Corp. */ 'use strict'; @@ -15,21 +17,20 @@ for (let mid in require.cache) delete require.cache[mid]; -/** - * Set the global arguments array, which is just the program's argv. We use an argvBuilder function to - * get around PythonMonkey's missing list->Array coercion. /wg june 2023 - */ -exports.makeArgvBuilder = function pmjsRequire$$makeArgvBuilder() -{ - const argv = []; - globalThis.arguments = argv; - return argvBuilder; +/* Recreate the python object as an EventEmitter */ +const { EventEmitter } = require('events'); +const originalPython = globalThis.python; +const python = globalThis.python = new EventEmitter('python'); +Object.assign(python, originalPython); - function argvBuilder(arg) - { - globalThis.arguments.push(arg) - } -} +/* Emulate node's process.on('error') behaviour with python.on('error'). */ +python.on('error', function unhandledError(error) +{ + if (python.listenerCount('error') > 1) + return; + if (python.listenerCount('error') === 0 || python.listeners('error')[0] === unhandledError) + python.emit('unhandledException', error); +}); /** * runProgramModule wants to include the require.cache from the pre-program loads (e.g. via -r or -e), but @@ -43,9 +44,38 @@ exports.makeArgvBuilder = function pmjsRequire$$makeArgvBuilder() exports.patchGlobalRequire = function pmjs$$patchGlobalRequire() { globalThis.require = require; -} +}; exports.initReplLibs = function pmjs$$initReplLibs() { globalThis.util = require('util'); -} + globalThis.events = require('events'); +}; + +/** + * Temporary API until we get EventEmitters working. Replace this export with a custom handler. + */ +exports.uncaughtExceptionHandler = function globalInit$$uncaughtExceptionHandler(error) +{ + if (python._events && python._events['uncaughtException']) + python.emit('uncaughtException', error); + else + { + console.error('Uncaught', error); + python.exit(1); + } +}; + +/** + * Temporary API until we get EventEmitters working. Replace this export with a custom handler. + */ +exports.unhandledRejectionHandler = function globalInit$$unhandledRejectionHandler(error) +{ + if (python._events && python._events['uncaughtRejection']) + python.emit('unhandledRejection', error); + else + { + console.error(error); + python.exit(1); + } +}; diff --git a/python/pythonmonkey/lib/wtfpm.py b/python/pythonmonkey/lib/wtfpm.py new file mode 100644 index 00000000..2c5809eb --- /dev/null +++ b/python/pythonmonkey/lib/wtfpm.py @@ -0,0 +1,42 @@ +# @file WTFPythonMonkey - A tool that detects any hanging setTimeout/setInterval timers when Ctrl-C is hit +# @author Tom Tang +# @date April 2024 +# @copyright Copyright (c) 2024 Distributive Corp. + +import pythonmonkey as pm + + +def printTimersDebugInfo(): + pm.eval("""(require) => { + const internalBinding = require('internal-binding'); + const { getAllRefedTimersDebugInfo: getDebugInfo } = internalBinding('timers'); + console.log(getDebugInfo()) + console.log(new Date()) + }""")(pm.createRequire(__file__)) + + +class WTF: + """ + WTFPythonMonkey to use as a Python context manager (`with`-statement) + + Usage: + ```py + from pythonmonkey.lib.wtfpm import WTF + + with WTF(): + # the main entry point for the program utilizes PythonMonkey event-loop + asyncio.run(pythonmonkey_main()) + ``` + """ + + def __enter__(self): + pass + + def __exit__(self, errType, errValue, traceback): + if errType is None: # no exception + return + elif issubclass(errType, KeyboardInterrupt): # except KeyboardInterrupt: + printTimersDebugInfo() + return True # exception suppressed + else: # other exceptions + return False diff --git a/python/pythonmonkey/py.typed b/python/pythonmonkey/py.typed new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/python/pythonmonkey/py.typed @@ -0,0 +1 @@ + diff --git a/python/pythonmonkey/pythonmonkey.pyi b/python/pythonmonkey/pythonmonkey.pyi index 15bed09d..b6e81761 100644 --- a/python/pythonmonkey/pythonmonkey.pyi +++ b/python/pythonmonkey/pythonmonkey.pyi @@ -5,68 +5,170 @@ stub file for type hints & documentations for the native module import typing as _typing + class EvalOptions(_typing.TypedDict, total=False): - filename: str - lineno: int - column: int - mutedErrors: bool - noScriptRval: bool - selfHosting: bool - strict: bool - module: bool - fromPythonFrame: bool + filename: str + lineno: int + column: int + mutedErrors: bool + noScriptRval: bool + selfHosting: bool + strict: bool + module: bool + fromPythonFrame: bool # pylint: disable=redefined-builtin + + def eval(code: str, evalOpts: EvalOptions = {}, /) -> _typing.Any: - """ - JavaScript evaluator in Python - """ + """ + JavaScript evaluator in Python + """ + + +def require(moduleIdentifier: str, /) -> JSObjectProxy: + """ + Return the exports of a CommonJS module identified by `moduleIdentifier`, using standard CommonJS semantics + """ + + +def new(ctor: JSFunctionProxy) -> _typing.Callable[..., _typing.Any]: + """ + Wrap the JS new operator, emitting a lambda which constructs a new + JS object upon invocation + """ + + +def typeof(jsval: _typing.Any, /): + """ + This is the JS `typeof` operator, wrapped in a function so that it can be used easily from Python. + """ + def wait() -> _typing.Awaitable[None]: - """ - Block until all asynchronous jobs (Promise/setTimeout/etc.) finish. - - ```py - await pm.wait() - ``` + """ + Block until all asynchronous jobs (Promise/setTimeout/etc.) finish. + + ```py + await pm.wait() + ``` + + This is the event-loop shield that protects the loop from being prematurely terminated. + """ + + +def stop() -> None: + """ + Stop all pending asynchronous jobs, and unblock `await pm.wait()` + """ + + +def runProgramModule(filename: str, argv: _typing.List[str], extraPaths: _typing.List[str] = []) -> None: + """ + Load and evaluate a program (main) module. Program modules must be written in JavaScript. + """ - This is the event-loop shield that protects the loop from being prematurely terminated. - """ def isCompilableUnit(code: str) -> bool: - """ - Hint if a string might be compilable Javascript without actual evaluation - """ + """ + Hint if a string might be compilable Javascript without actual evaluation + """ + + +def collect() -> None: + """ + Calls the spidermonkey garbage collector + """ + def internalBinding(namespace: str) -> JSObjectProxy: - """ - INTERNAL USE ONLY + """ + INTERNAL USE ONLY - See function declarations in ./builtin_modules/internal-binding.d.ts - """ + See function declarations in ./builtin_modules/internal-binding.d.ts + """ + + +class JSFunctionProxy(): + """ + JavaScript Function proxy + """ + + +class JSMethodProxy(JSFunctionProxy, object): + """ + JavaScript Method proxy + This constructs a callable object based on the first argument, bound to the second argument + Useful when you wish to implement a method on a class object with JavaScript + Example: + import pythonmonkey as pm + + jsFunc = pm.eval("(function(value) { this.value = value})") + class Class: + def __init__(self): + self.value = 0 + self.setValue = pm.JSMethodProxy(jsFunc, self) #setValue will be bound to self, so `this` will always be `self` + + myObject = Class() + print(myObject.value) # 0 + myObject.setValue(42) + print(myObject.value) # 42.0 + """ + + def __init__(self) -> None: "deleted" + + +class JSObjectProxy(dict): + """ + JavaScript Object proxy dict + """ + + def __init__(self) -> None: "deleted" + + +class JSArrayProxy(list): + """ + JavaScript Array proxy + """ + + def __init__(self) -> None: "deleted" + + +class JSArrayIterProxy(_typing.Iterator): + """ + JavaScript Array Iterator proxy + """ + + def __init__(self) -> None: "deleted" + + +class JSStringProxy(str): + """ + JavaScript String proxy + """ + + def __init__(self) -> None: "deleted" -def collect() -> None: - """ - Calls the spidermonkey garbage collector - """ class bigint(int): - """ - Representing JavaScript BigInt in Python - """ + """ + Representing JavaScript BigInt in Python + """ + class SpiderMonkeyError(Exception): - """ - Representing a corresponding JS Error in Python - """ + """ + Representing a corresponding JS Error in Python + """ -class JSObjectProxy(dict): - """ - JavaScript Object proxy dict - """ - def __init__(self) -> None: ... null = _typing.Annotated[ - _typing.NewType("pythonmonkey.null", object), - "Representing the JS null type in Python using a singleton object", + _typing.NewType("pythonmonkey.null", object), + "Representing the JS null type in Python using a singleton object", +] + + +globalThis = _typing.Annotated[ + JSObjectProxy, + "A Python Dict which is equivalent to the globalThis object in JavaScript", ] diff --git a/python/pythonmonkey/require.py b/python/pythonmonkey/require.py index 31819dac..696a4fbb 100644 --- a/python/pythonmonkey/require.py +++ b/python/pythonmonkey/require.py @@ -1,7 +1,7 @@ # @file require.py # Implementation of CommonJS "require" for PythonMonkey. This implementation uses the # ctx-module npm package to do the heavy lifting. That package makes a complete module -# system, obstensibly in a separate context, but our implementation here reuses the +# system, ostensibly in a separate context, but our implementation here reuses the # PythonMonkey global context for both. # # The context that ctx-module runs in needs a require function supporting @@ -19,11 +19,29 @@ # implementation in Python that doesn't leak global symbols should be possible once # some PythonMonkey bugs are fixed. # +# Module Context Summary +# - what CommonJS modules you can access depends on what require symbol you have +# - pm means the pythonmonkey Python module exports +# - pm.bootstrap.require is a special require that knows about modules which are not stored +# on disk. This is important, because ctx-module needs some modules, and this is how we +# solve the chicken<>egg problem. +# - There is a node_modules folder inside the pminit module that comes with pythonmonkey. +# This folder is managed by npm by invoking `pminit npm`. These modules are intended as +# global modules, analogously to require('process') in nodejs. +# - pm.require is a require that works as expected from python source code; i.e. relative +# modules are resolved relative to the python source code. +# - pm.require has access to the pminit node_modules modules but not the bootstrap modules. +# - The builtin_modules directory is available anywhere the pminit node_modules directory +# is, and has a higher precedence. +# # @author Wes Garland, wes@distributive.network # @date May 2023 # +# @copyright Copyright (c) 2023-2024 Distributive Corp. -import sys, os, io +import sys +import os +import io from typing import Union, Dict, Literal, List import importlib import importlib.util @@ -31,55 +49,64 @@ import inspect import functools -from . import pythonmonkey as pm +from . import pythonmonkey as pm node_modules = os.path.abspath( os.path.join( - importlib.util.find_spec("pminit").submodule_search_locations[0], # type: ignore + importlib.util.find_spec("pminit").submodule_search_locations[0], # type: ignore "..", "pythonmonkey", "node_modules" ) ) -evalOpts = { 'filename': __file__, 'fromPythonFrame': True } # type: pm.EvalOptions +evalOpts = {'filename': __file__, 'fromPythonFrame': True} # type: pm.EvalOptions # Force to use UTF-8 encoding # Windows may use other encodings / code pages that have many characters missing/unrepresentable -if isinstance(sys.stdin, io.TextIOWrapper): sys.stdin.reconfigure(encoding='utf-8') -if isinstance(sys.stdout, io.TextIOWrapper): sys.stdout.reconfigure(encoding='utf-8') -if isinstance(sys.stderr, io.TextIOWrapper): sys.stderr.reconfigure(encoding='utf-8') +if isinstance(sys.stdin, io.TextIOWrapper): + sys.stdin.reconfigure(encoding='utf-8') +if isinstance(sys.stdout, io.TextIOWrapper): + sys.stdout.reconfigure(encoding='utf-8') +if isinstance(sys.stderr, io.TextIOWrapper): + sys.stderr.reconfigure(encoding='utf-8') # Add some python functions to the global python object for code in this file to use. globalThis = pm.eval("globalThis;", evalOpts) -pm.eval("globalThis.python = { pythonMonkey: {}, stdout: {}, stderr: {} }", evalOpts); +pm.eval("globalThis.python = { pythonMonkey: {}, stdout: {}, stderr: {} }", evalOpts) globalThis.pmEval = pm.eval globalThis.python.pythonMonkey.dir = os.path.dirname(__file__) -#globalThis.python.pythonMonkey.version = pm.__version__ -#globalThis.python.pythonMonkey.module = pm globalThis.python.pythonMonkey.isCompilableUnit = pm.isCompilableUnit globalThis.python.pythonMonkey.nodeModules = node_modules -globalThis.python.print = print +globalThis.python.print = print globalThis.python.stdout.write = lambda s: sys.stdout.write(s) globalThis.python.stderr.write = lambda s: sys.stderr.write(s) globalThis.python.stdout.read = lambda n: sys.stdout.read(n) globalThis.python.stderr.read = lambda n: sys.stderr.read(n) -globalThis.python.eval = eval -globalThis.python.exec = exec +# Python 3.13 dramatically changed how the namespace in `exec`/`eval` works +# See https://docs.python.org/3.13/whatsnew/3.13.html#defined-mutation-semantics-for-locals +globalThis.python.eval = lambda x: eval(str(x)[:], None, sys._getframe(1).f_locals) +globalThis.python.exec = lambda x: exec(str(x)[:], None, sys._getframe(1).f_locals) globalThis.python.getenv = os.getenv -globalThis.python.paths = sys.path +globalThis.python.paths = sys.path globalThis.python.exit = pm.eval("""'use strict'; (exit) => function pythonExitWrapper(exitCode) { + if (typeof exitCode === 'undefined') + exitCode = pythonExitWrapper.code; + if (typeof exitCode == 'undefined') + exitCode = 0n; if (typeof exitCode === 'number') exitCode = BigInt(Math.floor(exitCode)); + if (typeof exitCode !== 'bigint') + exitCode = 1n; exit(exitCode); } -""", evalOpts)(sys.exit); +""", evalOpts)(sys.exit) # bootstrap is effectively a scoping object which keeps us from polluting the global JS scope. # The idea is that we hold a reference to the bootstrap object in Python-load, for use by the # innermost code in ctx-module, without forcing ourselves to expose this minimalist code to -# userland-require. +# userland-require bootstrap = pm.eval(""" 'use strict'; (function IIFE(python) { @@ -96,7 +123,12 @@ if (bootstrap.modules.hasOwnProperty(mid)) return bootstrap.modules[mid]; - throw new Error('module not found: ' + mid); + if (bootstrap.modules['ctx-module'].CtxModule) + return bootstrap.requireFromDisk(mid); + + const error = new Error('module not found: ' + mid); + error = 'MODULE_NOT_FOUND'; + throw error; } bootstrap.modules.vm.runInContext = function runInContext(code, _unused_contextifiedObject, options) @@ -115,7 +147,7 @@ /** * The debug module has as its exports a function which may, depending on the DEBUG env var, emit - * debugging statemnts to stdout. This is quick implementation of the node built-in. + * debugging statements to stdout. This is quick implementation of the node built-in. */ bootstrap.modules.debug = function debug(selector) { @@ -142,7 +174,21 @@ if (re.test(selector)) { return (function debugInner() { - python.print(`${colour}${selector}${noColour} ` + Array.from(arguments).join(' ')) + var output; + if (!bootstrap.inspect) + output = Array.from(arguments).join(' '); + else + output = Array.from(arguments).map(x => { + if (typeof x === 'string' || x instanceof String) + return x; + return bootstrap.inspect(x); + }).join(' '); + + output = output.split('\\n'); + python.print(`${colour}${selector}${noColour} ` + output[0]); + const spaces = ''.padEnd(selector.length + 1, ' '); + for (let i=1; i < output.length; i++) + python.print(spaces + output[i]); }); } } @@ -163,8 +209,8 @@ if (ret) return ret; - const err = new Error('file not found: ' + filename); - err.code='ENOENT'; + const err = new Error('file not found: ' + filename); + err.code='ENOENT'; throw err; }, }; @@ -172,44 +218,45 @@ /* Modules which will be available to all requires */ bootstrap.builtinModules = { debug: bootstrap.modules.debug }; -/* temp workaround for PythonMonkey bug */ -globalThis.bootstrap = bootstrap; - return bootstrap; })(globalThis.python)""", evalOpts) + def statSync_inner(filename: str) -> Union[Dict[str, int], bool]: - """ - Inner function for statSync. - - Returns: - Union[Dict[str, int], False]: The mode of the file or False if the file doesn't exist. - """ - from os import stat - filename = os.path.normpath(filename) - if (os.path.exists(filename)): - sb = stat(filename) - return { 'mode': sb.st_mode } - else: - return False + """ + Inner function for statSync. + + Returns: + Union[Dict[str, int], False]: The mode of the file or False if the file doesn't exist. + """ + from os import stat + filename = os.path.normpath(filename) + if (os.path.exists(filename)): + sb = stat(filename) + return {'mode': sb.st_mode} + else: + return False + def readFileSync(filename, charset) -> str: - """ - Utility function for reading files. - Returns: - str: The contents of the file - """ - filename = os.path.normpath(filename) - with open(filename, "r", encoding=charset) as fileHnd: - return fileHnd.read() + """ + Utility function for reading files. + Returns: + str: The contents of the file + """ + filename = os.path.normpath(filename) + with open(filename, "r", encoding=charset) as fileHnd: + return fileHnd.read() + def existsSync(filename: str) -> bool: - filename = os.path.normpath(filename) - return os.path.exists(filename) + filename = os.path.normpath(filename) + return os.path.exists(filename) + bootstrap.modules.fs.statSync_inner = statSync_inner -bootstrap.modules.fs.readFileSync = readFileSync -bootstrap.modules.fs.existsSync = existsSync +bootstrap.modules.fs.readFileSync = readFileSync +bootstrap.modules.fs.existsSync = existsSync # Read ctx-module module from disk and invoke so that this file is the "main module" and ctx-module has # require and exports symbols injected from the bootstrap object above. Current PythonMonkey bugs @@ -219,63 +266,60 @@ def existsSync(filename: str) -> bool: # lineno should be -5 but jsapi 102 uses unsigned line numbers, so we take the newlines out of the # wrapper prologue to make stack traces line up. with open(node_modules + "/ctx-module/ctx-module.js", "r") as ctxModuleSource: - initCtxModule = pm.eval("""'use strict'; -(function moduleWrapper_forCtxModule(broken_require, broken_exports) + initCtxModule = pm.eval("""'use strict'; +(function moduleWrapper_forCtxModule(require, exports) { - const require = bootstrap.require; - const exports = bootstrap.modules['ctx-module']; -""".replace("\n", " ") + "\n" + ctxModuleSource.read() + """ +""" + ctxModuleSource.read() + """ }) -""", { 'filename': node_modules + "/ctx-module/ctx-module.js", 'lineno': 0 }); -#broken initCtxModule(bootstrap.require, bootstrap.modules['ctx-module'].exports) -initCtxModule(); - -def load(filename: str) -> Dict: - """ - Loads a python module using the importlib machinery sourcefileloader, prefills it with an exports object and returns the module. - If the module is already loaded, returns it. - - Args: - filename (str): The filename of the python module to load. - - Returns: - : The loaded python module - """ - - filename = os.path.normpath(filename) - name = os.path.basename(filename) - if name not in sys.modules: - sourceFileLoader = machinery.SourceFileLoader(name, filename) - spec: machinery.ModuleSpec = importlib.util.spec_from_loader(sourceFileLoader.name, sourceFileLoader) # type: ignore - module = importlib.util.module_from_spec(spec) - sys.modules[name] = module - module.exports = {} # type: ignore - spec.loader.exec_module(module) # type: ignore - else: - module = sys.modules[name] - return module.exports +""", {'filename': node_modules + "/ctx-module/ctx-module.js", 'lineno': 0}) +initCtxModule(bootstrap.require, bootstrap.modules['ctx-module']) + + +def load(filename: str) -> Dict: + """ + Loads a python module using the importlib machinery sourcefileloader, prefills it with an exports object and returns + the module. + If the module is already loaded, returns it. + + Args: + filename (str): The filename of the python module to load. + + Returns: + : The loaded python module + """ + + filename = os.path.normpath(filename) + name = os.path.basename(filename) + if name not in sys.modules: + sourceFileLoader = machinery.SourceFileLoader(name, filename) + spec: machinery.ModuleSpec = importlib.util.spec_from_loader( + sourceFileLoader.name, sourceFileLoader) # type: ignore + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + module.exports = {} # type: ignore + spec.loader.exec_module(module) # type: ignore + else: + module = sys.modules[name] + return module.exports + + globalThis.python.load = load -# API: pm.createRequire -# We cache the return value of createRequire to always use the same require for the same filename -@functools.lru_cache(maxsize=None) # unbounded function cache that won't remove any old values -def _createRequireInner(*args): - return pm.eval("""'use strict';( +createRequireInner = pm.eval("""'use strict';( /** * Factory function which returns a fresh 'require' function. The module cache will inherit from - * globalTHis.require, assuming it has been defined. + * globalThis.require, assuming it has been defined. * * @param {string} filename the filename of the module that would get this require - * @param {object} bootstrap the bootstrap context; python imports that are invisible to normal JS + * @param {object} bootstrap the bootstrap context; python imports, modules, etc, that are invisible + * to normal JS * @param {string} extraPaths colon-delimited list of paths to add to require.path * @param {boolean} isMain true if the module is to be used as a program module * * @returns {function} require */ -function createRequire(filename, bootstrap_broken, extraPaths, isMain) +function createRequireInner(filename, bootstrap, extraPaths, isMain) { - filename = filename.split('\\\\').join('/'); - const bootstrap = globalThis.bootstrap; /** @bug PM-65 */ const CtxModule = bootstrap.modules['ctx-module'].CtxModule; const moduleCache = globalThis.require?.cache || {}; @@ -284,16 +328,31 @@ def _createRequireInner(*args): module.exports = python.load(filename); } + // TODO - find a better way to deal with Windows paths + if (filename) + filename = filename.split('\\\\').join('/'); if (moduleCache[filename]) return moduleCache[filename].require; const module = new CtxModule(globalThis, filename, moduleCache); - moduleCache[filename] = module; - for (let path of Array.from(python.paths)) - module.paths.push(path + '/node_modules'); + if (!filename) + module.paths = []; /* fully virtual module - no module.path or module.paths */ + else + { + moduleCache[filename] = module; + for (let path of Array.from(python.paths)) + module.paths.push(path + '/node_modules'); + } + module.require.path.push(python.pythonMonkey.dir + '/builtin_modules'); module.require.path.push(python.pythonMonkey.nodeModules); + + /* Add a .py loader, making it the first extension to be enumerated so py modules take precedence */ + const extCopy = Object.assign({}, module.require.extensions); + for (let ext in module.require.extensions) + delete module.require.extensions[ext]; module.require.extensions['.py'] = loadPythonModule; + Object.assign(module.require.extensions, extCopy); if (isMain) { @@ -309,48 +368,68 @@ def _createRequireInner(*args): module.require.path.splice(module.require.path.length, 0, ...(extraPaths.split(','))); return module.require; -})""", evalOpts)(*args) - -def createRequire(filename, extraPaths: Union[List[str], Literal[False]] = False, isMain = False): - """ - returns a require function that resolves modules relative to the filename argument. - Conceptually the same as node:module.createRequire(). - - example: - from pythonmonkey import createRequire - require = createRequire(__file__) - require('./my-javascript-module') - """ - fullFilename: str = os.path.abspath(filename) - if (extraPaths): - extraPathsStr = ':'.join(extraPaths) - else: - extraPathsStr = '' - return _createRequireInner(fullFilename, 'broken', extraPathsStr, isMain) +})""", evalOpts) + +# API: pm.createRequire +# We cache the return value of createRequire to always use the same require for the same filename + + +def createRequire(filename, extraPaths: Union[List[str], Literal[False]] = False, isMain=False): + """ + returns a require function that resolves modules relative to the filename argument. + Conceptually the same as node:module.createRequire(). + + example: + from pythonmonkey import createRequire + require = createRequire(__file__) + require('./my-javascript-module') + """ + fullFilename: str = os.path.abspath(filename) + if (extraPaths): + extraPathsStr = ':'.join(extraPaths) + else: + extraPathsStr = '' + return createRequireInner(fullFilename, bootstrap, extraPathsStr, isMain) + + +bootstrap.requireFromDisk = createRequireInner(None, bootstrap, '', False) +bootstrap.inspect = bootstrap.requireFromDisk('util').inspect # API: pm.runProgramModule + + def runProgramModule(filename, argv, extraPaths=[]): - """ - Run the program module. This loads the code from disk, sets up the execution environment, and then - invokes the program module (aka main module). The program module is different from other modules in that - 1. it cannot return (must throw) - 2. the outermost block scope is the global scope, effectively making its scope a super-global to - other modules - """ - fullFilename = os.path.abspath(filename) - createRequire(fullFilename, extraPaths, True) - globalThis.__filename = fullFilename; - globalThis.__dirname = os.path.dirname(fullFilename); - with open(fullFilename, encoding="utf-8", mode="r") as mainModuleSource: - pm.eval(mainModuleSource.read(), {'filename': fullFilename}) + """ + Run the program module. This loads the code from disk, sets up the execution environment, and then + invokes the program module (aka main module). The program module is different from other modules in that + 1. it cannot return (must throw) + 2. the outermost block scope is the global scope, effectively making its scope a super-global to + other modules + """ + fullFilename = os.path.abspath(filename) + createRequire(fullFilename, extraPaths, True) + globalThis.__filename = fullFilename + globalThis.__dirname = os.path.dirname(fullFilename) + with open(fullFilename, encoding="utf-8", mode="r") as mainModuleSource: + pm.eval(mainModuleSource.read(), {'filename': fullFilename, 'noScriptRval': True}) + # forcibly run in file mode. We shouldn't be getting the last expression of the script as the result value. + +# The pythonmonkey require export. Every time it is used, the stack is inspected so that the filename +# passed to createRequire is correct. This is necessary so that relative requires work. If the filename +# found on the stack doesn't exist, we assume we're in the REPL or something and simply use the current +# directory as the location of a virtual module for relative require purposes. +# +# todo: instead of cwd+__main_virtual__, use a full pathname which includes the directory that the +# running python program is in. +# + def require(moduleIdentifier: str): - # Retrieve the caller’s filename from the call stack - filename = inspect.stack()[1].filename - # From the REPL, the filename is "", which is not a valid path - if not os.path.exists(filename): - filename = os.path.join(os.getcwd(), "__main__") # use the CWD instead - return createRequire(filename)(moduleIdentifier) + filename = inspect.stack()[1].filename + if not os.path.exists(filename): + filename = os.path.join(os.getcwd(), "__main_virtual__") + return createRequire(filename)(moduleIdentifier) + # Restrict what symbols are exposed to the pythonmonkey module. -__all__ = ["globalThis", "require", "createRequire", "runProgramModule"] +__all__ = ["globalThis", "require", "createRequire", "runProgramModule", "bootstrap"] diff --git a/setup.sh b/setup.sh index 5532d89c..68560ade 100755 --- a/setup.sh +++ b/setup.sh @@ -2,24 +2,23 @@ set -euo pipefail IFS=$'\n\t' - # Get number of CPU cores CPUS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || getconf NPROCESSORS_ONLN 2>/dev/null || echo 1) echo "Installing dependencies" if [[ "$OSTYPE" == "linux-gnu"* ]]; then # Linux - sudo apt-get update --yes - sudo apt-get install --yes cmake graphviz llvm clang pkg-config m4 \ - wget curl python3-distutils python3-dev - # Install Doxygen - # the newest version in Ubuntu 20.04 repository is 1.8.17, but we need Doxygen 1.9 series - wget -c -q https://www.doxygen.nl/files/doxygen-1.9.7.linux.bin.tar.gz - tar xf doxygen-1.9.7.linux.bin.tar.gz - cd doxygen-1.9.7 && sudo make install && cd - - rm -rf doxygen-1.9.7 doxygen-1.9.7.linux.bin.tar.gz + SUDO='' + if command -v sudo >/dev/null; then + # sudo is present on the system, so use it + SUDO='sudo' + fi + echo "Installing apt packages" + $SUDO apt-get install --yes cmake llvm clang pkg-config m4 unzip \ + wget curl python3-dev elif [[ "$OSTYPE" == "darwin"* ]]; then # macOS brew update || true # allow failure - brew install cmake doxygen graphviz pkg-config wget coreutils # `coreutils` installs the `realpath` command + brew install cmake pkg-config wget unzip coreutils # `coreutils` installs the `realpath` command + brew install lld elif [[ "$OSTYPE" == "msys"* ]]; then # Windows echo "Dependencies are not going to be installed automatically on Windows." else @@ -27,54 +26,109 @@ else exit 1 fi # Install rust compiler -curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.69 # force to use Rust 1.69 because 1.70 has linking issues on Windows +echo "Installing rust compiler" +unset HOST_ABI_FLAGS +if [[ "$OSTYPE" == "msys"* ]]; then # Windows + HOST_ABI_FLAGS=("--default-host" "$(clang --print-target-triple)") +fi +curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/rust-lang/rustup/refs/tags/1.28.2/rustup-init.sh -sSf | sh -s -- -y ${HOST_ABI_FLAGS+"${HOST_ABI_FLAGS[@]}"} --default-toolchain 1.85 +CARGO_BIN="$HOME/.cargo/bin/cargo" # also works for Windows. On Windows this equals to %USERPROFILE%\.cargo\bin\cargo +$CARGO_BIN install cbindgen # Setup Poetry -curl -sSL https://install.python-poetry.org | python3 - --version "1.5.1" +echo "Installing poetry" +curl -sSL https://install.python-poetry.org | python3 - --version "1.7.1" if [[ "$OSTYPE" == "msys"* ]]; then # Windows POETRY_BIN="$APPDATA/Python/Scripts/poetry" else - POETRY_BIN=`echo ~/.local/bin/poetry` # expand tilde + POETRY_BIN="$HOME/.local/bin/poetry" fi $POETRY_BIN self add 'poetry-dynamic-versioning[plugin]' echo "Done installing dependencies" echo "Downloading spidermonkey source code" -wget -c -q https://ftp.mozilla.org/pub/firefox/releases/115.1.0esr/source/firefox-115.1.0esr.source.tar.xz -mkdir -p firefox-source -tar xf firefox-115.1.0esr.source.tar.xz -C firefox-source --strip-components=1 # strip the root folder +# Read the commit hash for mozilla-central from the `mozcentral.version` file +MOZCENTRAL_VERSION=$(cat mozcentral.version) +wget -c -q -O firefox-source-${MOZCENTRAL_VERSION}.zip https://github.com/mozilla-firefox/firefox/archive/${MOZCENTRAL_VERSION}.zip +unzip -q firefox-source-${MOZCENTRAL_VERSION}.zip && mv firefox-${MOZCENTRAL_VERSION} firefox-source echo "Done downloading spidermonkey source code" echo "Building spidermonkey" cd firefox-source + +# Apply patching # making it work for both GNU and BSD (macOS) versions of sed sed -i'' -e 's/os not in ("WINNT", "OSX", "Android")/os not in ("WINNT", "Android")/' ./build/moz.configure/pkg.configure # use pkg-config on macOS -sed -i'' -e '/"WindowsDllMain.cpp"/d' ./mozglue/misc/moz.build # https://discourse.mozilla.org/t/105671, https://bugzilla.mozilla.org/show_bug.cgi?id=1751561 -sed -i'' -e '/"winheap.cpp"/d' ./memory/mozalloc/moz.build # https://bugzilla.mozilla.org/show_bug.cgi?id=1802675 -sed -i'' -e 's/"install-name-tool"/"install_name_tool"/' ./moz.configure # `install-name-tool` does not exist, but we have `install_name_tool` sed -i'' -e 's/bool Unbox/JS_PUBLIC_API bool Unbox/g' ./js/public/Class.h # need to manually add JS_PUBLIC_API to js::Unbox until it gets fixed in Spidermonkey sed -i'' -e 's/bool js::Unbox/JS_PUBLIC_API bool js::Unbox/g' ./js/src/vm/JSObject.cpp # same here +sed -i'' -e 's/shared_lib = self._pretty_path(libdef.output_path, backend_file)/shared_lib = libdef.lib_name/' ./python/mozbuild/mozbuild/backend/recursivemake.py # would generate a Makefile to install the binary files from an invalid path prefix +sed -i'' -e 's/% self._pretty_path(libdef.import_path, backend_file)/% libdef.import_name/' ./python/mozbuild/mozbuild/backend/recursivemake.py # same as above. Shall we file a bug in bugzilla? +sed -i'' -e 's/if version < Version(mac_sdk_min_version())/if False/' ./build/moz.configure/toolchain.configure # do not verify the macOS SDK version as the required version is not available on Github Actions runner +sed -i'' -e 's/return JS::GetWeakRefsEnabled() == JS::WeakRefSpecifier::Disabled/return false/' ./js/src/vm/GlobalObject.cpp # forcibly enable FinalizationRegistry +sed -i'' -e 's/return !IsIteratorHelpersEnabled()/return false/' ./js/src/vm/GlobalObject.cpp # forcibly enable iterator helpers +sed -i'' -e '/MOZ_CRASH_UNSAFE_PRINTF/,/__PRETTY_FUNCTION__);/d' ./mfbt/LinkedList.h # would crash in Debug Build: in `~LinkedList()` it should have removed all this list's elements before the list's destruction +sed -i'' -e '/MOZ_ASSERT(stackRootPtr == nullptr);/d' ./js/src/vm/JSContext.cpp # would assert false in Debug Build since we extensively use `new JS::Rooted` +sed -i'' -e 's/"-fuse-ld=ld"/"-ld64" if c_compiler.version > "14.0.0" else "-fuse-ld=ld"/' ./build/moz.configure/toolchain.configure # XCode 15 changed the linker behaviour. See https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Linking +sed -i'' -e 's/defined(XP_WIN)/defined(_WIN32)/' ./mozglue/baseprofiler/public/BaseProfilerUtils.h # this header file is introduced to js/Debug.h in https://phabricator.services.mozilla.com/D221102, but it would be compiled without XP_WIN in this building configuration + cd js/src mkdir -p _build cd _build mkdir -p ../../../../_spidermonkey_install/ -../configure \ +../configure --target=$(clang --print-target-triple) \ --prefix=$(realpath $PWD/../../../../_spidermonkey_install) \ --with-intl-api \ - --without-system-zlib \ + $(if [[ "$OSTYPE" != "msys"* ]]; then echo "--without-system-zlib"; fi) \ --disable-debug-symbols \ --disable-jemalloc \ --disable-tests \ - --enable-optimize + $(if [[ "$OSTYPE" == "darwin"* ]]; then echo "--enable-linker=ld64"; fi) \ + --enable-optimize \ + --disable-explicit-resource-management +# disable-explicit-resource-management: Disable the `using` syntax that is enabled by default in SpiderMonkey nightly, otherwise the header files will disagree with the compiled lib .so file +# when it's using a `IF_EXPLICIT_RESOURCE_MANAGEMENT` macro, e.g., the `enum JSProtoKey` index would be off by 1 (header `JSProto_Uint8Array` 27 will be interpreted as `JSProto_Int8Array` in lib as lib has an extra element) +# https://bugzilla.mozilla.org/show_bug.cgi?id=1940342 make -j$CPUS echo "Done building spidermonkey" echo "Installing spidermonkey" # install to ../../../../_spidermonkey_install/ make install -cd ../../../../_spidermonkey_install/lib/ if [[ "$OSTYPE" == "darwin"* ]]; then # macOS + cd ../../../../_spidermonkey_install/lib/ # Set the `install_name` field to use RPATH instead of an absolute path # overrides https://hg.mozilla.org/releases/mozilla-esr102/file/89d799cb/js/src/build/Makefile.in#l83 install_name_tool -id @rpath/$(basename ./libmozjs*) ./libmozjs* # making it work for whatever name the libmozjs dylib is called fi echo "Done installing spidermonkey" + +# if this is being ran in the root directory of the PythonMonkey repo, then include dev configurations +if test -f .git/hooks/pre-commit; then + # set git hooks + ln -s -f ../../githooks/pre-commit .git/hooks/pre-commit + # set blame ignore file + git config blame.ignorerevsfile .git-blame-ignore-revs + # install autopep8 + $POETRY_BIN run pip install autopep8 + # install uncrustify + echo "Downloading uncrustify source code" + wget -c -q https://github.com/uncrustify/uncrustify/archive/refs/tags/uncrustify-0.78.1.tar.gz + mkdir -p uncrustify-source + tar -xzf uncrustify-0.78.1.tar.gz -C uncrustify-source --strip-components=1 # strip the root folder + echo "Done downloading uncrustify source code" + + echo "Building uncrustify" + cd uncrustify-source + mkdir -p build + cd build + if [[ "$OSTYPE" == "msys"* ]]; then # Windows + cmake ../ + cmake --build . -j$CPUS --config Release + cp Release/uncrustify.exe ../../uncrustify.exe + else + cmake ../ + make -j$CPUS + cp uncrustify ../../uncrustify + fi + cd ../.. + echo "Done building uncrustify" +fi diff --git a/src/BoolType.cc b/src/BoolType.cc index 47239a35..48d74b5d 100644 --- a/src/BoolType.cc +++ b/src/BoolType.cc @@ -1,14 +1,16 @@ -#include "include/BoolType.hh" - -#include "include/PyType.hh" -#include "include/TypeEnum.hh" +/** + * @file BoolType.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for representing python bools + * @date 2022-12-02 + * + * @copyright Copyright (c) 2022,2024 Distributive Corp. + * + */ -#include - -BoolType::BoolType(PyObject *object) : PyType(object) {} +#include "include/BoolType.hh" -BoolType::BoolType(long n) : PyType(PyBool_FromLong(n)) {} -long BoolType::getValue() const { - return PyLong_AS_LONG(pyObject); +PyObject *BoolType::getPyObject(long n) { + return PyBool_FromLong(n); } \ No newline at end of file diff --git a/src/BufferType.cc b/src/BufferType.cc index f94b76b3..f0726bce 100644 --- a/src/BufferType.cc +++ b/src/BufferType.cc @@ -1,29 +1,67 @@ /** - * @file BufferType.hh - * @author Tom Tang (xmader@distributive.network) + * @file BufferType.cc + * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing ArrayBuffers - * @version 0.1 * @date 2023-04-27 * - * @copyright Copyright (c) 2023 Distributive Corp. + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #include "include/BufferType.hh" - -#include "include/PyType.hh" -#include "include/TypeEnum.hh" +#include "include/PyBytesProxyHandler.hh" #include #include #include #include -#include +#include + +// JS to Python + +/* static */ +const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) { + // floating point types + switch (subtype) { + case JS::Scalar::Float16: + return "e"; + case JS::Scalar::Float32: + return "f"; + case JS::Scalar::Float64: + return "d"; + } + + // integer types + bool isSigned = JS::Scalar::isSignedIntType(subtype); + uint8_t byteSize = JS::Scalar::byteSize(subtype); + // Python `array` type codes are strictly mapped to basic C types (e.g., `int`), widths may vary on different architectures, + // but JS TypedArray uses fixed-width integer types (e.g., `uint32_t`) + switch (byteSize) { + case sizeof(char): + return isSigned ? "b" : "B"; + case sizeof(short): + return isSigned ? "h" : "H"; + case sizeof(int): + return isSigned ? "i" : "I"; + // case sizeof(long): // compile error: duplicate case value + // // And this is usually where the bit widths on 32/64-bit systems don't agree, + // // see https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models + // return isSigned ? "l" : "L"; + case sizeof(long long): + return isSigned ? "q" : "Q"; + default: // invalid + return "x"; // type code for pad bytes, no value + } +} -BufferType::BufferType(PyObject *object) : PyType(object) {} +/* static */ +bool BufferType::isSupportedJsTypes(JSObject *obj) { + return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj); +} -BufferType::BufferType(JSContext *cx, JS::HandleObject bufObj) { +PyObject *BufferType::getPyObject(JSContext *cx, JS::HandleObject bufObj) { + PyObject *pyObject; if (JS_IsTypedArrayObject(bufObj)) { pyObject = fromJsTypedArray(cx, bufObj); } else if (JS::IsArrayBufferObject(bufObj)) { @@ -33,11 +71,8 @@ BufferType::BufferType(JSContext *cx, JS::HandleObject bufObj) { PyErr_SetString(PyExc_TypeError, "`bufObj` is neither a TypedArray object nor an ArraryBuffer object."); pyObject = nullptr; } -} -/* static */ -bool BufferType::isSupportedJsTypes(JSObject *obj) { - return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj); + return pyObject; } /* static */ @@ -93,13 +128,29 @@ PyObject *BufferType::fromJsArrayBuffer(JSContext *cx, JS::HandleObject arrayBuf return PyMemoryView_FromBuffer(&bufInfo); } -JSObject *BufferType::toJsTypedArray(JSContext *cx) { + +// Python to JS + +static PyBytesProxyHandler pyBytesProxyHandler; + + +JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) { + Py_INCREF(pyObject); + // Get the pyObject's underlying buffer pointer and size Py_buffer *view = new Py_buffer{}; + bool immutable = false; if (PyObject_GetBuffer(pyObject, view, PyBUF_ND | PyBUF_WRITABLE /* C-contiguous and writable */ | PyBUF_FORMAT) < 0) { // the buffer is immutable (e.g., Python `bytes` type is read-only) - return nullptr; // raises a PyExc_BufferError + PyErr_Clear(); // a PyExc_BufferError was raised + + if (PyObject_GetBuffer(pyObject, view, PyBUF_ND /* C-contiguous */ | PyBUF_FORMAT) < 0) { + return nullptr; // a PyExc_BufferError was raised again + } + + immutable = true; } + if (view->ndim != 1) { PyErr_SetString(PyExc_BufferError, "multidimensional arrays are not allowed"); BufferType::_releasePyBuffer(view); @@ -109,22 +160,38 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx) { // Determine the TypedArray's subtype (Uint8Array, Float64Array, ...) JS::Scalar::Type subtype = _getPyBufferType(view); - JSObject *arrayBuffer = nullptr; + JSObject *arrayBuffer; if (view->len > 0) { // Create a new ExternalArrayBuffer object // Note: data will be copied instead of transferring the ownership when this external ArrayBuffer is "transferred" to a worker thread. // see https://hg.mozilla.org/releases/mozilla-esr102/file/a03fde6/js/public/ArrayBuffer.h#l86 + mozilla::UniquePtr dataPtr( + view->buf /* data pointer */, + {BufferType::_releasePyBuffer, view /* the `bufView` argument to `_releasePyBuffer` */} + ); + arrayBuffer = JS::NewExternalArrayBuffer(cx, - view->len /* byteLength */, view->buf /* data pointer */, - BufferType::_releasePyBuffer, view /* the `bufView` argument to `_releasePyBuffer` */ + view->len /* byteLength */, std::move(dataPtr) ); } else { // empty buffer arrayBuffer = JS::NewArrayBuffer(cx, 0); BufferType::_releasePyBuffer(view); // the buffer is no longer needed since we are creating a brand new empty ArrayBuffer } - JS::RootedObject arrayBufferRooted(cx, arrayBuffer); - return _newTypedArrayWithBuffer(cx, subtype, arrayBufferRooted); + if (!immutable) { + JS::RootedObject arrayBufferRooted(cx, arrayBuffer); + return _newTypedArrayWithBuffer(cx, subtype, arrayBufferRooted); + } else { + JS::RootedValue v(cx); + JS::RootedObject uint8ArrayPrototype(cx); + JS_GetClassPrototype(cx, JSProto_Uint8Array, &uint8ArrayPrototype); // so that instanceof will work, not that prototype methods will + JSObject *proxy = js::NewProxyObject(cx, &pyBytesProxyHandler, v, uint8ArrayPrototype.get()); + JS::SetReservedSlot(proxy, PyObjectSlot, JS::PrivateValue(pyObject)); + JS::PersistentRootedObject *arrayBufferPointer = new JS::PersistentRootedObject(cx); + arrayBufferPointer->set(arrayBuffer); + JS::SetReservedSlot(proxy, OtherSlot, JS::PrivateValue(arrayBufferPointer)); + return proxy; + } } /* static */ @@ -153,8 +220,11 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) { return JS::Scalar::Float32; } else if (typeCode == 'd') { return JS::Scalar::Float64; + } else if (typeCode == 'e') { + return JS::Scalar::Float16; } + // integer types // We can't rely on the type codes alone since the typecodes are mapped to C types and would have different sizes on different architectures // see https://docs.python.org/3.9/library/array.html#module-array @@ -176,48 +246,16 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) { } } -/* static */ -const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) { - // floating point types - if (subtype == JS::Scalar::Float32) { - return "f"; - } else if (subtype == JS::Scalar::Float64) { - return "d"; - } - - // integer types - bool isSigned = JS::Scalar::isSignedIntType(subtype); - uint8_t byteSize = JS::Scalar::byteSize(subtype); - // Python `array` type codes are strictly mapped to basic C types (e.g., `int`), widths may vary on different architectures, - // but JS TypedArray uses fixed-width integer types (e.g., `uint32_t`) - switch (byteSize) { - case sizeof(char): - return isSigned ? "b" : "B"; - case sizeof(short): - return isSigned ? "h" : "H"; - case sizeof(int): - return isSigned ? "i" : "I"; - // case sizeof(long): // compile error: duplicate case value - // // And this is usually where the bit widths on 32/64-bit systems don't agree, - // // see https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models - // return isSigned ? "l" : "L"; - case sizeof(long long): - return isSigned ? "q" : "Q"; - default: // invalid - return "x"; // type code for pad bytes, no value - } -} - JSObject *BufferType::_newTypedArrayWithBuffer(JSContext *cx, JS::Scalar::Type subtype, JS::HandleObject arrayBuffer) { switch (subtype) { #define NEW_TYPED_ARRAY_WITH_BUFFER(ExternalType, NativeType, Name) \ -case JS::Scalar::Name: \ - return JS_New ## Name ## ArrayWithBuffer(cx, arrayBuffer, 0 /* byteOffset */, -1 /* use up the ArrayBuffer */); + case JS::Scalar::Name: \ + return JS_New ## Name ## ArrayWithBuffer(cx, arrayBuffer, 0 /* byteOffset */, -1 /* use up the ArrayBuffer */); - JS_FOR_EACH_TYPED_ARRAY(NEW_TYPED_ARRAY_WITH_BUFFER) + JS_FOR_EACH_TYPED_ARRAY(NEW_TYPED_ARRAY_WITH_BUFFER) #undef NEW_TYPED_ARRAY_WITH_BUFFER default: // invalid PyErr_SetString(PyExc_TypeError, "Invalid Python buffer type."); return nullptr; } -} +} \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 69332a3a..d2e2e854 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(pythonmonkey SHARED ) target_include_directories(pythonmonkey PUBLIC ..) +target_compile_definitions(pythonmonkey PRIVATE BUILD_TYPE="${PM_BUILD_TYPE} $") if(WIN32) set_target_properties( diff --git a/src/DateType.cc b/src/DateType.cc index 8b9dc584..63d08db3 100644 --- a/src/DateType.cc +++ b/src/DateType.cc @@ -1,17 +1,21 @@ -#include "include/DateType.hh" +/** + * @file DateType.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for representing python dates + * @date 2022-12-21 + * + * @copyright Copyright (c) 2022,2024 Distributive Corp. + * + */ -#include "include/PyType.hh" -#include "include/TypeEnum.hh" +#include "include/DateType.hh" #include #include -#include #include -DateType::DateType(PyObject *object) : PyType(object) {} - -DateType::DateType(JSContext *cx, JS::HandleObject dateObj) { +PyObject *DateType::getPyObject(JSContext *cx, JS::HandleObject dateObj) { if (!PyDateTimeAPI) { PyDateTime_IMPORT; } // for PyDateTime_FromTimestamp JS::Rooted> args(cx); @@ -30,7 +34,7 @@ DateType::DateType(JSContext *cx, JS::HandleObject dateObj) { JS_CallFunctionName(cx, dateObj, "getUTCSeconds", args, &second); JS_CallFunctionName(cx, dateObj, "getUTCMilliseconds", args, &usecond); - pyObject = PyDateTimeAPI->DateTime_FromDateAndTime( + PyObject *pyObject = PyDateTimeAPI->DateTime_FromDateAndTime( year.toNumber(), month.toNumber() + 1, day.toNumber(), hour.toNumber(), minute.toNumber(), second.toNumber(), usecond.toNumber() * 1000, @@ -39,12 +43,14 @@ DateType::DateType(JSContext *cx, JS::HandleObject dateObj) { PyDateTimeAPI->DateTimeType ); Py_INCREF(PyDateTime_TimeZone_UTC); + + return pyObject; } -JSObject *DateType::toJsDate(JSContext *cx) { +JSObject *DateType::toJsDate(JSContext *cx, PyObject *pyObject) { // See https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp PyObject *timestamp = PyObject_CallMethod(pyObject, "timestamp", NULL); // the result is in seconds double milliseconds = PyFloat_AsDouble(timestamp) * 1000; Py_DECREF(timestamp); return JS::NewDateObject(cx, JS::TimeClip(milliseconds)); -} +} \ No newline at end of file diff --git a/src/DictType.cc b/src/DictType.cc index 6e84e15b..70671032 100644 --- a/src/DictType.cc +++ b/src/DictType.cc @@ -1,36 +1,29 @@ +/** + * @file DictType.cc + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct representing python dictionaries + * @date 2022-08-10 + * + * @copyright Copyright (c) 2022,2024 Distributive Corp. + * + */ + + #include "include/DictType.hh" -#include "include/modules/pythonmonkey/pythonmonkey.hh" #include "include/JSObjectProxy.hh" -#include "include/PyType.hh" -#include "include/pyTypeFactory.hh" -#include #include -#include - -#include - -DictType::DictType() { - this->pyObject = PyDict_New(); -} -DictType::DictType(PyObject *object) : PyType(object) {} - -DictType::DictType(JSContext *cx, JS::Handle jsObject) { +PyObject *DictType::getPyObject(JSContext *cx, JS::Handle jsObject) { JSObjectProxy *proxy = (JSObjectProxy *)PyObject_CallObject((PyObject *)&JSObjectProxyType, NULL); - JS::RootedObject obj(cx); - JS_ValueToObject(cx, jsObject, &obj); - proxy->jsObject.set(obj); - this->pyObject = (PyObject *)proxy; -} - -void DictType::set(PyType *key, PyType *value) { - PyDict_SetItem(this->pyObject, key->getPyObject(), value->getPyObject()); -} - -PyType *DictType::get(PyType *key) const { - PyObject *retrieved_object = PyDict_GetItem(this->pyObject, key->getPyObject()); - return retrieved_object != NULL ? pyTypeFactory(retrieved_object) : nullptr; + if (proxy != NULL) { + JS::RootedObject obj(cx); + JS_ValueToObject(cx, jsObject, &obj); + proxy->jsObject = new JS::PersistentRootedObject(cx); + proxy->jsObject->set(obj); + return (PyObject *)proxy; + } + return NULL; } \ No newline at end of file diff --git a/src/ExceptionType.cc b/src/ExceptionType.cc index e3fdb92f..620643f9 100644 --- a/src/ExceptionType.cc +++ b/src/ExceptionType.cc @@ -1,51 +1,315 @@ +/** + * @file ExceptionType.cc + * @author Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for representing Python Exception objects from a corresponding JS Error object + * @date 2023-04-11 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + #include "include/modules/pythonmonkey/pythonmonkey.hh" #include "include/setSpiderMonkeyException.hh" #include "include/ExceptionType.hh" +#include "include/StrType.hh" +#include "include/DictType.hh" +#include "include/JSObjectProxy.hh" #include #include #include +#include +#include "include/pyshim.hh" -ExceptionType::ExceptionType(PyObject *object) : PyType(object) {} -ExceptionType::ExceptionType(JSContext *cx, JS::HandleObject error) { +PyObject *ExceptionType::getPyObject(JSContext *cx, JS::HandleObject error) { // Convert the JS Error object to a Python string JS::RootedValue errValue(cx, JS::ObjectValue(*error)); // err JS::RootedObject errStack(cx, JS::ExceptionStackOrNull(error)); // err.stack - PyObject *errStr = getExceptionString(cx, JS::ExceptionStack(cx, errValue, errStack)); + PyObject *errStr = getExceptionString(cx, JS::ExceptionStack(cx, errValue, errStack), true); // Construct a new SpiderMonkeyError python object - // pyObject = SpiderMonkeyError(errStr) - #if PY_VERSION_HEX >= 0x03090000 - pyObject = PyObject_CallOneArg(SpiderMonkeyError, errStr); // _PyErr_CreateException, https://github.com/python/cpython/blob/3.9/Python/errors.c#L100 - #else - pyObject = PyObject_CallFunction(SpiderMonkeyError, "O", errStr); // PyObject_CallOneArg is not available in Python < 3.9 - #endif + PyObject *pyObject = PyObject_CallOneArg(SpiderMonkeyError, errStr); // _PyErr_CreateException, https://github.com/python/cpython/blob/3.9/Python/errors.c#L100 Py_XDECREF(errStr); + + // Preserve the original JS Error object as the Python Exception's `jsError` attribute for lossless two-way conversion + PyObject *originalJsErrCapsule = DictType::getPyObject(cx, errValue); + PyObject_SetAttrString(pyObject, "jsError", originalJsErrCapsule); + + return pyObject; +} + + +// Generating trace information + +#define PyTraceBack_LIMIT 1000 + +static const int TB_RECURSIVE_CUTOFF = 3; + +#if PY_VERSION_HEX >= 0x03090000 + +static inline int +tb_get_lineno(PyTracebackObject *tb) { + PyFrameObject *frame = tb->tb_frame; + PyCodeObject *code = PyFrame_GetCode(frame); + int lineno = PyCode_Addr2Line(code, tb->tb_lasti); + Py_DECREF(code); + return lineno; +} + +#endif + +static int +tb_print_line_repeated(_PyUnicodeWriter *writer, long cnt) +{ + cnt -= TB_RECURSIVE_CUTOFF; + PyObject *line = PyUnicode_FromFormat( + (cnt > 1) + ? "[Previous line repeated %ld more times]\n" + : "[Previous line repeated %ld more time]\n", + cnt); + if (line == NULL) { + return -1; + } + int err = _PyUnicodeWriter_WriteStr(writer, line); + Py_DECREF(line); + return err; } -// TODO (Tom Tang): preserve the original Python exception object somewhere in the JS obj for lossless two-way conversion -JSObject *ExceptionType::toJsError(JSContext *cx) { - PyObject *pyErrType = PyObject_Type(pyObject); +JSObject *ExceptionType::toJsError(JSContext *cx, PyObject *exceptionValue, PyObject *traceBack) { + assert(exceptionValue != NULL); + + if (PyObject_HasAttrString(exceptionValue, "jsError")) { + PyObject *originalJsErrCapsule = PyObject_GetAttrString(exceptionValue, "jsError"); + if (originalJsErrCapsule && PyObject_TypeCheck(originalJsErrCapsule, &JSObjectProxyType)) { + return *((JSObjectProxy *)originalJsErrCapsule)->jsObject; + } + } + + // Gather JS context + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wformat-zero-length" + JS_ReportErrorASCII(cx, ""); // throw JS error and gather all details + #pragma GCC diagnostic pop + + JS::ExceptionStack exceptionStack(cx); + if (!JS::GetPendingExceptionStack(cx, &exceptionStack)) { + return NULL; + } + JS_ClearPendingException(cx); + + std::stringstream stackStream; + JS::RootedObject stackObj(cx, exceptionStack.stack()); + if (stackObj.get()) { + JS::RootedString stackStr(cx); + JS::BuildStackString(cx, nullptr, stackObj, &stackStr, 2, js::StackFormat::SpiderMonkey); + JS::UniqueChars stackStrUtf8 = JS_EncodeStringToUTF8(cx, stackStr); + stackStream << "\nJS Stack Trace:\n" << stackStrUtf8.get(); + } + + + // Gather Python context + PyObject *pyErrType = PyObject_Type(exceptionValue); const char *pyErrTypeName = _PyType_Name((PyTypeObject *)pyErrType); - PyObject *pyErrMsg = PyObject_Str(pyObject); - // TODO (Tom Tang): Convert Python traceback and set it as the `stack` property on JS Error object - // PyObject *traceback = PyException_GetTraceback(pyObject); + + PyObject *pyErrMsg = PyObject_Str(exceptionValue); + + if (traceBack) { + _PyUnicodeWriter writer; + _PyUnicodeWriter_Init(&writer); + + PyObject *fileName = NULL; + int lineno = -1; + + PyTracebackObject *tb = (PyTracebackObject *)traceBack; + + long limit = PyTraceBack_LIMIT; + + PyObject *limitv = PySys_GetObject("tracebacklimit"); + if (limitv && PyLong_Check(limitv)) { + int overflow; + limit = PyLong_AsLongAndOverflow(limitv, &overflow); + if (overflow > 0) { + limit = LONG_MAX; + } + else if (limit <= 0) { + return NULL; + } + } + + PyCodeObject *code = NULL; + Py_ssize_t depth = 0; + PyObject *last_file = NULL; + int last_line = -1; + PyObject *last_name = NULL; + long cnt = 0; + PyTracebackObject *tb1 = tb; + int err = 0; + + int res; + PyObject *line = PyUnicode_FromString("Traceback (most recent call last):\n"); + if (line == NULL) { + goto error; + } + res = _PyUnicodeWriter_WriteStr(&writer, line); + Py_DECREF(line); + if (res < 0) { + goto error; + } + + // TODO should we reverse the stack and put it in the more common, non-python, top-most to bottom-most order? Wait for user feedback on experience + while (tb1 != NULL) { + depth++; + tb1 = tb1->tb_next; + } + while (tb != NULL && depth > limit) { + depth--; + tb = tb->tb_next; + } + +#if PY_VERSION_HEX >= 0x03090000 + + while (tb != NULL) { + code = PyFrame_GetCode(tb->tb_frame); + + int tb_lineno = tb->tb_lineno; + if (tb_lineno == -1) { + tb_lineno = tb_get_lineno(tb); + } + + if (last_file == NULL || + code->co_filename != last_file || + last_line == -1 || tb_lineno != last_line || + last_name == NULL || code->co_name != last_name) { + + if (cnt > TB_RECURSIVE_CUTOFF) { + if (tb_print_line_repeated(&writer, cnt) < 0) { + goto error; + } + } + last_file = code->co_filename; + last_line = tb_lineno; + last_name = code->co_name; + cnt = 0; + } + + cnt++; + + if (cnt <= TB_RECURSIVE_CUTOFF) { + fileName = code->co_filename; + lineno = tb_lineno; + + line = PyUnicode_FromFormat("File \"%U\", line %d, in %U\n", fileName, lineno, code->co_name); + if (line == NULL) { + goto error; + } + + int res = _PyUnicodeWriter_WriteStr(&writer, line); + Py_DECREF(line); + if (res < 0) { + goto error; + } + } + + Py_CLEAR(code); + tb = tb->tb_next; + } + if (cnt > TB_RECURSIVE_CUTOFF) { + if (tb_print_line_repeated(&writer, cnt) < 0) { + goto error; + } + } + +#else + + while (tb != NULL && err == 0) { + if (last_file == NULL || + tb->tb_frame->f_code->co_filename != last_file || + last_line == -1 || tb->tb_lineno != last_line || + last_name == NULL || tb->tb_frame->f_code->co_name != last_name) { + if (cnt > TB_RECURSIVE_CUTOFF) { + err = tb_print_line_repeated(&writer, cnt); + } + last_file = tb->tb_frame->f_code->co_filename; + last_line = tb->tb_lineno; + last_name = tb->tb_frame->f_code->co_name; + cnt = 0; + } + cnt++; + if (err == 0 && cnt <= TB_RECURSIVE_CUTOFF) { + fileName = tb->tb_frame->f_code->co_filename; + lineno = tb->tb_lineno; + + line = PyUnicode_FromFormat("File \"%U\", line %d, in %U\n", fileName, lineno, tb->tb_frame->f_code->co_name); + if (line == NULL) { + goto error; + } + + int res = _PyUnicodeWriter_WriteStr(&writer, line); + Py_DECREF(line); + if (res < 0) { + goto error; + } + } + tb = tb->tb_next; + } + if (err == 0 && cnt > TB_RECURSIVE_CUTOFF) { + err = tb_print_line_repeated(&writer, cnt); + } + + if (err) { + goto error; + } + +#endif + + { + std::stringstream msgStream; + msgStream << "Python " << pyErrTypeName << ": " << PyUnicode_AsUTF8(pyErrMsg) << "\n" << PyUnicode_AsUTF8(_PyUnicodeWriter_Finish(&writer)); + msgStream << stackStream.str(); + + JS::RootedValue rval(cx); + JS::RootedString filename(cx, JS_NewStringCopyZ(cx, PyUnicode_AsUTF8(fileName))); + JS::RootedString message(cx, JS_NewStringCopyZ(cx, msgStream.str().c_str())); + // stack argument cannot be passed in as a string anymore (deprecated), and could not find a proper example using the new argument type + if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, lineno, JS::ColumnNumberOneOrigin(1), nullptr, message, JS::NothingHandleValue, &rval)) { + return NULL; + } + + Py_DECREF(pyErrType); + Py_DECREF(pyErrMsg); + + return rval.toObjectOrNull(); + } + + error: + _PyUnicodeWriter_Dealloc(&writer); + Py_XDECREF(code); + } + + // gather additional JS context details + JS::ErrorReportBuilder reportBuilder(cx); + if (!reportBuilder.init(cx, exceptionStack, JS::ErrorReportBuilder::WithSideEffects)) { + return NULL; + } + JSErrorReport *errorReport = reportBuilder.report(); std::stringstream msgStream; msgStream << "Python " << pyErrTypeName << ": " << PyUnicode_AsUTF8(pyErrMsg); - std::string msg = msgStream.str(); + msgStream << stackStream.str(); JS::RootedValue rval(cx); - JS::RootedObject stack(cx); - JS::RootedString filename(cx, JS_NewStringCopyZ(cx, "[python code]")); - JS::RootedString message(cx, JS_NewStringCopyZ(cx, msg.c_str())); - JS::CreateError(cx, JSExnType::JSEXN_ERR, stack, filename, 0, 0, nullptr, message, JS::NothingHandleValue, &rval); + JS::RootedString filename(cx, JS_NewStringCopyZ(cx, "")); // cannot be null or omitted, but is overriden by the errorReport + JS::RootedString message(cx, JS_NewStringCopyZ(cx, msgStream.str().c_str())); + // filename cannot be null + if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, 0, JS::ColumnNumberOneOrigin(1), errorReport, message, JS::NothingHandleValue, &rval)) { + return NULL; + } Py_DECREF(pyErrType); Py_DECREF(pyErrMsg); return rval.toObjectOrNull(); -} +} \ No newline at end of file diff --git a/src/FloatType.cc b/src/FloatType.cc index 4c4c4bf0..cad25153 100644 --- a/src/FloatType.cc +++ b/src/FloatType.cc @@ -1,16 +1,17 @@ -#include "include/FloatType.hh" - -#include "include/PyType.hh" -#include "include/TypeEnum.hh" - -#include +/** + * @file FloatType.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for representing python floats + * @date 2022-12-02 + * + * @copyright Copyright (c) 2022,2024 Distributive Corp. + * + */ -FloatType::FloatType(PyObject *object) : PyType(object) {} - -FloatType::FloatType(long n) : PyType(Py_BuildValue("d", (double)n)) {} - -FloatType::FloatType(double n) : PyType(Py_BuildValue("d", n)) {} +#include "include/FloatType.hh" -double FloatType::getValue() const { - return PyFloat_AS_DOUBLE(pyObject); +PyObject *FloatType::getPyObject(double n) { + PyObject *doubleVal = Py_BuildValue("d", n); + Py_INCREF(doubleVal); + return doubleVal; } \ No newline at end of file diff --git a/src/FuncType.cc b/src/FuncType.cc index 553bf650..415eb892 100644 --- a/src/FuncType.cc +++ b/src/FuncType.cc @@ -1,11 +1,21 @@ -#include "include/FuncType.hh" +/** + * @file FuncType.cc + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct representing python functions + * @date 2022-08-08 + * + * @copyright Copyright (c) 2022,2024 Distributive Corp. + * + */ -#include "include/PyType.hh" +#include "include/FuncType.hh" +#include "include/JSFunctionProxy.hh" -#include +#include -FuncType::FuncType(PyObject *object) : PyType(object) {} -const char *FuncType::getValue() const { - return PyUnicode_AsUTF8(PyObject_GetAttrString(pyObject, "__name__")); +PyObject *FuncType::getPyObject(JSContext *cx, JS::HandleValue fval) { + JSFunctionProxy *proxy = (JSFunctionProxy *)PyObject_CallObject((PyObject *)&JSFunctionProxyType, NULL); + proxy->jsFunc->set(&fval.toObject()); + return (PyObject *)proxy; } \ No newline at end of file diff --git a/src/IntType.cc b/src/IntType.cc index 71b8c976..26a7ea1e 100644 --- a/src/IntType.cc +++ b/src/IntType.cc @@ -1,14 +1,21 @@ -#include "include/modules/pythonmonkey/pythonmonkey.hh" +/** + * @file IntType.cc + * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) & Tom Tang (xmader@distributive.network) + * @brief Struct for representing python ints + * @date 2023-03-16 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ +#include "include/modules/pythonmonkey/pythonmonkey.hh" #include "include/IntType.hh" -#include "include/PyType.hh" -#include "include/TypeEnum.hh" - #include #include #include +#include "include/pyshim.hh" #include @@ -40,11 +47,7 @@ static inline void PythonLong_SetSign(PyLongObject *op, int sign) { #else // Python version is less than 3.12 // see https://github.com/python/cpython/blob/v3.9.16/Objects/longobject.c#L956 Py_ssize_t pyDigitCount = Py_SIZE(op); - #if PY_VERSION_HEX >= 0x03090000 Py_SET_SIZE(op, sign * std::abs(pyDigitCount)); - #else - ((PyVarObject *)op)->ob_size = sign * std::abs(pyDigitCount); // Py_SET_SIZE is not available in Python < 3.9 - #endif #endif } @@ -64,11 +67,8 @@ static inline bool PythonLong_IsNegative(const PyLongObject *op) { #endif } -IntType::IntType(PyObject *object) : PyType(object) {} - -IntType::IntType(long n) : PyType(Py_BuildValue("i", n)) {} -IntType::IntType(JSContext *cx, JS::BigInt *bigint) { +PyObject *IntType::getPyObject(JSContext *cx, JS::BigInt *bigint) { // Get the sign bit bool isNegative = BigIntIsNegative(bigint); @@ -101,20 +101,18 @@ IntType::IntType(JSContext *cx, JS::BigInt *bigint) { // Cast to a pythonmonkey.bigint to differentiate it from a normal Python int, // allowing Py<->JS two-way BigInt conversion. // We don't do `Py_SET_TYPE` because `_PyLong_FromByteArray` may cache and reuse objects for small ints - #if PY_VERSION_HEX >= 0x03090000 - pyObject = PyObject_CallOneArg(PythonMonkey_BigInt, pyIntObj); // pyObject = pythonmonkey.bigint(pyIntObj) - #else - pyObject = PyObject_CallFunction(PythonMonkey_BigInt, "O", pyIntObj); // PyObject_CallOneArg is not available in Python < 3.9 - #endif + PyObject *pyObject = PyObject_CallOneArg(getPythonMonkeyBigInt(), pyIntObj); // pyObject = pythonmonkey.bigint(pyIntObj) Py_DECREF(pyIntObj); // Set the sign bit if (isNegative) { PythonLong_SetSign((PyLongObject *)pyObject, -1); } + + return pyObject; } -JS::BigInt *IntType::toJsBigInt(JSContext *cx) { +JS::BigInt *IntType::toJsBigInt(JSContext *cx, PyObject *pyObject) { // Figure out how many 64-bit "digits" we would have for JS BigInt // see https://github.com/python/cpython/blob/3.9/Modules/_randommodule.c#L306 size_t bitCount = _PyLong_NumBits(pyObject); @@ -136,7 +134,7 @@ JS::BigInt *IntType::toJsBigInt(JSContext *cx) { // Convert to bytes of 8-bit "digits" in **big-endian** order size_t byteCount = (size_t)JS_DIGIT_BYTE * jsDigitCount; uint8_t *bytes = (uint8_t *)PyMem_Malloc(byteCount); - _PyLong_AsByteArray((PyLongObject *)pyObject, bytes, byteCount, /*is_little_endian*/ false, false); + PyLong_AsByteArray((PyLongObject *)pyObject, bytes, byteCount, /*is_little_endian*/ false, false); // Convert pm.bigint to JS::BigInt through hex strings (no public API to convert directly through bytes) // TODO (Tom Tang): We could manually allocate the memory, https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.cpp#l162, but still no public API @@ -167,4 +165,4 @@ JS::BigInt *IntType::toJsBigInt(JSContext *cx) { } return bigint; -} +} \ No newline at end of file diff --git a/src/JSArrayIterProxy.cc b/src/JSArrayIterProxy.cc new file mode 100644 index 00000000..6f27c16c --- /dev/null +++ b/src/JSArrayIterProxy.cc @@ -0,0 +1,82 @@ +/** + * @file JSArrayIterProxy.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSArrayIterProxy is a custom C-implemented python type that derives from list iterator + * @date 2024-01-15 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + + +#include "include/JSArrayIterProxy.hh" + +#include "include/JSArrayProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" + +#include "include/pyTypeFactory.hh" + +#include + +#include + + +void JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_dealloc(JSArrayIterProxy *self) +{ + PyObject_GC_UnTrack(self); + Py_XDECREF(self->it.it_seq); + PyObject_GC_Del(self); +} + +int JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_traverse(JSArrayIterProxy *self, visitproc visit, void *arg) { + Py_VISIT(self->it.it_seq); + return 0; +} + +int JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_clear(JSArrayIterProxy *self) { + Py_CLEAR(self->it.it_seq); + return 0; +} + +PyObject *JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_iter(JSArrayIterProxy *self) { + Py_INCREF(&self->it); + return (PyObject *)&self->it; +} + +PyObject *JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_next(JSArrayIterProxy *self) { + PyListObject *seq = self->it.it_seq; + if (seq == NULL) { + return NULL; + } + + if (self->it.reversed) { + if (self->it.it_index >= 0) { + JS::RootedValue elementVal(GLOBAL_CX); + JS_GetElement(GLOBAL_CX, *(((JSArrayProxy *)seq)->jsArray), self->it.it_index--, &elementVal); + return pyTypeFactory(GLOBAL_CX, elementVal); + } + } + else { + if (self->it.it_index < JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)seq)) { + JS::RootedValue elementVal(GLOBAL_CX); + JS_GetElement(GLOBAL_CX, *(((JSArrayProxy *)seq)->jsArray), self->it.it_index++, &elementVal); + return pyTypeFactory(GLOBAL_CX, elementVal); + } + } + + self->it.it_seq = NULL; + Py_DECREF(seq); + return NULL; +} + +PyObject *JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_len(JSArrayIterProxy *self) { + Py_ssize_t len; + if (self->it.it_seq) { + len = JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)self->it.it_seq) - self->it.it_index; + if (len >= 0) { + return PyLong_FromSsize_t(len); + } + } + return PyLong_FromLong(0); +} \ No newline at end of file diff --git a/src/JSArrayProxy.cc b/src/JSArrayProxy.cc new file mode 100644 index 00000000..869e1503 --- /dev/null +++ b/src/JSArrayProxy.cc @@ -0,0 +1,1312 @@ +/** + * @file JSArrayProxy.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSArrayProxy is a custom C-implemented python type that derives from list. It acts as a proxy for JSArrays from Spidermonkey, and behaves like a list would. + * @date 2023-11-22 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + + +#include "include/JSArrayProxy.hh" + +#include "include/JSArrayIterProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/jsTypeFactory.hh" +#include "include/pyTypeFactory.hh" +#include "include/PyBaseProxyHandler.hh" +#include "include/JSFunctionProxy.hh" + +#include +#include + +#include +#include "include/pyshim.hh" + + +void JSArrayProxyMethodDefinitions::JSArrayProxy_dealloc(JSArrayProxy *self) +{ + self->jsArray->set(nullptr); + delete self->jsArray; + PyObject_GC_UnTrack(self); + PyObject_GC_Del(self); +} + +int JSArrayProxyMethodDefinitions::JSArrayProxy_traverse(JSArrayProxy *self, visitproc visit, void *arg) +{ + // Nothing to be done + return 0; +} + +int JSArrayProxyMethodDefinitions::JSArrayProxy_clear(JSArrayProxy *self) +{ + // Nothing to be done + return 0; +} + +Py_ssize_t JSArrayProxyMethodDefinitions::JSArrayProxy_length(JSArrayProxy *self) +{ + uint32_t length; + JS::GetArrayLength(GLOBAL_CX, *(self->jsArray), &length); + return (Py_ssize_t)length; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_get(JSArrayProxy *self, PyObject *key) +{ + JS::RootedId id(GLOBAL_CX); + if (!keyToId(key, &id)) { + PyErr_SetString(PyExc_AttributeError, "JSArrayProxy property name must be of type str or int"); + return NULL; + } + + // look through the methods for dispatch and return key if no method found + for (size_t index = 0;; index++) { + const char *methodName = JSArrayProxyType.tp_methods[index].ml_name; + if (methodName == NULL || !PyUnicode_Check(key)) { // reached end of list + JS::RootedValue value(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsArray), id, &value); + if (value.isUndefined() && PyUnicode_Check(key)) { + if (strcmp("__class__", PyUnicode_AsUTF8(key)) == 0) { + return PyObject_GenericGetAttr((PyObject *)self, key); + } + } + return pyTypeFactory(GLOBAL_CX, value); + } + else { + if (strcmp(methodName, PyUnicode_AsUTF8(key)) == 0) { + return PyObject_GenericGetAttr((PyObject *)self, key); + } + } + } +} + +// private +static PyObject *list_slice(JSArrayProxy *self, Py_ssize_t ilow, Py_ssize_t ihigh) +{ + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setInt32(ilow); + jArgs[1].setInt32(ihigh); + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "slice", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + return pyTypeFactory(GLOBAL_CX, jReturnedArray); +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_get_subscript(JSArrayProxy *self, PyObject *key) +{ + if (PyIndex_Check(key)) { + Py_ssize_t index = PyNumber_AsSsize_t(key, PyExc_IndexError); + if (index == -1 && PyErr_Occurred()) { + return NULL; + } + + Py_ssize_t selfLength = JSArrayProxy_length(self); + + if (index < 0) { + index += selfLength; + } + + if ((size_t)index >= (size_t)selfLength) { + PyErr_SetObject(PyExc_IndexError, PyUnicode_FromString("list index out of range")); + return NULL; + } + + JS::RootedId id(GLOBAL_CX); + JS_IndexToId(GLOBAL_CX, index, &id); + + JS::RootedValue value(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsArray), id, &value); + + return pyTypeFactory(GLOBAL_CX, value); + } + else if (PySlice_Check(key)) { + Py_ssize_t start, stop, step, slicelength, index; + + if (PySlice_Unpack(key, &start, &stop, &step) < 0) { + return NULL; + } + + slicelength = PySlice_AdjustIndices(JSArrayProxy_length(self), &start, &stop, step); + + if (slicelength <= 0) { + return PyList_New(0); + } + else if (step == 1) { + return list_slice(self, start, stop); + } + else { + JS::RootedObject jCombinedArray(GLOBAL_CX, JS::NewArrayObject(GLOBAL_CX, slicelength)); + + JS::RootedValue elementVal(GLOBAL_CX); + for (size_t cur = start, index = 0; index < slicelength; cur += (size_t)step, index++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), cur, &elementVal); + JS_SetElement(GLOBAL_CX, jCombinedArray, index, elementVal); + } + + JS::RootedValue jCombinedArrayValue(GLOBAL_CX); + jCombinedArrayValue.setObjectOrNull(jCombinedArray); + + return pyTypeFactory(GLOBAL_CX, jCombinedArrayValue); + } + } + else { + PyErr_Format(PyExc_TypeError, "list indices must be integers or slices, not %.200s", Py_TYPE(key)->tp_name); + return NULL; + } +} + +/* a[ilow:ihigh] = v if v != NULL. + * del a[ilow:ihigh] if v == NULL. + */ +// private +static int list_ass_slice(JSArrayProxy *self, Py_ssize_t ilow, Py_ssize_t ihigh, PyObject *v) +{ + /* Because [X]DECREF can recursively invoke list operations on + this list, we must postpone all [X]DECREF activity until + after the list is back in its canonical shape. Therefore + we must allocate an additional array, 'recycle', into which + we temporarily copy the items that are deleted from the + list. :-( */ + PyObject **item; + PyObject **vitem = NULL; + PyObject *v_as_SF = NULL; /* PySequence_Fast(v) */ + Py_ssize_t n; /* # of elements in replacement list */ + Py_ssize_t norig; /* # of elements in list getting replaced */ + Py_ssize_t d; /* Change in size */ + Py_ssize_t k; + size_t s; + int result = -1; /* guilty until proved innocent */ +#define b ((PyListObject *)v) + Py_ssize_t selfLength = JSArrayProxyMethodDefinitions::JSArrayProxy_length(self); + if (v == NULL) { + n = 0; + } + else { + if ((PyListObject *)self == b) { + /* Special case "a[i:j] = a" -- copy b first */ + v = list_slice(self, 0, selfLength); + if (v == NULL) { + return result; + } + result = list_ass_slice(self, ilow, ihigh, v); + Py_DECREF(v); + return result; + } + v_as_SF = PySequence_Fast(v, "can only assign an iterable"); + if (v_as_SF == NULL) { + return result; + } + n = PySequence_Fast_GET_SIZE(v_as_SF); + vitem = PySequence_Fast_ITEMS(v_as_SF); + } + + if (ilow < 0) { + ilow = 0; + } + else if (ilow > selfLength) { + ilow = selfLength; + } + + if (ihigh < ilow) { + ihigh = ilow; + } + else if (ihigh > selfLength) { + ihigh = selfLength; + } + + norig = ihigh - ilow; + assert(norig >= 0); + d = n - norig; + + if (selfLength + d == 0) { + Py_XDECREF(v_as_SF); + JSArrayProxyMethodDefinitions::JSArrayProxy_clear_method(self); + return 0; + } + + if (d < 0) { /* Delete -d items */ + JS::RootedValue elementVal(GLOBAL_CX); + for (size_t index = ihigh, count = 0; count < selfLength - ihigh; index++, count++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + JS_SetElement(GLOBAL_CX, *(self->jsArray), index+d, elementVal); + } + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), selfLength + d); + } + else if (d > 0) { /* Insert d items */ + k = selfLength; + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), k + d); + + selfLength = k + d; + + JS::RootedValue elementVal(GLOBAL_CX); + for (size_t index = ihigh, count = 0; count < k - ihigh; index++, count++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + JS_SetElement(GLOBAL_CX, *(self->jsArray), index+d, elementVal); + } + } + + JS::RootedValue elementVal(GLOBAL_CX); + for (k = 0; k < n; k++, ilow++) { + elementVal.set(jsTypeFactory(GLOBAL_CX, vitem[k])); + JS_SetElement(GLOBAL_CX, *(self->jsArray), ilow, elementVal); + } + + result = 0; + Py_XDECREF(v_as_SF); + return result; +#undef b +} + +int JSArrayProxyMethodDefinitions::JSArrayProxy_assign_key(JSArrayProxy *self, PyObject *key, PyObject *value) +{ + if (PyIndex_Check(key)) { + Py_ssize_t index = PyNumber_AsSsize_t(key, PyExc_IndexError); + if (index == -1 && PyErr_Occurred()) { + return -1; + } + + Py_ssize_t selfLength = JSArrayProxy_length(self); + + if (index < 0) { + index += selfLength; + } + + if ((size_t)index >= (size_t)selfLength) { + PyErr_SetObject(PyExc_IndexError, PyUnicode_FromString("list assignment index out of range")); + return -1; + } + + JS::RootedId id(GLOBAL_CX); + JS_IndexToId(GLOBAL_CX, index, &id); + + if (value) { // we are setting a value + JS::RootedValue jValue(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, value)); + JS_SetPropertyById(GLOBAL_CX, *(self->jsArray), id, jValue); + } else { // we are deleting a value + JS::ObjectOpResult ignoredResult; + JS_DeletePropertyById(GLOBAL_CX, *(self->jsArray), id, ignoredResult); + } + + return 0; + } + else if (PySlice_Check(key)) { + Py_ssize_t start, stop, step, slicelength; + + if (PySlice_Unpack(key, &start, &stop, &step) < 0) { + return -1; + } + + Py_ssize_t selfSize = JSArrayProxy_length(self); + + slicelength = PySlice_AdjustIndices(selfSize, &start, &stop, step); + + if (step == 1) { + return list_ass_slice(self, start, stop, value); + } + + /* Make sure s[5:2] = [..] inserts at the right place: + before 5, not before 2. */ + if ((step < 0 && start < stop) || (step > 0 && start > stop)) { + stop = start; + } + + if (value == NULL) { + /* delete slice */ + size_t cur; + Py_ssize_t i; + + if (slicelength <= 0) { + return 0; + } + + if (step < 0) { + stop = start + 1; + start = stop + step*(slicelength - 1) - 1; + step = -step; + } + + /* drawing pictures might help understand these for + loops. Basically, we memmove the parts of the + list that are *not* part of the slice: step-1 + items for each item that is part of the slice, + and then tail end of the list that was not + covered by the slice */ + JS::RootedValue elementVal(GLOBAL_CX); + for (cur = start, i = 0; cur < (size_t)stop; cur += step, i++) { + Py_ssize_t lim = step - 1; + + if (cur + step >= (size_t)selfSize) { + lim = selfSize - cur - 1; + } + + for (size_t index = cur, count = 0; count < lim; index++, count++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index + 1, &elementVal); + JS_SetElement(GLOBAL_CX, *(self->jsArray), index - i, elementVal); + } + } + + cur = start + (size_t)slicelength * step; + + if (cur < (size_t)selfSize) { + for (size_t index = cur, count = 0; count < selfSize - cur; index++, count++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + JS_SetElement(GLOBAL_CX, *(self->jsArray), index - slicelength, elementVal); + } + } + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), selfSize - slicelength); + + return 0; + } + else { + /* assign slice */ + PyObject *ins, *seq; + PyObject **seqitems, **selfitems; + Py_ssize_t i; + size_t cur; + + /* protect against a[::-1] = a */ + if ((PyListObject *)self == (PyListObject *)value) { + seq = list_slice((JSArrayProxy *)value, 0, JSArrayProxy_length((JSArrayProxy *)value)); + } + else { + seq = PySequence_Fast(value, "must assign iterable to extended slice"); + } + + if (!seq) { + return -1; + } + + if (PySequence_Fast_GET_SIZE(seq) != slicelength) { + PyErr_Format(PyExc_ValueError, "attempt to assign sequence of size %zd to extended slice of size %zd", + PySequence_Fast_GET_SIZE(seq), slicelength); + Py_DECREF(seq); + return -1; + } + + if (!slicelength) { + Py_DECREF(seq); + return 0; + } + + seqitems = PySequence_Fast_ITEMS(seq); + + JS::RootedValue elementVal(GLOBAL_CX); + for (cur = start, i = 0; i < slicelength; cur += (size_t)step, i++) { + elementVal.set(jsTypeFactory(GLOBAL_CX, seqitems[i])); + JS_SetElement(GLOBAL_CX, *(self->jsArray), cur, elementVal); + } + + Py_DECREF(seq); + + return 0; + } + } + else { + PyErr_Format(PyExc_TypeError, "list indices must be integers or slices, not %.200s", Py_TYPE(key)->tp_name); + return -1; + } +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_richcompare(JSArrayProxy *self, PyObject *other, int op) +{ + if (!PyList_Check(self) || !PyList_Check(other)) { + Py_RETURN_NOTIMPLEMENTED; + } + + if (self == (JSArrayProxy *)other && (op == Py_EQ || op == Py_NE)) { + if (op == Py_EQ) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } + } + + Py_ssize_t selfLength = JSArrayProxy_length(self); + Py_ssize_t otherLength; + + if (PyObject_TypeCheck(other, &JSArrayProxyType)) { + otherLength = JSArrayProxy_length((JSArrayProxy *)other); + } else { + otherLength = Py_SIZE(other); + } + + if (selfLength != otherLength && (op == Py_EQ || op == Py_NE)) { + /* Shortcut: if the lengths differ, the lists differ */ + if (op == Py_EQ) { + Py_RETURN_FALSE; + } + else { + Py_RETURN_TRUE; + } + } + + JS::RootedValue elementVal(GLOBAL_CX); + + Py_ssize_t index; + /* Search for the first index where items are different */ + for (index = 0; index < selfLength && index < otherLength; index++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + + PyObject *leftItem = pyTypeFactory(GLOBAL_CX, elementVal); + PyObject *rightItem; + + bool needToDecRefRightItem; + if (PyObject_TypeCheck(other, &JSArrayProxyType)) { + JS_GetElement(GLOBAL_CX, *(((JSArrayProxy *)other)->jsArray), index, &elementVal); + rightItem = pyTypeFactory(GLOBAL_CX, elementVal); + needToDecRefRightItem = true; + } else { + rightItem = ((PyListObject *)other)->ob_item[index]; + needToDecRefRightItem = false; + } + + if (leftItem == rightItem) { + continue; + } + + Py_INCREF(leftItem); + Py_INCREF(rightItem); + int k = PyObject_RichCompareBool(leftItem, rightItem, Py_EQ); + Py_DECREF(leftItem); + Py_DECREF(rightItem); + if (k < 0) { + return NULL; + } + if (!k) { + break; + } + + Py_DECREF(leftItem); + if (needToDecRefRightItem) { + Py_DECREF(rightItem); + } + } + + if (index >= selfLength || index >= otherLength) { + /* No more items to compare -- compare sizes */ + Py_RETURN_RICHCOMPARE(selfLength, otherLength, op); + } + + /* We have an item that differs -- shortcuts for EQ/NE */ + if (op == Py_EQ) { + Py_RETURN_FALSE; + } + else if (op == Py_NE) { + Py_RETURN_TRUE; + } + + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + /* Compare the final item again using the proper operator */ + PyObject *pyElementVal = pyTypeFactory(GLOBAL_CX, elementVal); + PyObject *result = PyObject_RichCompare(pyElementVal, ((PyListObject *)other)->ob_item[index], op); + Py_DECREF(pyElementVal); + return result; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_repr(JSArrayProxy *self) { + Py_ssize_t selfLength = JSArrayProxy_length(self); + + if (selfLength == 0) { + return PyUnicode_FromString("[]"); + } + + Py_ssize_t i = Py_ReprEnter((PyObject *)self); + if (i != 0) { + return i > 0 ? PyUnicode_FromString("[...]") : NULL; + } + + _PyUnicodeWriter writer; + + _PyUnicodeWriter_Init(&writer); + writer.overallocate = 1; + /* "[" + "1" + ", 2" * (len - 1) + "]" */ + writer.min_length = 1 + 1 + (2 + 1) * (selfLength - 1) + 1; + + JS::RootedValue elementVal(GLOBAL_CX); + + if (_PyUnicodeWriter_WriteChar(&writer, '[') < 0) { + goto error; + } + + /* Do repr() on each element. Note that this may mutate the list, so must refetch the list size on each iteration. */ + for (Py_ssize_t index = 0; index < JSArrayProxy_length(self); index++) { + if (index > 0) { + if (_PyUnicodeWriter_WriteASCIIString(&writer, ", ", 2) < 0) { + goto error; + } + } + + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + + PyObject *s; + if (&elementVal.toObject() == (*(self->jsArray)).get()) { + s = PyObject_Repr((PyObject *)self); + } else { + PyObject *pyElementVal = pyTypeFactory(GLOBAL_CX, elementVal); + s = PyObject_Repr(pyElementVal); + Py_DECREF(pyElementVal); + } + if (s == NULL) { + goto error; + } + + if (_PyUnicodeWriter_WriteStr(&writer, s) < 0) { + Py_DECREF(s); + goto error; + } + Py_DECREF(s); + } + + writer.overallocate = 0; + if (_PyUnicodeWriter_WriteChar(&writer, ']') < 0) { + goto error; + } + + Py_ReprLeave((PyObject *)self); + return _PyUnicodeWriter_Finish(&writer); + +error: + _PyUnicodeWriter_Dealloc(&writer); + Py_ReprLeave((PyObject *)self); + return NULL; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_iter(JSArrayProxy *self) { + JSArrayIterProxy *iterator = PyObject_GC_New(JSArrayIterProxy, &JSArrayIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = false; + iterator->it.it_index = 0; + Py_INCREF(self); + iterator->it.it_seq = (PyListObject *)self; + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_iter_reverse(JSArrayProxy *self) { + JSArrayIterProxy *iterator = PyObject_GC_New(JSArrayIterProxy, &JSArrayIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = true; + iterator->it.it_index = JSArrayProxyMethodDefinitions::JSArrayProxy_length(self) - 1; + Py_INCREF(self); + iterator->it.it_seq = (PyListObject *)self; + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_concat(JSArrayProxy *self, PyObject *value) { + // value must be a list + if (!PyList_Check(value)) { + PyErr_Format(PyExc_TypeError, "can only concatenate list (not \"%.200s\") to list", Py_TYPE(value)->tp_name); + return NULL; + } + + Py_ssize_t sizeSelf = JSArrayProxy_length(self); + Py_ssize_t sizeValue; + if (PyObject_TypeCheck(value, &JSArrayProxyType)) { + sizeValue = JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)value); + } else { + sizeValue = Py_SIZE(value); + } + + assert((size_t)sizeSelf + (size_t)sizeValue < PY_SSIZE_T_MAX); + + if (sizeValue == 0) { + if (sizeSelf == 0) { + return PyList_New(0); + } + else { + Py_INCREF(self); + return (PyObject *)self; + } + } + + JS::RootedObject jCombinedArray(GLOBAL_CX, JS::NewArrayObject(GLOBAL_CX, (size_t)sizeSelf + (size_t)sizeValue)); + + JS::RootedValue elementVal(GLOBAL_CX); + + for (Py_ssize_t inputIdx = 0; inputIdx < sizeSelf; inputIdx++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), inputIdx, &elementVal); + JS_SetElement(GLOBAL_CX, jCombinedArray, inputIdx, elementVal); + } + + if (PyObject_TypeCheck(value, &JSArrayProxyType)) { + for (Py_ssize_t inputIdx = 0; inputIdx < sizeValue; inputIdx++) { + JS_GetElement(GLOBAL_CX, *(((JSArrayProxy *)value)->jsArray), inputIdx, &elementVal); + JS_SetElement(GLOBAL_CX, jCombinedArray, sizeSelf + inputIdx, elementVal); + } + } else { + for (Py_ssize_t inputIdx = 0; inputIdx < sizeValue; inputIdx++) { + PyObject *item = PyList_GetItem(value, inputIdx); + elementVal.set(jsTypeFactory(GLOBAL_CX, item)); + JS_SetElement(GLOBAL_CX, jCombinedArray, sizeSelf + inputIdx, elementVal); + } + } + + JS::RootedValue jCombinedArrayValue(GLOBAL_CX); + jCombinedArrayValue.setObjectOrNull(jCombinedArray); + + return pyTypeFactory(GLOBAL_CX, jCombinedArrayValue); +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_repeat(JSArrayProxy *self, Py_ssize_t n) { + const Py_ssize_t input_size = JSArrayProxy_length(self); + if (input_size == 0 || n <= 0) { + return PyList_New(0); + } + + if (input_size > PY_SSIZE_T_MAX / n) { + return PyErr_NoMemory(); + } + + JS::RootedObject jCombinedArray(GLOBAL_CX, JS::NewArrayObject(GLOBAL_CX, input_size * n)); + // repeat within new array + // one might think of using copyWithin but in SpiderMonkey it's implemented in JS! + JS::RootedValue elementVal(GLOBAL_CX); + for (Py_ssize_t inputIdx = 0; inputIdx < input_size; inputIdx++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), inputIdx, &elementVal); + for (Py_ssize_t repeatIdx = 0; repeatIdx < n; repeatIdx++) { + JS_SetElement(GLOBAL_CX, jCombinedArray, repeatIdx * input_size + inputIdx, elementVal); + } + } + + JS::RootedValue jCombinedArrayValue(GLOBAL_CX); + jCombinedArrayValue.setObjectOrNull(jCombinedArray); + + return pyTypeFactory(GLOBAL_CX, jCombinedArrayValue); +} + +int JSArrayProxyMethodDefinitions::JSArrayProxy_contains(JSArrayProxy *self, PyObject *element) { + Py_ssize_t index; + int cmp; + + Py_ssize_t numElements = JSArrayProxy_length(self); + + JS::RootedValue elementVal(GLOBAL_CX); + for (index = 0, cmp = 0; cmp == 0 && index < numElements; ++index) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + PyObject *item = pyTypeFactory(GLOBAL_CX, elementVal); + Py_INCREF(item); + cmp = PyObject_RichCompareBool(item, element, Py_EQ); + Py_DECREF(item); + Py_DECREF(item); + } + return cmp; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_inplace_concat(JSArrayProxy *self, PyObject *value) { + Py_ssize_t selfLength = JSArrayProxy_length(self); + Py_ssize_t valueLength = Py_SIZE(value); + + // allocate extra spacePy_SIZE + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), selfLength + valueLength); + + JS::RootedValue jValue(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, value)); + JS::RootedObject jRootedValue = JS::RootedObject(GLOBAL_CX, jValue.toObjectOrNull()); + + JS::RootedValue elementVal(GLOBAL_CX); + for (Py_ssize_t inputIdx = 0; inputIdx < valueLength; inputIdx++) { + JS_GetElement(GLOBAL_CX, jRootedValue, inputIdx, &elementVal); + JS_SetElement(GLOBAL_CX, *(self->jsArray), selfLength + inputIdx, elementVal); + } + + Py_INCREF(self); + return (PyObject *)self; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_inplace_repeat(JSArrayProxy *self, Py_ssize_t n) { + Py_ssize_t input_size = JSArrayProxy_length(self); + if (input_size == 0 || n == 1) { + Py_INCREF(self); + return (PyObject *)self; + } + + if (n < 1) { + JSArrayProxy_clear_method(self); + Py_INCREF(self); + return (PyObject *)self; + } + + if (input_size > PY_SSIZE_T_MAX / n) { + return PyErr_NoMemory(); + } + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), input_size * n); + + // repeat within self + // one might think of using copyWithin but in SpiderMonkey it's implemented in JS! + JS::RootedValue elementVal(GLOBAL_CX); + for (Py_ssize_t inputIdx = 0; inputIdx < input_size; inputIdx++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), inputIdx, &elementVal); + for (Py_ssize_t repeatIdx = 0; repeatIdx < n; repeatIdx++) { + JS_SetElement(GLOBAL_CX, *(self->jsArray), repeatIdx * input_size + inputIdx, elementVal); + } + } + + Py_INCREF(self); + return (PyObject *)self; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_clear_method(JSArrayProxy *self) { + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), 0); + Py_RETURN_NONE; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_copy(JSArrayProxy *self) { + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setInt32(0); + jArgs[1].setInt32(JSArrayProxy_length(self)); + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "slice", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + return pyTypeFactory(GLOBAL_CX, jReturnedArray); +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_append(JSArrayProxy *self, PyObject *value) { + Py_ssize_t len = JSArrayProxy_length(self); + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), len + 1); + JS::RootedValue jValue(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, value)); + JS_SetElement(GLOBAL_CX, *(self->jsArray), len, jValue); + + Py_RETURN_NONE; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_insert(JSArrayProxy *self, PyObject *const *args, Py_ssize_t nargs) { + PyObject *return_value = NULL; + Py_ssize_t index; + PyObject *value; + + if (!_PyArg_CheckPositional("insert", nargs, 2, 2)) { + return NULL; + } + + { + Py_ssize_t ival = -1; + PyObject *iobj = PyNumber_Index(args[0]); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) { + return NULL; + } + index = ival; + } + + value = args[1]; + + Py_ssize_t n = JSArrayProxy_length(self); + + if (index < 0) { + index += n; + if (index < 0) { + index = 0; + } + } + if (index > n) { + index = n; + } + + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setInt32(index); + jArgs[1].setInt32(0); + jArgs[2].set(jsTypeFactory(GLOBAL_CX, value)); + + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "splice", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + Py_RETURN_NONE; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_extend(JSArrayProxy *self, PyObject *iterable) { + if (PyList_CheckExact(iterable) || PyTuple_CheckExact(iterable) || (PyObject *)self == iterable) { + iterable = PySequence_Fast(iterable, "argument must be iterable"); + if (!iterable) { + return NULL; + } + + Py_ssize_t n = PySequence_Fast_GET_SIZE(iterable); + if (n == 0) { + /* short circuit when iterable is empty */ + Py_DECREF(iterable); + Py_RETURN_NONE; + } + + Py_ssize_t m = JSArrayProxy_length(self); + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), m + n); + + // populate the end of self with iterable's items. + PyObject **src = PySequence_Fast_ITEMS(iterable); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *o = src[i]; + JS::RootedValue jValue(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, o)); + JS_SetElement(GLOBAL_CX, *(self->jsArray), m + i, jValue); + } + + Py_DECREF(iterable); + } + else { + PyObject *it = PyObject_GetIter(iterable); + if (it == NULL) { + return NULL; + } + PyObject *(*iternext)(PyObject *) = *Py_TYPE(it)->tp_iternext; + + Py_ssize_t len = JSArrayProxy_length(self); + + for (;; ) { + PyObject *item = iternext(it); + if (item == NULL) { + if (PyErr_Occurred()) { + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + } + else { + Py_DECREF(it); + return NULL; + } + } + break; + } + + JS::SetArrayLength(GLOBAL_CX, *(self->jsArray), len + 1); + JS::RootedValue jValue(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, item)); + JS_SetElement(GLOBAL_CX, *(self->jsArray), len, jValue); + len++; + } + + Py_DECREF(it); + } + Py_RETURN_NONE; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_pop(JSArrayProxy *self, PyObject *const *args, Py_ssize_t nargs) { + Py_ssize_t index = -1; + + if (!_PyArg_CheckPositional("pop", nargs, 0, 1)) { + return NULL; + } + + if (nargs >= 1) { + Py_ssize_t ival = -1; + PyObject *iobj = PyNumber_Index(args[0]); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) { + return NULL; + } + index = ival; + } + + Py_ssize_t selfSize = JSArrayProxy_length(self); + + if (selfSize == 0) { + /* Special-case most common failure cause */ + PyErr_SetString(PyExc_IndexError, "pop from empty list"); + return NULL; + } + + if (index < 0) { + index += selfSize; + } + + if ((size_t)index >= (size_t)selfSize) { + PyErr_SetString(PyExc_IndexError, "pop index out of range"); + return NULL; + } + + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setInt32(index); + jArgs[1].setInt32(1); + + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "splice", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + // need the value in the returned array, not the array itself + JS::RootedObject rootedReturnedArray(GLOBAL_CX, jReturnedArray.toObjectOrNull()); + JS::RootedValue elementVal(GLOBAL_CX); + JS_GetElement(GLOBAL_CX, rootedReturnedArray, 0, &elementVal); + + return pyTypeFactory(GLOBAL_CX, elementVal); +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_remove(JSArrayProxy *self, PyObject *value) { + Py_ssize_t selfSize = JSArrayProxy_length(self); + + JS::RootedValue elementVal(GLOBAL_CX); + for (Py_ssize_t index = 0; index < selfSize; index++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + PyObject *obj = pyTypeFactory(GLOBAL_CX, elementVal); + Py_INCREF(obj); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + Py_DECREF(obj); + Py_DECREF(obj); + if (cmp > 0) { + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setInt32(index); + jArgs[1].setInt32(1); + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "splice", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + Py_RETURN_NONE; + } + else if (cmp < 0) { + return NULL; + } + } + + PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); + return NULL; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_index(JSArrayProxy *self, PyObject *const *args, Py_ssize_t nargs) { + PyObject *value; + Py_ssize_t start = 0; + Py_ssize_t stop = PY_SSIZE_T_MAX; + + if (!_PyArg_CheckPositional("index", nargs, 1, 3)) { + return NULL; + } + value = args[0]; + if (nargs < 2) { + goto skip_optional; + } + if (!_PyEval_SliceIndexNotNone(args[1], &start)) { + return NULL; + } + if (nargs < 3) { + goto skip_optional; + } + if (!_PyEval_SliceIndexNotNone(args[2], &stop)) { + return NULL; + } + +skip_optional: + Py_ssize_t selfSize = JSArrayProxy_length(self); + + if (start < 0) { + start += selfSize; + if (start < 0) { + start = 0; + } + } + if (stop < 0) { + stop += selfSize; + if (stop < 0) { + stop = 0; + } + } + + JS::RootedValue elementVal(GLOBAL_CX); + for (Py_ssize_t index = start; index < stop && index < selfSize; index++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + PyObject *obj = pyTypeFactory(GLOBAL_CX, elementVal); + Py_INCREF(obj); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + Py_DECREF(obj); + Py_DECREF(obj); + if (cmp > 0) { + return PyLong_FromSsize_t(index); + } + else if (cmp < 0) { + return NULL; + } + } + + PyErr_Format(PyExc_ValueError, "%R is not in list", value); + return NULL; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_count(JSArrayProxy *self, PyObject *value) { + Py_ssize_t count = 0; + + Py_ssize_t length = JSArrayProxy_length(self); + JS::RootedValue elementVal(GLOBAL_CX); + for (Py_ssize_t index = 0; index < length; index++) { + JS_GetElement(GLOBAL_CX, *(self->jsArray), index, &elementVal); + PyObject *obj = pyTypeFactory(GLOBAL_CX, elementVal); + Py_INCREF(obj); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + Py_DECREF(obj); + Py_DECREF(obj); + if (cmp > 0) { + count++; + } + else if (cmp < 0) { + return NULL; + } + } + return PyLong_FromSsize_t(count); +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_reverse(JSArrayProxy *self) { + if (JSArrayProxy_length(self) > 1) { + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "reverse", JS::HandleValueArray::empty(), &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + } + + Py_RETURN_NONE; +} + +// private +static bool sort_compare_key_func(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject callee(cx, &args.callee()); + + JS::RootedValue keyFunc(cx); + if (!JS_GetProperty(cx, callee, "_key_func_param", &keyFunc)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return false; + } + PyObject *keyfunc = (PyObject *)keyFunc.toPrivate(); + + JS::RootedValue reverseValue(cx); + if (!JS_GetProperty(cx, callee, "_reverse_param", &reverseValue)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return false; + } + bool reverse = reverseValue.toBoolean(); + + JS::RootedValue elementVal0(cx, args[0]); + PyObject *args_0 = pyTypeFactory(cx, elementVal0); + PyObject *args_0_result = PyObject_CallFunction(keyfunc, "O", args_0); + Py_DECREF(args_0); + if (!args_0_result) { + return false; + } + + JS::RootedValue elementVal1(cx, args[1]); + PyObject *args_1 = pyTypeFactory(cx, elementVal1); + PyObject *args_1_result = PyObject_CallFunction(keyfunc, "O", args_1); + Py_DECREF(args_1); + if (!args_1_result) { + return false; + } + + int cmp = PyObject_RichCompareBool(args_0_result, args_1_result, Py_LT); + if (cmp > 0) { + args.rval().setInt32(reverse ? 1 : -1); + } else if (cmp == 0) { + cmp = PyObject_RichCompareBool(args_0_result, args_1_result, Py_EQ); + if (cmp > 0) { + args.rval().setInt32(0); + } + else if (cmp == 0) { + args.rval().setInt32(reverse ? -1 : 1); + } + else { + return false; + } + } + else { + return false; + } + + return true; +} + +// private +static bool sort_compare_default(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject callee(cx, &args.callee()); + JS::RootedValue reverseValue(cx); + if (!JS_GetProperty(cx, callee, "_reverse_param", &reverseValue)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return false; + } + bool reverse = reverseValue.toBoolean(); + + JS::RootedValue elementVal0(cx, args[0]); + PyObject *args_0 = pyTypeFactory(cx, elementVal0); + + JS::RootedValue elementVal1(cx, args[1]); + PyObject *args_1 = pyTypeFactory(cx, elementVal1); + + int cmp = PyObject_RichCompareBool(args_0, args_1, Py_LT); + if (cmp > 0) { + args.rval().setInt32(reverse ? 1 : -1); + } + else if (cmp == 0) { + cmp = PyObject_RichCompareBool(args_0, args_1, Py_EQ); + if (cmp > 0) { + args.rval().setInt32(0); + } + else if (cmp == 0) { + args.rval().setInt32(reverse ? -1 : 1); + } + else { + Py_DECREF(args_0); + Py_DECREF(args_1); + return false; + } + } + else { + Py_DECREF(args_0); + Py_DECREF(args_1); + return false; + } + + Py_DECREF(args_0); + Py_DECREF(args_1); + return true; +} + +PyObject *JSArrayProxyMethodDefinitions::JSArrayProxy_sort(JSArrayProxy *self, PyObject *args, PyObject *kwargs) { + static const char *const _keywords[] = {"key", "reverse", NULL}; + + PyObject *keyfunc = Py_None; + int reverse = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|$Op:sort", (char **)_keywords, &keyfunc, &reverse)) { + return NULL; + } + + if (JSArrayProxy_length(self) > 1) { + JS::RootedValue jReturnedArray(GLOBAL_CX); + if (keyfunc != Py_None) { + if (PyFunction_Check(keyfunc)) { + // we got a python key function, check if two-argument js style or standard python 1-arg + PyObject *code = PyFunction_GetCode(keyfunc); + if (((PyCodeObject *)code)->co_argcount == 1) { + // adapt to python style, provide js-style comp wrapper that does it the python way, which is < based with calls to keyFunc + JS::RootedObject funObj(GLOBAL_CX, JS_GetFunctionObject(JS_NewFunction(GLOBAL_CX, sort_compare_key_func, 2, 0, NULL))); + + JS::RootedValue privateValue(GLOBAL_CX, JS::PrivateValue(keyfunc)); + if (!JS_SetProperty(GLOBAL_CX, funObj, "_key_func_param", privateValue)) { // JS::SetReservedSlot(functionObj, KeyFuncSlot, JS::PrivateValue(keyfunc)); does not work + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + JS::RootedValue reverseValue(GLOBAL_CX); + reverseValue.setBoolean(reverse); + if (!JS_SetProperty(GLOBAL_CX, funObj, "_reverse_param", reverseValue)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setObject(*funObj); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "sort", jArgs, &jReturnedArray)) { + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + } + return NULL; + } + + // cleanup + if (!JS_DeleteProperty(GLOBAL_CX, funObj, "_key_func_param")) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + if (!JS_DeleteProperty(GLOBAL_CX, funObj, "_reverse_param")) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + } + else { + // two-arg js-style + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].set(jsTypeFactory(GLOBAL_CX, keyfunc)); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "sort", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + if (reverse) { + JSArrayProxy_reverse(self); + } + } + } + else if (PyObject_TypeCheck(keyfunc, &JSFunctionProxyType)) { + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setObject(**((JSFunctionProxy *)keyfunc)->jsFunc); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "sort", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + if (reverse) { + JSArrayProxy_reverse(self); + } + } + else if (PyCFunction_Check(keyfunc)) { + JS::RootedObject funObj(GLOBAL_CX, JS_GetFunctionObject(JS_NewFunction(GLOBAL_CX, sort_compare_key_func, 2, 0, NULL))); + + JS::RootedValue privateValue(GLOBAL_CX, JS::PrivateValue(keyfunc)); + if (!JS_SetProperty(GLOBAL_CX, funObj, "_key_func_param", privateValue)) { // JS::SetReservedSlot(functionObj, KeyFuncSlot, JS::PrivateValue(keyfunc)); does not work + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + JS::RootedValue reverseValue(GLOBAL_CX); + reverseValue.setBoolean(reverse); + if (!JS_SetProperty(GLOBAL_CX, funObj, "_reverse_param", reverseValue)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setObject(*funObj); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "sort", jArgs, &jReturnedArray)) { + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + } + return NULL; + } + } + else { + PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable", Py_TYPE(keyfunc)->tp_name); + return NULL; + } + } + else { + // adapt to python style, provide js-style comp wrapper that does it the python way, which is < based + JSFunction *cmpFunction = JS_NewFunction(GLOBAL_CX, sort_compare_default, 2, 0, NULL); + JS::RootedObject funObj(GLOBAL_CX, JS_GetFunctionObject(cmpFunction)); + + JS::RootedValue reverseValue(GLOBAL_CX); + reverseValue.setBoolean(reverse); + if (!JS_SetProperty(GLOBAL_CX, funObj, "_reverse_param", reverseValue)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + + JS::Rooted> jArgs(GLOBAL_CX); + jArgs[0].setObject(*funObj); + if (!JS_CallFunctionName(GLOBAL_CX, *(self->jsArray), "sort", jArgs, &jReturnedArray)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSArrayProxyType.tp_name); + return NULL; + } + } + } + Py_RETURN_NONE; +} \ No newline at end of file diff --git a/src/JSFunctionProxy.cc b/src/JSFunctionProxy.cc new file mode 100644 index 00000000..99a32552 --- /dev/null +++ b/src/JSFunctionProxy.cc @@ -0,0 +1,67 @@ +/** + * @file JSFunctionProxy.cc + * @author Caleb Aikens (caleb@distributive.network) + * @brief JSFunctionProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a function would. + * @date 2023-09-28 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#include "include/JSFunctionProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/jsTypeFactory.hh" +#include "include/pyTypeFactory.hh" +#include "include/setSpiderMonkeyException.hh" + +#include + +#include + +void JSFunctionProxyMethodDefinitions::JSFunctionProxy_dealloc(JSFunctionProxy *self) +{ + delete self->jsFunc; +} + +PyObject *JSFunctionProxyMethodDefinitions::JSFunctionProxy_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) { + JSFunctionProxy *self = (JSFunctionProxy *)subtype->tp_alloc(subtype, 0); + if (self) { + self->jsFunc = new JS::PersistentRootedObject(GLOBAL_CX); + } + return (PyObject *)self; +} + +PyObject *JSFunctionProxyMethodDefinitions::JSFunctionProxy_call(PyObject *self, PyObject *args, PyObject *kwargs) { + JSContext *cx = GLOBAL_CX; + JS::RootedValue jsFunc(GLOBAL_CX, JS::ObjectValue(**((JSFunctionProxy *)self)->jsFunc)); + JSObject *jsFuncObj = jsFunc.toObjectOrNull(); + JS::RootedObject thisObj(GLOBAL_CX, JS::CurrentGlobalOrNull(GLOBAL_CX)); // if jsFunc is not bound, assume `this` is `globalThis` + + JS::RootedVector jsArgsVector(cx); + Py_ssize_t nargs = PyTuple_Size(args); + for (size_t i = 0; i < nargs; i++) { + JS::Value jsValue = jsTypeFactory(cx, PyTuple_GetItem(args, i)); + if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control + return NULL; // Fail-fast + } + if (!jsArgsVector.append(jsValue)) { + // out of memory + setSpiderMonkeyException(cx); + return NULL; + } + } + + JS::HandleValueArray jsArgs(jsArgsVector); + JS::RootedValue jsReturnVal(cx); + if (!JS_CallFunctionValue(cx, thisObj, jsFunc, jsArgs, &jsReturnVal)) { + setSpiderMonkeyException(cx); + return NULL; + } + + if (PyErr_Occurred()) { + return NULL; + } + + return pyTypeFactory(cx, jsReturnVal); +} \ No newline at end of file diff --git a/src/JSMethodProxy.cc b/src/JSMethodProxy.cc new file mode 100644 index 00000000..78e1189b --- /dev/null +++ b/src/JSMethodProxy.cc @@ -0,0 +1,78 @@ +/** + * @file JSMethodProxy.cc + * @author Caleb Aikens (caleb@distributive.network) + * @brief JSMethodProxy is a custom C-implemented python type. It acts as a proxy for JSFunctions from Spidermonkey, and behaves like a method would, treating `self` as `this`. + * @date 2023-11-14 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#include "include/JSMethodProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/jsTypeFactory.hh" +#include "include/pyTypeFactory.hh" +#include "include/setSpiderMonkeyException.hh" + +#include + +#include + +void JSMethodProxyMethodDefinitions::JSMethodProxy_dealloc(JSMethodProxy *self) +{ + delete self->jsFunc; + return; +} + +PyObject *JSMethodProxyMethodDefinitions::JSMethodProxy_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) { + JSFunctionProxy *jsFunctionProxy; + PyObject *im_self; + + if (!PyArg_ParseTuple(args, "O!O", &JSFunctionProxyType, &jsFunctionProxy, &im_self)) { + return NULL; + } + + JSMethodProxy *self = (JSMethodProxy *)subtype->tp_alloc(subtype, 0); + if (self) { + self->self = im_self; + self->jsFunc = new JS::PersistentRootedObject(GLOBAL_CX); + self->jsFunc->set(*(jsFunctionProxy->jsFunc)); + } + + return (PyObject *)self; +} + +PyObject *JSMethodProxyMethodDefinitions::JSMethodProxy_call(PyObject *self, PyObject *args, PyObject *kwargs) { + JSContext *cx = GLOBAL_CX; + JS::RootedValue jsFunc(GLOBAL_CX, JS::ObjectValue(**((JSMethodProxy *)self)->jsFunc)); + JS::RootedValue selfValue(cx, jsTypeFactory(cx, ((JSMethodProxy *)self)->self)); + JS::RootedObject selfObject(cx); + JS_ValueToObject(cx, selfValue, &selfObject); + + JS::RootedVector jsArgsVector(cx); + for (size_t i = 0; i < PyTuple_Size(args); i++) { + JS::Value jsValue = jsTypeFactory(cx, PyTuple_GetItem(args, i)); + if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control + return NULL; // Fail-fast + } + if (!jsArgsVector.append(jsValue)) { + // out of memory + setSpiderMonkeyException(cx); + return NULL; + } + } + + JS::HandleValueArray jsArgs(jsArgsVector); + JS::RootedValue jsReturnVal(cx); + if (!JS_CallFunctionValue(cx, selfObject, jsFunc, jsArgs, &jsReturnVal)) { + setSpiderMonkeyException(cx); + return NULL; + } + + if (PyErr_Occurred()) { + return NULL; + } + + return pyTypeFactory(cx, jsReturnVal); +} \ No newline at end of file diff --git a/src/JSObjectItemsProxy.cc b/src/JSObjectItemsProxy.cc new file mode 100644 index 00000000..2aad8e16 --- /dev/null +++ b/src/JSObjectItemsProxy.cc @@ -0,0 +1,115 @@ +/** + * @file JSObjectItemsProxy.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectItemsProxy is a custom C-implemented python type that derives from dict keys + * @date 2024-01-19 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#include "include/JSObjectItemsProxy.hh" + +#include "include/JSObjectIterProxy.hh" +#include "include/JSObjectProxy.hh" +#include "include/JSArrayProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/jsTypeFactory.hh" +#include "include/PyBaseProxyHandler.hh" + +#include +#include + +#include + + + +void JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_dealloc(JSObjectItemsProxy *self) +{ + PyObject_GC_UnTrack(self); + Py_XDECREF(self->dv.dv_dict); + PyObject_GC_Del(self); +} + +Py_ssize_t JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_length(JSObjectItemsProxy *self) +{ + if (self->dv.dv_dict == NULL) { + return 0; + } + return JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->dv.dv_dict); +} + +int JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_traverse(JSObjectItemsProxy *self, visitproc visit, void *arg) { + Py_VISIT(self->dv.dv_dict); + return 0; +} + +int JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_clear(JSObjectItemsProxy *self) { + Py_CLEAR(self->dv.dv_dict); + return 0; +} + +PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_iter(JSObjectItemsProxy *self) { + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = false; + iterator->it.it_index = 0; + iterator->it.kind = KIND_ITEMS; + Py_INCREF(self->dv.dv_dict); + iterator->it.di_dict = self->dv.dv_dict; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { + return NULL; + } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_iter_reverse(JSObjectItemsProxy *self) { + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = true; + iterator->it.it_index = JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_length(self) - 1; + iterator->it.kind = KIND_ITEMS; + Py_INCREF(self->dv.dv_dict); + iterator->it.di_dict = self->dv.dv_dict; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { + return NULL; + } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_repr(JSObjectItemsProxy *self) { + PyObject *seq; + PyObject *result = NULL; + + Py_ssize_t rc = Py_ReprEnter((PyObject *)self); + if (rc != 0) { + return rc > 0 ? PyUnicode_FromString("...") : NULL; + } + + seq = PySequence_List((PyObject *)self); + if (seq == NULL) { + goto Done; + } + + result = PyUnicode_FromFormat("%s(%R)", PyDictItems_Type.tp_name, seq); + Py_DECREF(seq); + +Done: + Py_ReprLeave((PyObject *)self); + return result; +} + +PyObject *JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)) { + return PyDictProxy_New((PyObject *)((_PyDictViewObject *)self)->dv_dict); +} \ No newline at end of file diff --git a/src/JSObjectIterProxy.cc b/src/JSObjectIterProxy.cc new file mode 100644 index 00000000..0163e713 --- /dev/null +++ b/src/JSObjectIterProxy.cc @@ -0,0 +1,134 @@ +/** + * @file JSObjectIterProxy.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectIterProxy is a custom C-implemented python type that derives from list iterator + * @date 2024-01-17 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + + +#include "include/JSObjectIterProxy.hh" + +#include "include/JSObjectProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" + +#include "include/pyTypeFactory.hh" + +#include "include/PyDictProxyHandler.hh" + +#include + +#include + +#include + + +void JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_dealloc(JSObjectIterProxy *self) +{ + delete self->it.props; + PyObject_GC_UnTrack(self); + Py_XDECREF(self->it.di_dict); + PyObject_GC_Del(self); +} + +int JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_traverse(JSObjectIterProxy *self, visitproc visit, void *arg) { + Py_VISIT(self->it.di_dict); + return 0; +} + +int JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_clear(JSObjectIterProxy *self) { + Py_CLEAR(self->it.di_dict); + return 0; +} + +PyObject *JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_iter(JSObjectIterProxy *self) { + Py_INCREF(&self->it); + return (PyObject *)&self->it; +} + +PyObject *JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_nextkey(JSObjectIterProxy *self) { + PyDictObject *dict = self->it.di_dict; + if (dict == NULL) { + return NULL; + } + + if (self->it.reversed) { + if (self->it.it_index >= 0) { + JS::HandleId id = (*(self->it.props))[(self->it.it_index)--]; + PyObject *key = idToKey(GLOBAL_CX, id); + PyObject *value; + + if (self->it.kind != KIND_KEYS) { + JS::RootedValue jsVal(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(((JSObjectProxy *)(self->it.di_dict))->jsObject), id, &jsVal); + value = pyTypeFactory(GLOBAL_CX, jsVal); + } + + PyObject *ret; + if (self->it.kind == KIND_ITEMS) { + ret = PyTuple_Pack(2, key, value); + } + else if (self->it.kind == KIND_VALUES) { + ret = value; + } + else { + ret = key; + } + + Py_INCREF(ret); + if (self->it.kind != KIND_KEYS) { + Py_DECREF(value); + } + + return ret; + } + } else { + if (self->it.it_index < JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)dict)) { + JS::HandleId id = (*(self->it.props))[(self->it.it_index)++]; + PyObject *key = idToKey(GLOBAL_CX, id); + PyObject *value; + + if (self->it.kind != KIND_KEYS) { + JS::RootedValue jsVal(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(((JSObjectProxy *)(self->it.di_dict))->jsObject), id, &jsVal); + value = pyTypeFactory(GLOBAL_CX, jsVal); + } + + PyObject *ret; + if (self->it.kind == KIND_ITEMS) { + ret = PyTuple_Pack(2, key, value); + } + else if (self->it.kind == KIND_VALUES) { + ret = value; + } + else { + ret = key; + } + + Py_INCREF(ret); + if (self->it.kind != KIND_KEYS) { + Py_DECREF(value); + } + + return ret; + } + } + + self->it.di_dict = NULL; + Py_DECREF(dict); + return NULL; +} + +PyObject *JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_len(JSObjectIterProxy *self) { + Py_ssize_t len; + if (self->it.di_dict) { + len = JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->it.di_dict) - self->it.it_index; + if (len >= 0) { + return PyLong_FromSsize_t(len); + } + } + return PyLong_FromLong(0); +} \ No newline at end of file diff --git a/src/JSObjectKeysProxy.cc b/src/JSObjectKeysProxy.cc new file mode 100644 index 00000000..4c4e54ce --- /dev/null +++ b/src/JSObjectKeysProxy.cc @@ -0,0 +1,384 @@ +/** + * @file JSObjectKeysProxy.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectKeysProxy is a custom C-implemented python type that derives from dict keys + * @date 2024-01-18 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#include "include/JSObjectKeysProxy.hh" + +#include "include/JSObjectIterProxy.hh" +#include "include/JSObjectProxy.hh" +#include "include/JSArrayProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/jsTypeFactory.hh" +#include "include/PyDictProxyHandler.hh" + +#include +#include + +#include +#include "include/pyshim.hh" + +void JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_dealloc(JSObjectKeysProxy *self) +{ + PyObject_GC_UnTrack(self); + Py_XDECREF(self->dv.dv_dict); + PyObject_GC_Del(self); +} + +Py_ssize_t JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_length(JSObjectKeysProxy *self) +{ + if (self->dv.dv_dict == NULL) { + return 0; + } + return JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->dv.dv_dict); +} + +int JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_contains(JSObjectKeysProxy *self, PyObject *key) +{ + if (self->dv.dv_dict == NULL) { + return 0; + } + return JSObjectProxyMethodDefinitions::JSObjectProxy_contains((JSObjectProxy *)self->dv.dv_dict, key); +} + +int JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_traverse(JSObjectKeysProxy *self, visitproc visit, void *arg) { + Py_VISIT(self->dv.dv_dict); + return 0; +} + +int JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_clear(JSObjectKeysProxy *self) { + Py_CLEAR(self->dv.dv_dict); + return 0; +} + +// private +static int all_contained_in(PyObject *self, PyObject *other) { + PyObject *iter = PyObject_GetIter(self); + int ok = 1; + + if (iter == NULL) { + return -1; + } + + for (;; ) { + PyObject *next = PyIter_Next(iter); + if (next == NULL) { + if (PyErr_Occurred()) + ok = -1; + break; + } + if (PyObject_TypeCheck(other, &JSObjectKeysProxyType)) { + JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_contains((JSObjectKeysProxy *)other, next); + } + else { + ok = PySequence_Contains(other, next); + } + Py_DECREF(next); + if (ok <= 0) + break; + } + + Py_DECREF(iter); + return ok; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_richcompare(JSObjectKeysProxy *self, PyObject *other, int op) { + Py_ssize_t len_self, len_other; + int ok; + PyObject *result; + + if (!PyAnySet_Check(other) && !PyDictViewSet_Check(other)) { + Py_RETURN_NOTIMPLEMENTED; + } + + len_self = JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->dv.dv_dict); + if (len_self < 0) { + return NULL; + } + + if (PyObject_TypeCheck(other, &JSObjectKeysProxyType)) { + len_other = JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->dv.dv_dict); + } + else { + len_other = PyObject_Size(other); + } + if (len_other < 0) { + return NULL; + } + + ok = 0; + switch (op) { + case Py_NE: + case Py_EQ: + if (len_self == len_other) { + + ok = all_contained_in((PyObject *)self, other); + } + if (op == Py_NE && ok >= 0) { + ok = !ok; + } + break; + + case Py_LT: + if (len_self < len_other) { + ok = all_contained_in((PyObject *)self, other); + } + break; + + case Py_LE: + if (len_self <= len_other) { + ok = all_contained_in((PyObject *)self, other); + } + break; + + case Py_GT: + if (len_self > len_other) { + ok = all_contained_in(other, (PyObject *)self); + } + break; + + case Py_GE: + if (len_self >= len_other) { + ok = all_contained_in(other, (PyObject *)self); + } + break; + } + + if (ok < 0) { + return NULL; + } + + result = ok ? Py_True : Py_False; + + Py_INCREF(result); + return result; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_iter(JSObjectKeysProxy *self) { + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = false; + iterator->it.it_index = 0; + iterator->it.kind = KIND_KEYS; + Py_INCREF(self->dv.dv_dict); + iterator->it.di_dict = self->dv.dv_dict; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { + return NULL; + } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_iter_reverse(JSObjectKeysProxy *self) { + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = true; + iterator->it.it_index = JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_length(self) - 1; + iterator->it.kind = KIND_KEYS; + Py_INCREF(self->dv.dv_dict); + iterator->it.di_dict = self->dv.dv_dict; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { + return NULL; + } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_repr(JSObjectKeysProxy *self) { + PyObject *seq; + PyObject *result = NULL; + + Py_ssize_t rc = Py_ReprEnter((PyObject *)self); + if (rc != 0) { + return rc > 0 ? PyUnicode_FromString("...") : NULL; + } + + seq = PySequence_List((PyObject *)self); + if (seq == NULL) { + goto Done; + } + + result = PyUnicode_FromFormat("%s(%R)", PyDictKeys_Type.tp_name, seq); + Py_DECREF(seq); + +Done: + Py_ReprLeave((PyObject *)self); + return result; +} + +// private +static Py_ssize_t dictview_len(_PyDictViewObject *dv) { + Py_ssize_t len = 0; + if (dv->dv_dict != NULL) { + len = dv->dv_dict->ma_used; + } + return len; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_intersect(JSObjectKeysProxy *self, PyObject *other) { + PyObject *result; + PyObject *it; + PyObject *key; + Py_ssize_t len_self; + int rv; + + // Python interpreter swaps parameters when dict view is on right side of & + if (!PyDictViewSet_Check(self)) { + PyObject *tmp = other; + other = (PyObject *)self; + self = (JSObjectKeysProxy *)tmp; + } + + if (PyObject_TypeCheck(self, &JSObjectKeysProxyType)) { + len_self = JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_length(self); + } + else { + len_self = dictview_len((_PyDictViewObject *)self); + } + + // if other is a set and self is smaller than other, reuse set intersection logic + if (PySet_Check(other) && len_self <= PyObject_Size(other)) { + return PyObject_CallMethod(other, "intersection", "O", self); + } + + // if other is another dict view, and it is bigger than self, swap them + if (PyDictViewSet_Check(other)) { + Py_ssize_t len_other = dictview_len((_PyDictViewObject *)other); + if (len_other > len_self) { + PyObject *tmp = other; + other = (PyObject *)self; + self = (JSObjectKeysProxy *)tmp; + } + } + + /* at this point, two things should be true + 1. self is a dictview + 2. if other is a dictview then it is smaller than self */ + result = PySet_New(NULL); + if (result == NULL) { + return NULL; + } + + it = PyObject_GetIter(other); + if (it == NULL) { + Py_DECREF(result); + return NULL; + } + + while ((key = PyIter_Next(it)) != NULL) { + if (PyObject_TypeCheck(self, &JSObjectKeysProxyType)) { + rv = JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_contains(self, key); + } + else { + if (((_PyDictViewObject *)self)->dv_dict == NULL) { + rv = 0; + } else { + rv = PyDict_Contains((PyObject *)((_PyDictViewObject *)self)->dv_dict, key); + } + } + if (rv < 0) { + goto error; + } + if (rv) { + if (PySet_Add(result, key)) { + goto error; + } + } + Py_DECREF(key); + } + + Py_DECREF(it); + if (PyErr_Occurred()) { + Py_DECREF(result); + return NULL; + } + return result; + +error: + Py_DECREF(it); + Py_DECREF(result); + Py_DECREF(key); + return NULL; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_isDisjoint(JSObjectKeysProxy *self, PyObject *other) { + PyObject *it; + PyObject *item = NULL; + + Py_ssize_t selfLen = JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_length(self); + + if ((PyObject *)self == other) { + if (selfLen == 0) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } + } + + /* Iterate over the shorter object (only if other is a set, + * because PySequence_Contains may be expensive otherwise): */ + if (PyAnySet_Check(other) || PyDictViewSet_Check(other)) { + Py_ssize_t len_self = selfLen; + Py_ssize_t len_other = PyObject_Size(other); + if (len_other == -1) { + return NULL; + } + + if ((len_other > len_self)) { + PyObject *tmp = other; + other = (PyObject *)self; + self = (JSObjectKeysProxy *)tmp; + } + } + + it = PyObject_GetIter(other); + if (it == NULL) { + return NULL; + } + + while ((item = PyIter_Next(it)) != NULL) { + int contains; + if (PyObject_TypeCheck(self, &JSObjectKeysProxyType)) { + contains = JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_contains(self, item); + } + else { + contains = PySequence_Contains((PyObject *)self, item); + } + Py_DECREF(item); + if (contains == -1) { + Py_DECREF(it); + return NULL; + } + + if (contains) { + Py_DECREF(it); + Py_RETURN_FALSE; + } + } + + Py_DECREF(it); + if (PyErr_Occurred()) { + return NULL; /* PyIter_Next raised an exception. */ + } + + Py_RETURN_TRUE; +} + +PyObject *JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)) { + return PyDictProxy_New((PyObject *)((_PyDictViewObject *)self)->dv_dict); +} \ No newline at end of file diff --git a/src/JSObjectProxy.cc b/src/JSObjectProxy.cc index 00392ba7..e983335b 100644 --- a/src/JSObjectProxy.cc +++ b/src/JSObjectProxy.cc @@ -1,37 +1,48 @@ /** * @file JSObjectProxy.cc - * @author Caleb Aikens (caleb@distributive.network) & Tom Tang (xmader@distributive.network) + * @author Caleb Aikens (caleb@distributive.network), Tom Tang (xmader@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief JSObjectProxy is a custom C-implemented python type that derives from dict. It acts as a proxy for JSObjects from Spidermonkey, and behaves like a dict would. - * @version 0.1 * @date 2023-06-26 * - * Copyright (c) 2023 Distributive Corp. + * @copyright Copyright (c) 2023-2024 Distributive Corp. * */ #include "include/JSObjectProxy.hh" +#include "include/JSObjectIterProxy.hh" + +#include "include/JSObjectKeysProxy.hh" +#include "include/JSObjectValuesProxy.hh" +#include "include/JSObjectItemsProxy.hh" + #include "include/modules/pythonmonkey/pythonmonkey.hh" #include "include/jsTypeFactory.hh" #include "include/pyTypeFactory.hh" -#include "include/PyProxyHandler.hh" +#include "include/PyBaseProxyHandler.hh" + +#include "include/JSFunctionProxy.hh" #include #include #include +#include "include/pyshim.hh" + +#include JSContext *GLOBAL_CX; /**< pointer to PythonMonkey's JSContext */ bool keyToId(PyObject *key, JS::MutableHandleId idp) { if (PyUnicode_Check(key)) { // key is str type JS::RootedString idString(GLOBAL_CX); - const char *keyStr = PyUnicode_AsUTF8(key); - JS::ConstUTF8CharsZ utf8Chars(keyStr, strlen(keyStr)); - idString.set(JS_NewStringCopyUTF8Z(GLOBAL_CX, utf8Chars)); + Py_ssize_t length; + const char *keyStr = PyUnicode_AsUTF8AndSize(key, &length); + JS::UTF8Chars utf8Chars(keyStr, length); + idString.set(JS_NewStringCopyUTF8N(GLOBAL_CX, utf8Chars)); return JS_StringToId(GLOBAL_CX, idString, idp); } else if (PyLong_Check(key)) { // key is int type - uint32_t keyAsInt = PyLong_AsUnsignedLong(key); // raise OverflowError if the value of pylong is out of range for a unsigned long + uint32_t keyAsInt = PyLong_AsUnsignedLong(key); // TODO raise OverflowError if the value of pylong is out of range for a unsigned long return JS_IndexToId(GLOBAL_CX, keyAsInt, idp); } else { return false; // fail @@ -40,75 +51,152 @@ bool keyToId(PyObject *key, JS::MutableHandleId idp) { void JSObjectProxyMethodDefinitions::JSObjectProxy_dealloc(JSObjectProxy *self) { - // TODO (Caleb Aikens): intentional override of PyDict_Type's tp_dealloc. Probably results in leaking dict memory - self->jsObject.set(nullptr); - return; + self->jsObject->set(nullptr); + delete self->jsObject; + PyObject_GC_UnTrack(self); + PyObject_GC_Del(self); } -PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) +int JSObjectProxyMethodDefinitions::JSObjectProxy_traverse(JSObjectProxy *self, visitproc visit, void *arg) { - PyObject *self = PyDict_Type.tp_new(subtype, args, kwds); - ((JSObjectProxy *)self)->jsObject = JS::RootedObject(GLOBAL_CX, nullptr); - return self; + // Nothing to be done + return 0; } -int JSObjectProxyMethodDefinitions::JSObjectProxy_init(JSObjectProxy *self, PyObject *args, PyObject *kwds) +int JSObjectProxyMethodDefinitions::JSObjectProxy_clear(JSObjectProxy *self) { - // make fresh JSObject for proxy - self->jsObject.set(JS_NewPlainObject(GLOBAL_CX)); + // Nothing to be done return 0; } Py_ssize_t JSObjectProxyMethodDefinitions::JSObjectProxy_length(JSObjectProxy *self) { JS::RootedIdVector props(GLOBAL_CX); - if (!js::GetPropertyKeys(GLOBAL_CX, self->jsObject, JSITER_OWNONLY | JSITER_HIDDEN, &props)) + if (!js::GetPropertyKeys(GLOBAL_CX, *(self->jsObject), JSITER_OWNONLY, &props)) { - // @TODO (Caleb Aikens) raise exception here + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); return -1; } - return props.length(); } +static inline PyObject *getKey(JSObjectProxy *self, PyObject *key, JS::HandleId id, bool checkPropertyShadowsMethod) { + // look through the methods for dispatch + for (size_t index = 0;; index++) { + const char *methodName = JSObjectProxyType.tp_methods[index].ml_name; + if (methodName == NULL || !PyUnicode_Check(key)) { + JS::RootedValue value(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsObject), id, &value); + // if value is a JSFunction, bind `this` to self + /* (Caleb Aikens) its potentially problematic to bind it like this since if the function + * ever gets assigned to another object like so: + * + * jsObjA.func = jsObjB.func + * jsObjA.func() # `this` will be jsObjB not jsObjA + * + * It will be bound to the wrong object, however I can't find a better way to do this, + * and even pyodide works this way weirdly enough: + * https://github.com/pyodide/pyodide/blob/ee863a7f7907dfb6ee4948bde6908453c9d7ac43/src/core/jsproxy.c#L388 + * + * if the user wants to get an unbound JS function to bind later, they will have to get it without accessing it through + * a JSObjectProxy (such as via pythonmonkey.eval or as the result of some other function) + */ + if (value.isObject()) { + JS::RootedObject valueObject(GLOBAL_CX); + JS_ValueToObject(GLOBAL_CX, value, &valueObject); + js::ESClass cls; + JS::GetBuiltinClass(GLOBAL_CX, valueObject, &cls); + if (cls == js::ESClass::Function) { + JS::Rooted> args(GLOBAL_CX); + args[0].setObject(*((*(self->jsObject)).get())); + JS::RootedValue boundFunction(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, valueObject, "bind", args, &boundFunction)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + value.set(boundFunction); + } + } + else if (value.isUndefined() && PyUnicode_Check(key)) { + if (strcmp("__class__", PyUnicode_AsUTF8(key)) == 0) { + return PyObject_GenericGetAttr((PyObject *)self, key); + } + } + + return pyTypeFactory(GLOBAL_CX, value); + } + else { + if (strcmp(methodName, PyUnicode_AsUTF8(key)) == 0) { + if (checkPropertyShadowsMethod) { + // just make sure no property is shadowing a method by name + JS::RootedValue value(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsObject), id, &value); + if (!value.isUndefined()) { + return pyTypeFactory(GLOBAL_CX, value); + } + } + + return PyObject_GenericGetAttr((PyObject *)self, key); + } + } + } +} + PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_get(JSObjectProxy *self, PyObject *key) { JS::RootedId id(GLOBAL_CX); if (!keyToId(key, &id)) { - // TODO (Caleb Aikens): raise exception here - return NULL; // key is not a str or int + PyErr_SetString(PyExc_AttributeError, "JSObjectProxy property name must be of type str or int"); + return NULL; } - JS::RootedValue *value = new JS::RootedValue(GLOBAL_CX); - JS_GetPropertyById(GLOBAL_CX, self->jsObject, id, value); - JS::RootedObject *global = new JS::RootedObject(GLOBAL_CX, JS::GetNonCCWObjectGlobal(self->jsObject)); - return pyTypeFactory(GLOBAL_CX, global, value)->getPyObject(); + return getKey(self, key, id, false); } -int JSObjectProxyMethodDefinitions::JSObjectProxy_assign(JSObjectProxy *self, PyObject *key, PyObject *value) +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_get_subscript(JSObjectProxy *self, PyObject *key) { JS::RootedId id(GLOBAL_CX); - if (!keyToId(key, &id)) { // invalid key - // TODO (Caleb Aikens): raise exception here + if (!keyToId(key, &id)) { + PyErr_SetString(PyExc_AttributeError, "JSObjectProxy property name must be of type str or int"); + return NULL; + } + + return getKey(self, key, id, true); +} + +int JSObjectProxyMethodDefinitions::JSObjectProxy_contains(JSObjectProxy *self, PyObject *key) +{ + JS::RootedId id(GLOBAL_CX); + if (!keyToId(key, &id)) { + PyErr_SetString(PyExc_AttributeError, "JSObjectProxy property name must be of type str or int"); return -1; } + JS::RootedValue value(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsObject), id, &value); + return value.isUndefined() ? 0 : 1; +} +static inline void assignKeyValue(JSObjectProxy *self, PyObject *key, JS::HandleId id, PyObject *value) { if (value) { // we are setting a value JS::RootedValue jValue(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, value)); - JS_SetPropertyById(GLOBAL_CX, self->jsObject, id, jValue); + JS_SetPropertyById(GLOBAL_CX, *(self->jsObject), id, jValue); } else { // we are deleting a value JS::ObjectOpResult ignoredResult; - JS_DeletePropertyById(GLOBAL_CX, self->jsObject, id, ignoredResult); + JS_DeletePropertyById(GLOBAL_CX, *(self->jsObject), id, ignoredResult); } - - return 0; } -void JSObjectProxyMethodDefinitions::JSObjectProxy_set_helper(JS::HandleObject jsObject, PyObject *key, JS::HandleValue value) +int JSObjectProxyMethodDefinitions::JSObjectProxy_assign(JSObjectProxy *self, PyObject *key, PyObject *value) { JS::RootedId id(GLOBAL_CX); - keyToId(key, &id); - JS_SetPropertyById(GLOBAL_CX, jsObject, id, value); + if (!keyToId(key, &id)) { // invalid key + PyErr_SetString(PyExc_AttributeError, "JSObjectProxy property name must be of type str or int"); + return -1; + } + + assignKeyValue(self, key, id, value); + + return 0; } PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_richcompare(JSObjectProxy *self, PyObject *other, int op) @@ -147,8 +235,8 @@ bool JSObjectProxyMethodDefinitions::JSObjectProxy_richcompare_helper(JSObjectPr visited.insert({{(PyObject *)self, other}}); if (Py_TYPE((PyObject *)self) == Py_TYPE(other)) { - JS::RootedValue selfVal(GLOBAL_CX, JS::ObjectValue(*self->jsObject)); - JS::RootedValue otherVal(GLOBAL_CX, JS::ObjectValue(*(*(JSObjectProxy *)other).jsObject)); + JS::RootedValue selfVal(GLOBAL_CX, JS::ObjectValue(**(self->jsObject))); + JS::RootedValue otherVal(GLOBAL_CX, JS::ObjectValue(**(*(JSObjectProxy *)other).jsObject)); if (selfVal.asRawBits() == otherVal.asRawBits()) { return true; } @@ -156,23 +244,24 @@ bool JSObjectProxyMethodDefinitions::JSObjectProxy_richcompare_helper(JSObjectPr } JS::RootedIdVector props(GLOBAL_CX); - if (!js::GetPropertyKeys(GLOBAL_CX, self->jsObject, JSITER_OWNONLY | JSITER_HIDDEN, &props)) + if (!js::GetPropertyKeys(GLOBAL_CX, *(self->jsObject), JSITER_OWNONLY, &props)) { - // @TODO (Caleb Aikens) raise exception here - return NULL; + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return false; } // iterate recursively through members of self and check for equality - for (size_t i = 0; i < props.length(); i++) + size_t length = props.length(); + for (size_t i = 0; i < length; i++) { JS::HandleId id = props[i]; - JS::RootedValue *key = new JS::RootedValue(GLOBAL_CX); - key->setString(id.toString()); + JS::RootedValue key(GLOBAL_CX); + key.setString(id.toString()); - JS::RootedObject *global = new JS::RootedObject(GLOBAL_CX, JS::GetNonCCWObjectGlobal(self->jsObject)); - PyObject *pyKey = pyTypeFactory(GLOBAL_CX, global, key)->getPyObject(); + PyObject *pyKey = pyTypeFactory(GLOBAL_CX, key); PyObject *pyVal1 = PyObject_GetItem((PyObject *)self, pyKey); PyObject *pyVal2 = PyObject_GetItem((PyObject *)other, pyKey); + Py_DECREF(pyKey); if (!pyVal2) { // if other.key is NULL then not equal return false; } @@ -197,56 +286,510 @@ bool JSObjectProxyMethodDefinitions::JSObjectProxy_richcompare_helper(JSObjectPr } PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_iter(JSObjectProxy *self) { - JSContext *cx = GLOBAL_CX; - JS::RootedObject *global = new JS::RootedObject(cx, JS::GetNonCCWObjectGlobal(self->jsObject)); - + // key iteration + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.it_index = 0; + iterator->it.reversed = false; + iterator->it.kind = KIND_KEYS; + Py_INCREF(self); + iterator->it.di_dict = (PyDictObject *)self; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); // Get **enumerable** own properties - JS::RootedIdVector props(cx); - if (!js::GetPropertyKeys(cx, self->jsObject, JSITER_OWNONLY, &props)) { + if (!js::GetPropertyKeys(GLOBAL_CX, *(self->jsObject), JSITER_OWNONLY, iterator->it.props)) { return NULL; } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} - // Populate a Python tuple with (propertyKey, value) pairs from the JS object - // Similar to `Object.entries()` - size_t length = props.length(); - PyObject *seq = PyTuple_New(length); - for (size_t i = 0; i < length; i++) { - JS::HandleId id = props[i]; - PyObject *key = idToKey(cx, id); +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_iter_next(JSObjectProxy *self) { + PyObject *key = PyUnicode_FromString("next"); + JS::RootedId id(GLOBAL_CX); + if (!keyToId(key, &id)) { + PyErr_SetString(PyExc_SystemError, "JSObjectProxy failed type conversion"); + return NULL; + } - JS::RootedValue *jsVal = new JS::RootedValue(cx); - JS_GetPropertyById(cx, self->jsObject, id, jsVal); - PyObject *value = pyTypeFactory(cx, global, jsVal)->getPyObject(); + PyObject *nextFunction = getKey(self, key, id, false); + Py_DECREF(key); + if (nextFunction == NULL) { + PyErr_SetString(PyExc_SystemError, "JSObjectProxy could not retrieve key"); + return NULL; + } + + PyObject *retVal = JSFunctionProxyMethodDefinitions::JSFunctionProxy_call(nextFunction, PyTuple_New(0), NULL); + Py_DECREF(nextFunction); + if (retVal == NULL) { + return NULL; + } - PyTuple_SetItem(seq, i, PyTuple_Pack(2, key, value)); + // check if end of iteration + key = PyUnicode_FromString("done"); + PyObject *done = JSObjectProxy_get((JSObjectProxy *)retVal, key); + Py_DECREF(key); + if (done == Py_True) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; } - // Convert to a Python iterator - return PyObject_GetIter(seq); + key = PyUnicode_FromString("value"); + PyObject *value = JSObjectProxy_get((JSObjectProxy *)retVal, key); + Py_DECREF(key); + + return value; } PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_repr(JSObjectProxy *self) { // Detect cyclic objects - PyObject *objPtr = PyLong_FromVoidPtr(self->jsObject.get()); + PyObject *objPtr = PyLong_FromVoidPtr(self->jsObject->get()); // For `Py_ReprEnter`, we must get a same PyObject when visiting the same JSObject. // We cannot simply use the object returned by `PyLong_FromVoidPtr` because it won't reuse the PyLongObjects for ints not between -5 and 256. // Instead, we store this PyLongObject in a global dict, using itself as the hashable key, effectively interning the PyLongObject. PyObject *tsDict = PyThreadState_GetDict(); PyObject *cyclicKey = PyDict_SetDefault(tsDict, /*key*/ objPtr, /*value*/ objPtr); // cyclicKey = (tsDict[objPtr] ??= objPtr) - int status = Py_ReprEnter(cyclicKey); - if (status != 0) { // the object has already been processed - return status > 0 ? PyUnicode_FromString("[Circular]") : NULL; + int i = Py_ReprEnter(cyclicKey); + if (i != 0) { + return i > 0 ? PyUnicode_FromString("{...}") : NULL; + } + + Py_ssize_t selfLength = JSObjectProxy_length(self); + + if (selfLength == 0) { + Py_ReprLeave(cyclicKey); + PyDict_DelItem(tsDict, cyclicKey); + return PyUnicode_FromString("{}"); } - // Convert JSObjectProxy to a dict - PyObject *dict = PyDict_New(); - // Update from the iterator emitting key-value pairs - // see https://docs.python.org/3/c-api/dict.html#c.PyDict_MergeFromSeq2 - PyDict_MergeFromSeq2(dict, JSObjectProxy_iter(self), /*override*/ false); - // Get the string representation of this dict - PyObject *str = PyObject_Repr(dict); + _PyUnicodeWriter writer; + + _PyUnicodeWriter_Init(&writer); + + writer.overallocate = 1; + /* "{" + "1: 2" + ", 3: 4" * (len - 1) + "}" */ + writer.min_length = 1 + 4 + (2 + 4) * (selfLength - 1) + 1; + + PyObject *key = NULL, *value = NULL; + + JS::RootedIdVector props(GLOBAL_CX); + + if (_PyUnicodeWriter_WriteChar(&writer, '{') < 0) { + goto error; + } + + /* Do repr() on each key+value pair, and insert ": " between them. Note that repr may mutate the dict. */ + + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(self->jsObject), JSITER_OWNONLY, &props)) { + return NULL; + } + + for (Py_ssize_t index = 0; index < selfLength; index++) { + if (index > 0) { + if (_PyUnicodeWriter_WriteASCIIString(&writer, ", ", 2) < 0) { + goto error; + } + } + + JS::HandleId id = props[index]; + key = idToKey(GLOBAL_CX, id); + + // escape infinite recur on superclass reference + if (strcmp(PyUnicode_AsUTF8(key), "$super") == 0) { + continue; + } + + // Prevent repr from deleting key or value during key format. + Py_INCREF(key); + + PyObject *s = PyObject_Repr(key); + if (s == NULL) { + goto error; + } + + int res = _PyUnicodeWriter_WriteStr(&writer, s); + Py_DECREF(s); + + if (res < 0) { + goto error; + } + + if (_PyUnicodeWriter_WriteASCIIString(&writer, ": ", 2) < 0) { + goto error; + } + + JS::RootedValue elementVal(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsObject), id, &elementVal); + + if (&elementVal.toObject() == (*(self->jsObject)).get()) { + value = (PyObject *)self; + Py_INCREF(value); + } else { + value = pyTypeFactory(GLOBAL_CX, elementVal); + } + + if (value != NULL) { + s = PyObject_Repr(value); + if (s == NULL) { + goto error; + } + + res = _PyUnicodeWriter_WriteStr(&writer, s); + Py_DECREF(s); + if (res < 0) { + goto error; + } + } else { + // clear any exception that was just set + if (PyErr_Occurred()) { + PyErr_Clear(); + } + + if (_PyUnicodeWriter_WriteASCIIString(&writer, "", 19) < 0) { + goto error; + } + } + + Py_CLEAR(key); + Py_CLEAR(value); + } + + writer.overallocate = 0; + if (_PyUnicodeWriter_WriteChar(&writer, '}') < 0) { + goto error; + } Py_ReprLeave(cyclicKey); PyDict_DelItem(tsDict, cyclicKey); - return str; + return _PyUnicodeWriter_Finish(&writer); + +error: + Py_ReprLeave(cyclicKey); + PyDict_DelItem(tsDict, cyclicKey); + _PyUnicodeWriter_Dealloc(&writer); + Py_XDECREF(key); + Py_XDECREF(value); + return NULL; } + +// private +static int mergeFromSeq2(JSObjectProxy *self, PyObject *seq2) { + PyObject *it; /* iter(seq2) */ + Py_ssize_t i; /* index into seq2 of current element */ + PyObject *item; /* seq2[i] */ + PyObject *fast; /* item as a 2-tuple or 2-list */ + + it = PyObject_GetIter(seq2); + if (it == NULL) + return -1; + + for (i = 0;; ++i) { + PyObject *key, *value; + Py_ssize_t n; + + fast = NULL; + item = PyIter_Next(it); + if (item == NULL) { + if (PyErr_Occurred()) + goto Fail; + break; + } + + /* Convert item to sequence, and verify length 2. */ + fast = PySequence_Fast(item, ""); + if (fast == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) + PyErr_Format(PyExc_TypeError, + "cannot convert dictionary update " + "sequence element #%zd to a sequence", + i); + goto Fail; + } + n = PySequence_Fast_GET_SIZE(fast); + if (n != 2) { + PyErr_Format(PyExc_ValueError, + "dictionary update sequence element #%zd " + "has length %zd; 2 is required", + i, n); + goto Fail; + } + + /* Update/merge with this (key, value) pair. */ + key = PySequence_Fast_GET_ITEM(fast, 0); + value = PySequence_Fast_GET_ITEM(fast, 1); + Py_INCREF(key); + Py_INCREF(value); + + if (JSObjectProxyMethodDefinitions::JSObjectProxy_assign(self, key, value) < 0) { + Py_DECREF(key); + Py_DECREF(value); + goto Fail; + } + + Py_DECREF(key); + Py_DECREF(value); + Py_DECREF(fast); + Py_DECREF(item); + } + + i = 0; + goto Return; +Fail: + Py_XDECREF(item); + Py_XDECREF(fast); + i = -1; +Return: + Py_DECREF(it); + return Py_SAFE_DOWNCAST(i, Py_ssize_t, int); +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_or(JSObjectProxy *self, PyObject *other) { + #if PY_VERSION_HEX < 0x03090000 + // | is not supported on dicts in python3.8 or less, so only allow if both + // operands are JSObjectProxy + if (!PyObject_TypeCheck(self, &JSObjectProxyType) || !PyObject_TypeCheck(other, &JSObjectProxyType)) { + Py_RETURN_NOTIMPLEMENTED; + } + #endif + if (!PyDict_Check(self) || !PyDict_Check(other)) { + Py_RETURN_NOTIMPLEMENTED; + } + + if (!PyObject_TypeCheck(self, &JSObjectProxyType) && PyObject_TypeCheck(other, &JSObjectProxyType)) { + return PyDict_Type.tp_as_number->nb_or((PyObject *)&(self->dict), other); + } + else { + JS::Rooted> args(GLOBAL_CX); + args[0].setObjectOrNull(JS_NewPlainObject(GLOBAL_CX)); + args[1].setObjectOrNull(*(self->jsObject)); // this is null is left operand is real dict + JS::RootedValue jValueOther(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, other)); + args[2].setObject(jValueOther.toObject()); + + JS::RootedObject global(GLOBAL_CX, JS::GetNonCCWObjectGlobal(*(self->jsObject))); + + // call Object.assign + JS::RootedValue Object(GLOBAL_CX); + if (!JS_GetProperty(GLOBAL_CX, global, "Object", &Object)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + + JS::RootedObject rootedObject(GLOBAL_CX, Object.toObjectOrNull()); + JS::RootedValue ret(GLOBAL_CX); + + if (!JS_CallFunctionName(GLOBAL_CX, rootedObject, "assign", args, &ret)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + return pyTypeFactory(GLOBAL_CX, ret); + } +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_ior(JSObjectProxy *self, PyObject *other) { + if (PyDict_Check(other)) { + JS::Rooted> args(GLOBAL_CX); + args[0].setObjectOrNull(*(self->jsObject)); + JS::RootedValue jValueOther(GLOBAL_CX, jsTypeFactory(GLOBAL_CX, other)); + args[1].setObject(jValueOther.toObject()); + + JS::RootedObject global(GLOBAL_CX, JS::GetNonCCWObjectGlobal(*(self->jsObject))); + + // call Object.assign + JS::RootedValue Object(GLOBAL_CX); + if (!JS_GetProperty(GLOBAL_CX, global, "Object", &Object)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + + JS::RootedObject rootedObject(GLOBAL_CX, Object.toObjectOrNull()); + JS::RootedValue ret(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, rootedObject, "assign", args, &ret)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + } + else { + if (mergeFromSeq2(self, other) < 0) { + return NULL; + } + } + + Py_INCREF(self); + return (PyObject *)self; +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_get_method(JSObjectProxy *self, PyObject *const *args, Py_ssize_t nargs) { + PyObject *key; + PyObject *default_value = Py_None; + + if (!_PyArg_CheckPositional("get", nargs, 1, 2)) { + return NULL; + } + key = args[0]; + if (nargs < 2) { + goto skip_optional; + } + + default_value = args[1]; + +skip_optional: + + PyObject *value = JSObjectProxy_get(self, key); + if (value == Py_None) { + Py_INCREF(default_value); + value = default_value; + } + + return value; +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_setdefault_method(JSObjectProxy *self, PyObject *const *args, Py_ssize_t nargs) { + PyObject *key; + PyObject *default_value = Py_None; + + if (!_PyArg_CheckPositional("setdefault", nargs, 1, 2)) { + return NULL; + } + key = args[0]; + if (nargs < 2) { + goto skip_optional; + } + + default_value = args[1]; + +skip_optional: + + JS::RootedId id(GLOBAL_CX); + if (!keyToId(key, &id)) { // invalid key + // TODO (Caleb Aikens): raise exception here + return NULL; + } + + PyObject *value = getKey(self, key, id, true); + if (value == Py_None) { + assignKeyValue(self, key, id, default_value); + Py_XINCREF(default_value); + return default_value; + } + + return value; +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_pop_method(JSObjectProxy *self, PyObject *const *args, Py_ssize_t nargs) { + PyObject *key; + PyObject *default_value = NULL; + + if (!_PyArg_CheckPositional("pop", nargs, 1, 2)) { + return NULL; + } + key = args[0]; + if (nargs < 2) { + goto skip_optional; + } + default_value = args[1]; + +skip_optional: + JS::RootedId id(GLOBAL_CX); + if (!keyToId(key, &id)) { + PyErr_SetString(PyExc_AttributeError, "JSObjectProxy property name must be of type str or int"); + return NULL; + } + + JS::RootedValue value(GLOBAL_CX); + JS_GetPropertyById(GLOBAL_CX, *(self->jsObject), id, &value); + if (value.isUndefined()) { + if (default_value != NULL) { + Py_INCREF(default_value); + return default_value; + } + PyErr_SetKeyError(key); + return NULL; + } + else { + JS::ObjectOpResult ignoredResult; + JS_DeletePropertyById(GLOBAL_CX, *(self->jsObject), id, ignoredResult); + + return pyTypeFactory(GLOBAL_CX, value); + } +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_clear_method(JSObjectProxy *self) { + JS::RootedIdVector props(GLOBAL_CX); + if (!js::GetPropertyKeys(GLOBAL_CX, *(self->jsObject), JSITER_OWNONLY, &props)) + { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + + JS::ObjectOpResult ignoredResult; + size_t length = props.length(); + for (size_t index = 0; index < length; index++) { + JS_DeletePropertyById(GLOBAL_CX, *(self->jsObject), props[index], ignoredResult); + } + + Py_RETURN_NONE; +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_copy_method(JSObjectProxy *self) { + JS::Rooted> args(GLOBAL_CX); + args[0].setObjectOrNull(JS_NewPlainObject(GLOBAL_CX)); + args[1].setObjectOrNull(*(self->jsObject)); + + JS::RootedObject global(GLOBAL_CX, JS::GetNonCCWObjectGlobal(*(self->jsObject))); + + // call Object.assign + JS::RootedValue Object(GLOBAL_CX); + if (!JS_GetProperty(GLOBAL_CX, global, "Object", &Object)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + + JS::RootedObject rootedObject(GLOBAL_CX, Object.toObjectOrNull()); + JS::RootedValue ret(GLOBAL_CX); + if (!JS_CallFunctionName(GLOBAL_CX, rootedObject, "assign", args, &ret)) { + PyErr_Format(PyExc_SystemError, "%s JSAPI call failed", JSObjectProxyType.tp_name); + return NULL; + } + return pyTypeFactory(GLOBAL_CX, ret); +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_update_method(JSObjectProxy *self, PyObject *args, PyObject *kwds) { + PyObject *arg = NULL; + int result = 0; + + if (!PyArg_UnpackTuple(args, "update", 0, 1, &arg)) { + return NULL; + } + else if (arg != NULL) { + if (PyDict_CheckExact(arg) || PyObject_TypeCheck(arg, &JSObjectProxyType)) { + JSObjectProxyMethodDefinitions::JSObjectProxy_ior((JSObjectProxy *)self, arg); + result = 0; + } else { // iterable + result = mergeFromSeq2((JSObjectProxy *)self, arg); + if (result < 0) { + return NULL; + } + } + } + + if (result == 0 && kwds != NULL) { + if (PyArg_ValidateKeywordArguments(kwds)) { + JSObjectProxyMethodDefinitions::JSObjectProxy_ior((JSObjectProxy *)self, kwds); + } + } + Py_RETURN_NONE; +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_keys_method(JSObjectProxy *self) { + return PyDictView_New((PyObject *)self, &JSObjectKeysProxyType); +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_values_method(JSObjectProxy *self) { + return PyDictView_New((PyObject *)self, &JSObjectValuesProxyType); +} + +PyObject *JSObjectProxyMethodDefinitions::JSObjectProxy_items_method(JSObjectProxy *self) { + return PyDictView_New((PyObject *)self, &JSObjectItemsProxyType); +} \ No newline at end of file diff --git a/src/JSObjectValuesProxy.cc b/src/JSObjectValuesProxy.cc new file mode 100644 index 00000000..ead71b66 --- /dev/null +++ b/src/JSObjectValuesProxy.cc @@ -0,0 +1,152 @@ +/** + * @file JSObjectValuesProxy.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief JSObjectValuesProxy is a custom C-implemented python type that derives from dict values + * @date 2024-01-19 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#include "include/JSObjectValuesProxy.hh" + +#include "include/JSObjectIterProxy.hh" +#include "include/JSObjectProxy.hh" +#include "include/JSArrayProxy.hh" + +#include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/jsTypeFactory.hh" +#include "include/PyDictProxyHandler.hh" + +#include +#include + +#include +#include "include/pyshim.hh" + +void JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_dealloc(JSObjectValuesProxy *self) +{ + PyObject_GC_UnTrack(self); + Py_XDECREF(self->dv.dv_dict); + PyObject_GC_Del(self); +} + +Py_ssize_t JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_length(JSObjectValuesProxy *self) +{ + if (self->dv.dv_dict == NULL) { + return 0; + } + return JSObjectProxyMethodDefinitions::JSObjectProxy_length((JSObjectProxy *)self->dv.dv_dict); +} + +int JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_contains(JSObjectValuesProxy *self, PyObject *key) +{ + if (self->dv.dv_dict == NULL) { + return 0; + } + return JSObjectProxyMethodDefinitions::JSObjectProxy_contains((JSObjectProxy *)self->dv.dv_dict, key); +} + +int JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_traverse(JSObjectValuesProxy *self, visitproc visit, void *arg) { + Py_VISIT(self->dv.dv_dict); + return 0; +} + +int JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_clear(JSObjectValuesProxy *self) { + Py_CLEAR(self->dv.dv_dict); + return 0; +} + +// private +static int all_contained_in(PyObject *self, PyObject *other) { + PyObject *iter = PyObject_GetIter(self); + int ok = 1; + + if (iter == NULL) { + return -1; + } + + for (;; ) { + PyObject *next = PyIter_Next(iter); + if (next == NULL) { + if (PyErr_Occurred()) + ok = -1; + break; + } + if (PyObject_TypeCheck(other, &JSObjectValuesProxyType)) { + JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_contains((JSObjectValuesProxy *)other, next); + } + else { + ok = PySequence_Contains(other, next); + } + Py_DECREF(next); + if (ok <= 0) + break; + } + + Py_DECREF(iter); + return ok; +} + +PyObject *JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_iter(JSObjectValuesProxy *self) { + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = false; + iterator->it.it_index = 0; + iterator->it.kind = KIND_VALUES; + Py_INCREF(self->dv.dv_dict); + iterator->it.di_dict = self->dv.dv_dict; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { + return NULL; + } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_iter_reverse(JSObjectValuesProxy *self) { + JSObjectIterProxy *iterator = PyObject_GC_New(JSObjectIterProxy, &JSObjectIterProxyType); + if (iterator == NULL) { + return NULL; + } + iterator->it.reversed = true; + iterator->it.it_index = JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_length(self) - 1; + iterator->it.kind = KIND_VALUES; + Py_INCREF(self->dv.dv_dict); + iterator->it.di_dict = self->dv.dv_dict; + iterator->it.props = new JS::PersistentRootedIdVector(GLOBAL_CX); + // Get **enumerable** own properties + if (!js::GetPropertyKeys(GLOBAL_CX, *(((JSObjectProxy *)(self->dv.dv_dict))->jsObject), JSITER_OWNONLY, iterator->it.props)) { + return NULL; + } + PyObject_GC_Track(iterator); + return (PyObject *)iterator; +} + +PyObject *JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_repr(JSObjectValuesProxy *self) { + PyObject *seq; + PyObject *result = NULL; + + Py_ssize_t rc = Py_ReprEnter((PyObject *)self); + if (rc != 0) { + return rc > 0 ? PyUnicode_FromString("...") : NULL; + } + + seq = PySequence_List((PyObject *)self); + if (seq == NULL) { + goto Done; + } + + result = PyUnicode_FromFormat("%s(%R)", PyDictValues_Type.tp_name, seq); + Py_DECREF(seq); +Done: + Py_ReprLeave((PyObject *)self); + return result; +} + +PyObject *JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_mapping(PyObject *self, void *Py_UNUSED(ignored)) { + return PyDictProxy_New((PyObject *)((_PyDictViewObject *)self)->dv_dict); +} \ No newline at end of file diff --git a/src/JSStringProxy.cc b/src/JSStringProxy.cc new file mode 100644 index 00000000..2b68f6bb --- /dev/null +++ b/src/JSStringProxy.cc @@ -0,0 +1,29 @@ +/** + * @file JSStringProxy.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (plaporte@distributive.network) + * @brief JSStringProxy is a custom C-implemented python type that derives from str. It acts as a proxy for JSStrings from Spidermonkey, and behaves like a str would. + * @date 2024-05-15 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + +#include "include/JSStringProxy.hh" + +#include "include/StrType.hh" + +std::unordered_set jsStringProxies; +extern JSContext *GLOBAL_CX; + + +void JSStringProxyMethodDefinitions::JSStringProxy_dealloc(JSStringProxy *self) +{ + jsStringProxies.erase(self); + delete self->jsString; +} + +PyObject *JSStringProxyMethodDefinitions::JSStringProxy_copy_method(JSStringProxy *self) { + JS::RootedString selfString(GLOBAL_CX, ((JSStringProxy *)self)->jsString->toString()); + JS::RootedValue selfStringValue(GLOBAL_CX, JS::StringValue(selfString)); + return StrType::proxifyString(GLOBAL_CX, selfStringValue); +} \ No newline at end of file diff --git a/src/JobQueue.cc b/src/JobQueue.cc index ee7a013f..928746fd 100644 --- a/src/JobQueue.cc +++ b/src/JobQueue.cc @@ -1,16 +1,34 @@ +/** + * @file JobQueue.cc + * @author Tom Tang (xmader@distributive.network) + * @brief Implements the ECMAScript Job Queue + * @date 2023-04-03 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ #include "include/JobQueue.hh" +#include "include/modules/pythonmonkey/pythonmonkey.hh" #include "include/PyEventLoop.hh" #include "include/pyTypeFactory.hh" +#include "include/PromiseType.hh" #include + #include +#include #include -JSObject *JobQueue::getIncumbentGlobal(JSContext *cx) { - return JS::CurrentGlobalOrNull(cx); +JobQueue::JobQueue(JSContext *cx) { + finalizationRegistryCallbacks = new JS::PersistentRooted(cx); // Leaks but it's OK since freed at process exit +} + +bool JobQueue::getHostDefinedData(JSContext *cx, JS::MutableHandle data) const { + data.set(nullptr); // We don't need the host defined data + return true; // `true` indicates no error } bool JobQueue::enqueuePromiseJob(JSContext *cx, @@ -20,42 +38,49 @@ bool JobQueue::enqueuePromiseJob(JSContext *cx, JS::HandleObject incumbentGlobal) { // Convert the `job` JS function to a Python function for event-loop callback - MOZ_RELEASE_ASSERT(js::IsFunctionObject(job)); - // FIXME (Tom Tang): memory leak, objects not free-ed - // FIXME (Tom Tang): `job` function is going to be GC-ed ??? - auto global = new JS::RootedObject(cx, incumbentGlobal); - auto jobv = new JS::RootedValue(cx, JS::ObjectValue(*job)); - auto callback = pyTypeFactory(cx, global, jobv)->getPyObject(); - - // Inform the JS runtime that the job queue is no longer empty - JS::JobQueueMayNotBeEmpty(cx); + JS::RootedValue jobv(cx, JS::ObjectValue(*job)); + PyObject *callback = pyTypeFactory(cx, jobv); // Send job to the running Python event-loop PyEventLoop loop = PyEventLoop::getRunningLoop(); if (!loop.initialized()) return false; + + // Inform the JS runtime that the job queue is no longer empty + JS::JobQueueMayNotBeEmpty(cx); + loop.enqueue(callback); + Py_DECREF(callback); return true; } void JobQueue::runJobs(JSContext *cx) { - return; + // Do nothing } -// is empty bool JobQueue::empty() const { // TODO (Tom Tang): implement using `get_running_loop` and getting job count on loop??? - throw std::logic_error("JobQueue::empty is not implemented\n"); + return true; // see https://hg.mozilla.org/releases/mozilla-esr128/file/tip/js/src/builtin/Promise.cpp#l6946 +} + +bool JobQueue::isDrainingStopped() const { + // TODO (Tom Tang): implement this by detecting if the Python event-loop is still running + return false; } js::UniquePtr JobQueue::saveJobQueue(JSContext *cx) { auto saved = js::MakeUnique(); + if (!saved) { + JS_ReportOutOfMemory(cx); + return NULL; + } return saved; } bool JobQueue::init(JSContext *cx) { JS::SetJobQueue(cx, this); - JS::InitDispatchToEventLoop(cx, /* callback */ dispatchToEventLoop, /* closure */ cx); + JS::InitDispatchToEventLoop(cx, dispatchToEventLoop, cx); + JS::SetPromiseRejectionTrackerCallback(cx, promiseRejectionTracker); return true; } @@ -65,12 +90,31 @@ static PyObject *callDispatchFunc(PyObject *dispatchFuncTuple, PyObject *Py_UNUS dispatchable->run(cx, JS::Dispatchable::NotShuttingDown); Py_RETURN_NONE; } + static PyMethodDef callDispatchFuncDef = {"JsDispatchCallable", callDispatchFunc, METH_NOARGS, NULL}; +bool JobQueue::dispatchToEventLoop(void *closure, JS::Dispatchable *dispatchable) { + JSContext *cx = (JSContext *)closure; + + // The `dispatchToEventLoop` function is running in a helper thread, so + // we must acquire the Python GIL (global interpreter lock) + // see https://docs.python.org/3/c-api/init.html#non-python-created-threads + PyGILState_STATE gstate = PyGILState_Ensure(); + + PyObject *dispatchFuncTuple = PyTuple_Pack(2, PyLong_FromVoidPtr(cx), PyLong_FromVoidPtr(dispatchable)); + PyObject *pyFunc = PyCFunction_New(&callDispatchFuncDef, dispatchFuncTuple); + + // Avoid using the current, JS helper thread to send jobs to event-loop as it may cause deadlock + PyThread_start_new_thread((void (*)(void *)) &sendJobToMainLoop, pyFunc); + + PyGILState_Release(gstate); + return true; +} + bool sendJobToMainLoop(PyObject *pyFunc) { PyGILState_STATE gstate = PyGILState_Ensure(); - // Send job to the running Python event-loop on `cx`'s thread (the main thread) + // Send job to the running Python event-loop on cx's thread (the main thread) PyEventLoop loop = PyEventLoop::getMainLoop(); if (!loop.initialized()) { PyGILState_Release(gstate); @@ -78,25 +122,66 @@ bool sendJobToMainLoop(PyObject *pyFunc) { } loop.enqueue(pyFunc); + loop._loop = nullptr; // the `Py_XDECREF` Python API call in `PyEventLoop`'s destructor will not be accessible once we hand over the GIL by `PyGILState_Release` PyGILState_Release(gstate); return true; } -/* static */ -bool JobQueue::dispatchToEventLoop(void *closure, JS::Dispatchable *dispatchable) { - JSContext *cx = (JSContext *)closure; // `closure` is provided in `JS::InitDispatchToEventLoop` call - - // The `dispatchToEventLoop` function is running in a helper thread, so - // we must acquire the Python GIL (global interpreter lock) - // see https://docs.python.org/3/c-api/init.html#non-python-created-threads - PyGILState_STATE gstate = PyGILState_Ensure(); +void JobQueue::promiseRejectionTracker(JSContext *cx, + bool mutedErrors, + JS::HandleObject promise, + JS::PromiseRejectionHandlingState state, + [[maybe_unused]] void *privateData) { - PyObject *dispatchFuncTuple = PyTuple_Pack(2, PyLong_FromVoidPtr(cx), PyLong_FromVoidPtr(dispatchable)); - PyObject *pyFunc = PyCFunction_New(&callDispatchFuncDef, dispatchFuncTuple); + // We only care about unhandled Promises + if (state != JS::PromiseRejectionHandlingState::Unhandled) { + return; + } + // If the `mutedErrors` option is set to True in `pm.eval`, eval errors or unhandled rejections should be ignored. + if (mutedErrors) { + return; + } - // Avoid using the JS helper thread to send jobs to event-loop as it may cause deadlock - PyThread_start_new_thread((void (*)(void *)) &sendJobToMainLoop, pyFunc); + // Test if there's no user-defined (or pmjs defined) exception handler on the Python event-loop + PyEventLoop loop = PyEventLoop::getRunningLoop(); + if (!loop.initialized()) return; + PyObject *customHandler = PyObject_GetAttrString(loop._loop, "_exception_handler"); // see https://github.com/python/cpython/blob/v3.9.16/Lib/asyncio/base_events.py#L1782 + if (customHandler == Py_None) { // we only have the default exception handler + // Set an exception handler to the event-loop + PyObject *pmModule = PyImport_ImportModule("pythonmonkey"); + PyObject *exceptionHandler = PyObject_GetAttrString(pmModule, "simpleUncaughtExceptionHandler"); + PyObject_CallMethod(loop._loop, "set_exception_handler", "O", exceptionHandler); + Py_DECREF(pmModule); + Py_DECREF(exceptionHandler); + } + Py_DECREF(customHandler); + + // Go ahead and send this unhandled Promise rejection to the exception handler on the Python event-loop + PyObject *pyFuture = PromiseType::getPyObject(cx, promise); // ref count == 2 + // Unhandled Future object calls the event-loop exception handler in its destructor (the `__del__` magic method) + // See https://github.com/python/cpython/blob/v3.9.16/Lib/asyncio/futures.py#L108 + // or https://github.com/python/cpython/blob/v3.9.16/Modules/_asynciomodule.c#L1457-L1467 (It will actually use the C module by default, see futures.py#L417-L423) + Py_DECREF(pyFuture); // decreasing the reference count from 2 to 1, leaving one for the `onResolved` callback in `PromiseType::getPyObject`, which will be called very soon and clean up the reference +} - PyGILState_Release(gstate); - return true; // dispatchable must eventually run +void JobQueue::queueFinalizationRegistryCallback(JSFunction *callback) { + mozilla::Unused << finalizationRegistryCallbacks->append(callback); } + +bool JobQueue::runFinalizationRegistryCallbacks(JSContext *cx) { + bool ranCallbacks = false; + JS::Rooted callbacks(cx); + std::swap(callbacks.get(), finalizationRegistryCallbacks->get()); + for (JSFunction *f: callbacks) { + JS::ExposeObjectToActiveJS(JS_GetFunctionObject(f)); + + JSAutoRealm ar(cx, JS_GetFunctionObject(f)); + JS::RootedFunction func(cx, f); + JS::RootedValue unused_rval(cx); + // we don't raise an exception here because there is nowhere to catch it + mozilla::Unused << JS_CallFunction(cx, NULL, func, JS::HandleValueArray::empty(), &unused_rval); + ranCallbacks = true; + } + + return ranCallbacks; +} \ No newline at end of file diff --git a/src/ListType.cc b/src/ListType.cc index 2d6e7652..753e6237 100644 --- a/src/ListType.cc +++ b/src/ListType.cc @@ -1,31 +1,25 @@ -#include "include/ListType.hh" - -#include "include/PyType.hh" -#include "include/pyTypeFactory.hh" - -#include +/** + * @file ListType.cc + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for representing python lists + * @date 2022-08-18 + * + * @copyright Copyright (c) 2022, 2023, 2024 Distributive Corp + * + */ -#include -ListType::ListType() : PyType(PyList_New(0)) {} -ListType::ListType(PyObject *object) : PyType(object) {} - -PyType *ListType::get(int index) const { - return pyTypeFactory(PyList_GetItem(this->pyObject, index)); -} - -void ListType::set(int index, PyType *object) { - PyList_SetItem(this->pyObject, index, object->getPyObject()); -} +#include "include/ListType.hh" -void ListType::append(PyType *value) { - PyList_Append(this->pyObject, value->getPyObject()); -} +#include "include/JSArrayProxy.hh" -int ListType::len() const { - return PyList_Size(this->pyObject); -} -void ListType::sort() { - PyList_Sort(this->pyObject); +PyObject *ListType::getPyObject(JSContext *cx, JS::HandleObject jsArrayObj) { + JSArrayProxy *proxy = (JSArrayProxy *)PyObject_CallObject((PyObject *)&JSArrayProxyType, NULL); + if (proxy != NULL) { + proxy->jsArray = new JS::PersistentRootedObject(cx); + proxy->jsArray->set(jsArrayObj); + return (PyObject *)proxy; + } + return NULL; } \ No newline at end of file diff --git a/src/NoneType.cc b/src/NoneType.cc index 3136c779..93c0d01c 100644 --- a/src/NoneType.cc +++ b/src/NoneType.cc @@ -1,17 +1,16 @@ /** - * @file NoneType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @file NoneType.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing None - * @version 0.1 * @date 2023-02-22 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #include "include/NoneType.hh" -#include "include/PyType.hh" -#include "include/TypeEnum.hh" - -NoneType::NoneType() : PyType(Py_None) {} \ No newline at end of file +PyObject *NoneType::getPyObject() { + Py_INCREF(Py_None); + return Py_None; +} \ No newline at end of file diff --git a/src/NullType.cc b/src/NullType.cc index 9aa510ed..2b4528a2 100644 --- a/src/NullType.cc +++ b/src/NullType.cc @@ -1,18 +1,19 @@ /** - * @file NullType.hh - * @author Caleb Aikens (caleb@distributive.network) + * @file NullType.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Struct for representing JS null in a python object - * @version 0.1 * @date 2023-02-22 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023,2024 Distributive Corp. * */ #include "include/NullType.hh" #include "include/modules/pythonmonkey/pythonmonkey.hh" -#include "include/PyType.hh" -#include "include/TypeEnum.hh" -NullType::NullType() : PyType(PythonMonkey_Null) {} \ No newline at end of file +PyObject *NullType::getPyObject() { + PyObject *pmNull = getPythonMonkeyNull(); + Py_INCREF(pmNull); + return pmNull; +} \ No newline at end of file diff --git a/src/PromiseType.cc b/src/PromiseType.cc index 13365a67..5a3f94b3 100644 --- a/src/PromiseType.cc +++ b/src/PromiseType.cc @@ -2,20 +2,16 @@ * @file PromiseType.cc * @author Tom Tang (xmader@distributive.network) * @brief Struct for representing Promises - * @version 0.1 * @date 2023-03-29 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023 Distributive Corp. * */ #include "include/modules/pythonmonkey/pythonmonkey.hh" - #include "include/PromiseType.hh" - +#include "include/DictType.hh" #include "include/PyEventLoop.hh" -#include "include/PyType.hh" -#include "include/TypeEnum.hh" #include "include/pyTypeFactory.hh" #include "include/jsTypeFactory.hh" @@ -24,35 +20,32 @@ #include #include +#include "include/pyshim.hh" -#define PY_FUTURE_OBJ_SLOT 0 // slot id to access the python object in JS callbacks +// slot ids to access the python object in JS callbacks +#define PY_FUTURE_OBJ_SLOT 0 #define PROMISE_OBJ_SLOT 1 -// slot id must be less than 2 (https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/JSFunction.h#l866), otherwise it will access to arbitrary unsafe memory locations static bool onResolvedCb(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); // Get the Promise state JS::Value promiseObjVal = js::GetFunctionNativeReserved(&args.callee(), PROMISE_OBJ_SLOT); - JS::RootedObject promise = JS::RootedObject(cx, &promiseObjVal.toObject()); + JS::RootedObject promise(cx, &promiseObjVal.toObject()); JS::PromiseState state = JS::GetPromiseState(promise); // Convert the Promise's result (either fulfilled resolution or rejection reason) to a Python object - // FIXME (Tom Tang): memory leak, not free-ed // The result might be another JS function, so we must keep them alive - JS::RootedObject *thisv = new JS::RootedObject(cx); - args.computeThis(cx, thisv); // thisv is the global object, not the promise - JS::RootedValue *resultArg = new JS::RootedValue(cx, args[0]); - PyObject *result = pyTypeFactory(cx, thisv, resultArg)->getPyObject(); + JS::RootedValue resultArg(cx, args[0]); + PyObject *result = pyTypeFactory(cx, resultArg); if (state == JS::PromiseState::Rejected && !PyExceptionInstance_Check(result)) { // Wrap the result object into a SpiderMonkeyError object // because only *Exception objects can be thrown in Python `raise` statement and alike - #if PY_VERSION_HEX >= 0x03090000 PyObject *wrapped = PyObject_CallOneArg(SpiderMonkeyError, result); // wrapped = SpiderMonkeyError(result) - #else - PyObject *wrapped = PyObject_CallFunction(SpiderMonkeyError, "O", result); // PyObject_CallOneArg is not available in Python < 3.9 - #endif - Py_XDECREF(result); + // Preserve the original JS value as the `jsError` attribute for lossless conversion back + PyObject *originalJsErrCapsule = DictType::getPyObject(cx, resultArg); + PyObject_SetAttrString(wrapped, "jsError", originalJsErrCapsule); + Py_DECREF(result); result = wrapped; } @@ -61,42 +54,46 @@ static bool onResolvedCb(JSContext *cx, unsigned argc, JS::Value *vp) { PyObject *futureObj = (PyObject *)(futureObjVal.toPrivate()); // Settle the Python asyncio.Future by the Promise's result - PyEventLoop::Future future = PyEventLoop::Future(futureObj); + PyEventLoop::Future future = PyEventLoop::Future(futureObj); // will decrease the reference count of `futureObj` in its destructor when the `onResolvedCb` function ends if (state == JS::PromiseState::Fulfilled) { future.setResult(result); } else { // state == JS::PromiseState::Rejected future.setException(result); } + Py_DECREF(result); + // Py_DECREF(futureObj) // the destructor for the `PyEventLoop::Future` above already does this return true; } -PromiseType::PromiseType(PyObject *object) : PyType(object) {} - -PromiseType::PromiseType(JSContext *cx, JS::HandleObject promise) { +PyObject *PromiseType::getPyObject(JSContext *cx, JS::HandleObject promise) { // Create a python asyncio.Future on the running python event-loop PyEventLoop loop = PyEventLoop::getRunningLoop(); - if (!loop.initialized()) return; - PyEventLoop::Future future = loop.createFuture(); + if (!loop.initialized()) return NULL; + PyEventLoop::Future future = loop.createFuture(); // ref count == 1 // Callbacks to settle the Python asyncio.Future once the JS Promise is resolved JS::RootedObject onResolved = JS::RootedObject(cx, (JSObject *)js::NewFunctionWithReserved(cx, onResolvedCb, 1, 0, NULL)); - js::SetFunctionNativeReserved(onResolved, PY_FUTURE_OBJ_SLOT, JS::PrivateValue(future.getFutureObject())); // put the address of the Python object in private slot so we can access it later + js::SetFunctionNativeReserved(onResolved, PY_FUTURE_OBJ_SLOT, JS::PrivateValue(future.getFutureObject())); // ref count == 2 js::SetFunctionNativeReserved(onResolved, PROMISE_OBJ_SLOT, JS::ObjectValue(*promise)); - AddPromiseReactions(cx, promise, onResolved, onResolved); + JS::AddPromiseReactions(cx, promise, onResolved, onResolved); - pyObject = future.getFutureObject(); // must be a new reference + return future.getFutureObject(); // must be a new reference, ref count == 3 + // Here the ref count for the `future` object is 3, but will immediately decrease to 2 in `PyEventLoop::Future`'s destructor when the `PromiseType::getPyObject` function ends + // Leaving one reference for the returned Python object, and another one for the `onResolved` callback function } // Callback to resolve or reject the JS Promise when the Future is done static PyObject *futureOnDoneCallback(PyObject *futureCallbackTuple, PyObject *args) { JSContext *cx = (JSContext *)PyLong_AsVoidPtr(PyTuple_GetItem(futureCallbackTuple, 0)); - auto rootedPtr = (JS::PersistentRooted *)PyLong_AsVoidPtr(PyTuple_GetItem(futureCallbackTuple, 1)); + JS::PersistentRootedObject *rootedPtr = (JS::PersistentRootedObject *)PyLong_AsVoidPtr(PyTuple_GetItem(futureCallbackTuple, 1)); JS::HandleObject promise = *rootedPtr; PyObject *futureObj = PyTuple_GetItem(args, 0); // the callback is called with the Future object as its only argument // see https://docs.python.org/3.9/library/asyncio-future.html#asyncio.Future.add_done_callback PyEventLoop::Future future = PyEventLoop::Future(futureObj); + PyEventLoop::_locker->decCounter(); + PyObject *exception = future.getException(); if (exception == NULL || PyErr_Occurred()) { // awaitable is cancelled, `futureObj.exception()` raises a CancelledError // Reject the promise with the CancelledError, or very unlikely, an InvalidStateError exception if the Future isn’t done yet @@ -121,7 +118,7 @@ static PyObject *futureOnDoneCallback(PyObject *futureCallbackTuple, PyObject *a } static PyMethodDef futureCallbackDef = {"futureOnDoneCallback", futureOnDoneCallback, METH_VARARGS, NULL}; -JSObject *PromiseType::toJsPromise(JSContext *cx) { +JSObject *PromiseType::toJsPromise(JSContext *cx, PyObject *pyObject) { // Create a new JS Promise object JSObject *promise = JS::NewPromiseObject(cx, nullptr); @@ -130,12 +127,14 @@ JSObject *PromiseType::toJsPromise(JSContext *cx) { if (!loop.initialized()) return nullptr; PyEventLoop::Future future = loop.ensureFuture(pyObject); + PyEventLoop::_locker->incCounter(); + // Resolve or Reject the JS Promise once the python awaitable is done JS::PersistentRooted *rootedPtr = new JS::PersistentRooted(cx, promise); // `promise` is required to be rooted from here to the end of onDoneCallback PyObject *futureCallbackTuple = PyTuple_Pack(2, PyLong_FromVoidPtr(cx), PyLong_FromVoidPtr(rootedPtr)); PyObject *onDoneCb = PyCFunction_New(&futureCallbackDef, futureCallbackTuple); future.addDoneCallback(onDoneCb); - + Py_INCREF(pyObject); return promise; } @@ -144,4 +143,4 @@ bool PythonAwaitable_Check(PyObject *obj) { PyTypeObject *tp = Py_TYPE(obj); bool isAwaitable = tp->tp_as_async != NULL && tp->tp_as_async->am_await != NULL; return isAwaitable; -} +} \ No newline at end of file diff --git a/src/PyBaseProxyHandler.cc b/src/PyBaseProxyHandler.cc new file mode 100644 index 00000000..6c6f33ae --- /dev/null +++ b/src/PyBaseProxyHandler.cc @@ -0,0 +1,63 @@ +/** + * @file PyBaseProxyHandler.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects + * @date 2023-04-20 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + + +#include "include/PyBaseProxyHandler.hh" + +#include + +#include + + +PyObject *idToKey(JSContext *cx, JS::HandleId id) { + JS::RootedValue idv(cx, js::IdToValue(id)); + JS::RootedString idStr(cx); + if (!id.isSymbol()) { // `JS::ToString` returns `nullptr` for JS symbols + idStr = JS::ToString(cx, idv); + } else { + // TODO (Tom Tang): Revisit this once we have Symbol coercion support + // FIXME (Tom Tang): key collision for symbols without a description string, or pure strings look like "Symbol(xxx)" + idStr = JS_ValueToSource(cx, idv); + } + + // We convert all types of property keys to string + auto chars = JS_EncodeStringToUTF8(cx, idStr); + return PyUnicode_FromString(chars.get()); +} + +bool idToIndex(JSContext *cx, JS::HandleId id, Py_ssize_t *index) { + if (id.isInt()) { // int-like strings have already been automatically converted to ints + *index = id.toInt(); + return true; + } else { + return false; // fail + } +} + +bool PyBaseProxyHandler::getPrototypeIfOrdinary(JSContext *cx, JS::HandleObject proxy, + bool *isOrdinary, + JS::MutableHandleObject protop) const { + // We don't have a custom [[GetPrototypeOf]] + *isOrdinary = true; + protop.set(js::GetStaticPrototype(proxy)); + return true; +} + +bool PyBaseProxyHandler::preventExtensions(JSContext *cx, JS::HandleObject proxy, + JS::ObjectOpResult &result) const { + result.succeed(); + return true; +} + +bool PyBaseProxyHandler::isExtensible(JSContext *cx, JS::HandleObject proxy, + bool *extensible) const { + *extensible = false; + return true; +} \ No newline at end of file diff --git a/src/PyBytesProxyHandler.cc b/src/PyBytesProxyHandler.cc new file mode 100644 index 00000000..304c37c9 --- /dev/null +++ b/src/PyBytesProxyHandler.cc @@ -0,0 +1,430 @@ +/** + * @file PyBytesProxyHandler.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS Uint8Array-like proxy objects for immutable bytes objects + * @date 2024-07-23 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + + +#include "include/PyBytesProxyHandler.hh" + +#include +#include + + +const char PyBytesProxyHandler::family = 0; + + +static bool array_valueOf(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(proxy, OtherSlot); + JS::RootedObject rootedArrayBuffer(cx, arrayBuffer->get()); + + auto byteLength = JS::GetArrayBufferByteLength(rootedArrayBuffer); + + bool isSharedMemory; + JS::AutoCheckCannotGC autoNoGC(cx); + uint8_t *data = JS::GetArrayBufferData(rootedArrayBuffer, &isSharedMemory, autoNoGC); + + size_t numberOfDigits = 0; + for (size_t i = 0; i < byteLength; i++) { + numberOfDigits += data[i] < 10 ? 1 : data[i] < 100 ? 2 : 3; + } + + const size_t STRING_LENGTH = byteLength + numberOfDigits; + JS::Latin1Char *buffer = (JS::Latin1Char *)malloc(sizeof(JS::Latin1Char) * STRING_LENGTH); + + if (snprintf((char *)&buffer[0], 3 + 1, "%hu", data[0]) < 0) { + return false; + } + size_t charIndex = data[0] < 10 ? 1 : data[0] < 100 ? 2 : 3; + + for (size_t dataIndex = 1; dataIndex < byteLength; dataIndex++) { + buffer[charIndex] = ','; + charIndex++; + if (snprintf((char *)&buffer[charIndex], 3 + 1, "%hu", data[dataIndex]) < 0) { + return false; + } + charIndex += data[dataIndex] < 10 ? 1 : data[dataIndex] < 100 ? 2 : 3; + } + + JS::UniqueLatin1Chars str(buffer); + args.rval().setString(JS_NewLatin1String(cx, std::move(str), STRING_LENGTH - 1)); // don't include the null terminating byte + return true; +} + +static bool array_toString(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_valueOf(cx, argc, vp); +} + + +// BytesIterator + + +#define ITEM_KIND_KEY 0 +#define ITEM_KIND_VALUE 1 +#define ITEM_KIND_KEY_AND_VALUE 2 + +enum { + BytesIteratorSlotIteratedObject, + BytesIteratorSlotNextIndex, + BytesIteratorSlotItemKind, + BytesIteratorSlotCount +}; + +static JSClass bytesIteratorClass = {"BytesIterator", JSCLASS_HAS_RESERVED_SLOTS(BytesIteratorSlotCount)}; + +static bool iterator_next(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JS::RootedObject thisObj(cx); + if (!args.computeThis(cx, &thisObj)) return false; + + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(thisObj, BytesIteratorSlotIteratedObject); + JS::RootedObject rootedArrayBuffer(cx, arrayBuffer->get()); + + JS::RootedValue rootedNextIndex(cx, JS::GetReservedSlot(thisObj, BytesIteratorSlotNextIndex)); + JS::RootedValue rootedItemKind(cx, JS::GetReservedSlot(thisObj, BytesIteratorSlotItemKind)); + + int32_t nextIndex; + int32_t itemKind; + if (!JS::ToInt32(cx, rootedNextIndex, &nextIndex) || !JS::ToInt32(cx, rootedItemKind, &itemKind)) return false; + + JS::RootedObject result(cx, JS_NewPlainObject(cx)); + + Py_ssize_t len = JS::GetArrayBufferByteLength(rootedArrayBuffer); + + if (nextIndex >= len) { + // UnsafeSetReservedSlot(obj, ITERATOR_SLOT_TARGET, null); // TODO lose ref + JS::RootedValue done(cx, JS::BooleanValue(true)); + if (!JS_SetProperty(cx, result, "done", done)) return false; + args.rval().setObject(*result); + return result; + } + + JS::SetReservedSlot(thisObj, BytesIteratorSlotNextIndex, JS::Int32Value(nextIndex + 1)); + + JS::RootedValue done(cx, JS::BooleanValue(false)); + if (!JS_SetProperty(cx, result, "done", done)) return false; + + if (itemKind == ITEM_KIND_VALUE) { + bool isSharedMemory; + JS::AutoCheckCannotGC autoNoGC(cx); + uint8_t *data = JS::GetArrayBufferData(rootedArrayBuffer, &isSharedMemory, autoNoGC); + + JS::RootedValue value(cx, JS::Int32Value(data[nextIndex])); + if (!JS_SetProperty(cx, result, "value", value)) return false; + } + else if (itemKind == ITEM_KIND_KEY_AND_VALUE) { + JS::Rooted> items(cx); + + JS::RootedValue rootedNextIndex(cx, JS::Int32Value(nextIndex)); + items[0].set(rootedNextIndex); + + bool isSharedMemory; + JS::AutoCheckCannotGC autoNoGC(cx); + uint8_t *data = JS::GetArrayBufferData(rootedArrayBuffer, &isSharedMemory, autoNoGC); + + JS::RootedValue value(cx, JS::Int32Value(data[nextIndex])); + items[1].set(value); + + JS::RootedValue pair(cx); + JSObject *array = JS::NewArrayObject(cx, items); + pair.setObject(*array); + if (!JS_SetProperty(cx, result, "value", pair)) return false; + } + else { // itemKind == ITEM_KIND_KEY + JS::RootedValue value(cx, JS::Int32Value(nextIndex)); + if (!JS_SetProperty(cx, result, "value", value)) return false; + } + + args.rval().setObject(*result); + return true; +} + +static JSFunctionSpec bytes_iterator_methods[] = { + JS_FN("next", iterator_next, 0, JSPROP_ENUMERATE), + JS_FS_END +}; + +static bool BytesIteratorConstructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.isConstructing()) { + JS_ReportErrorASCII(cx, "You must call this constructor with 'new'"); + return false; + } + + JS::RootedObject thisObj(cx, JS_NewObjectForConstructor(cx, &bytesIteratorClass, args)); + if (!thisObj) { + return false; + } + + args.rval().setObject(*thisObj); + return true; +} + +static bool DefineBytesIterator(JSContext *cx, JS::HandleObject global) { + JS::RootedObject iteratorPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Iterator, &iteratorPrototype)) { + return false; + } + + JS::RootedObject protoObj(cx, + JS_InitClass(cx, global, + nullptr, iteratorPrototype, + "BytesIterator", + BytesIteratorConstructor, 0, + nullptr, bytes_iterator_methods, + nullptr, nullptr) + ); + + return protoObj; // != nullptr +} + +/// private util +static bool array_iterator_func(JSContext *cx, unsigned argc, JS::Value *vp, int itemKind) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + + JS::RootedObject global(cx, JS::GetNonCCWObjectGlobal(proxy)); + + JS::RootedValue constructor_val(cx); + if (!JS_GetProperty(cx, global, "BytesIterator", &constructor_val)) return false; + if (!constructor_val.isObject()) { + if (!DefineBytesIterator(cx, global)) { + return false; + } + + if (!JS_GetProperty(cx, global, "BytesIterator", &constructor_val)) return false; + if (!constructor_val.isObject()) { + JS_ReportErrorASCII(cx, "BytesIterator is not a constructor"); + return false; + } + } + JS::RootedObject constructor(cx, &constructor_val.toObject()); + + JS::RootedObject obj(cx); + if (!JS::Construct(cx, constructor_val, JS::HandleValueArray::empty(), &obj)) return false; + if (!obj) return false; + + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(proxy, OtherSlot); + + JS::SetReservedSlot(obj, BytesIteratorSlotIteratedObject, JS::PrivateValue(arrayBuffer)); + JS::SetReservedSlot(obj, BytesIteratorSlotNextIndex, JS::Int32Value(0)); + JS::SetReservedSlot(obj, BytesIteratorSlotItemKind, JS::Int32Value(itemKind)); + + args.rval().setObject(*obj); + return true; +} + +static bool array_entries(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_iterator_func(cx, argc, vp, ITEM_KIND_KEY_AND_VALUE); +} + +static bool array_keys(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_iterator_func(cx, argc, vp, ITEM_KIND_KEY); +} + +static bool array_values(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_iterator_func(cx, argc, vp, ITEM_KIND_VALUE); +} + + +static JSMethodDef array_methods[] = { + {"toString", array_toString, 0}, + {"valueOf", array_valueOf, 0}, + {"entries", array_entries, 0}, + {"keys", array_keys, 0}, + {"values", array_values, 0}, + {NULL, NULL, 0} +}; + + +bool PyBytesProxyHandler::set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::HandleValue v, JS::HandleValue receiver, + JS::ObjectOpResult &result) const { + + // block all modifications + + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + PyErr_Format(PyExc_TypeError, + "'%.100s' object has only read-only attributes", + Py_TYPE(self)->tp_name); + + return result.failReadOnly(); +} + +bool PyBytesProxyHandler::getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc +) const { + // see if we're calling a function + if (id.isString()) { + for (size_t index = 0;; index++) { + bool isThatFunction; + const char *methodName = array_methods[index].name; + if (methodName == NULL) { + break; + } + else if (JS_StringEqualsAscii(cx, id.toString(), methodName, &isThatFunction) && isThatFunction) { + JSFunction *newFunction = JS_NewFunction(cx, array_methods[index].call, array_methods[index].nargs, 0, NULL); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + } + } + + if (id.isString()) { + bool isProperty; + + JSString *idString = id.toString(); + + // "length" and "byteLength" properties have the same value + if ((JS_StringEqualsLiteral(cx, idString, "length", &isProperty) && isProperty) || (JS_StringEqualsLiteral(cx, id.toString(), "byteLength", &isProperty) && isProperty)) { + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(proxy, OtherSlot); + + JS::RootedObject rootedArrayBuffer(cx, arrayBuffer->get()); + + auto byteLength = JS::GetArrayBufferByteLength(rootedArrayBuffer); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::Int32Value(byteLength) + ) + )); + return true; + } + + // "buffer" property + if (JS_StringEqualsLiteral(cx, idString, "buffer", &isProperty) && isProperty) { + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(proxy, OtherSlot); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*(arrayBuffer->get())) + ) + )); + return true; + } + + // "BYTES_PER_ELEMENT" property + if (JS_StringEqualsLiteral(cx, idString, "BYTES_PER_ELEMENT", &isProperty) && isProperty) { + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::Int32Value(1) + ) + )); + return true; + } + + // "byteOffset" property + if (JS_StringEqualsLiteral(cx, idString, "byteOffset", &isProperty) && isProperty) { + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::Int32Value(0) + ) + )); + return true; + } + + // "constructor" property + if (JS_StringEqualsLiteral(cx, idString, "constructor", &isProperty) && isProperty) { + JS::RootedObject uint8ArrayPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Uint8Array, &uint8ArrayPrototype)) { + return false; + } + + JS::RootedValue Uint8Array_Prototype_Constructor(cx); + if (!JS_GetProperty(cx, uint8ArrayPrototype, "constructor", &Uint8Array_Prototype_Constructor)) { + return false; + } + + JS::RootedObject rootedUint8ArrayPrototypeConstructor(cx, Uint8Array_Prototype_Constructor.toObjectOrNull()); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*rootedUint8ArrayPrototypeConstructor), + {JS::PropertyAttribute::Enumerable} + ) + )); + + return true; + } + } + + if (id.isSymbol()) { + JS::RootedSymbol rootedSymbol(cx, id.toSymbol()); + + if (JS::GetSymbolCode(rootedSymbol) == JS::SymbolCode::iterator) { + JSFunction *newFunction = JS_NewFunction(cx, array_values, 0, 0, NULL); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + } else { + desc.set(mozilla::Nothing()); + } + + return true; + } + + // item + Py_ssize_t index; + if (idToIndex(cx, id, &index)) { + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(proxy, OtherSlot); + JS::RootedObject rootedArrayBuffer(cx, arrayBuffer->get()); + + bool isSharedMemory; + JS::AutoCheckCannotGC autoNoGC(cx); + uint8_t *data = JS::GetArrayBufferData(rootedArrayBuffer, &isSharedMemory, autoNoGC); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::Int32Value(data[index]) + ) + )); + + return true; + } + + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *item = PyObject_GetAttr(self, attrName); + if (!item && PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); // clear error, we will be returning undefined in this case + } + + return handleGetOwnPropertyDescriptor(cx, id, desc, item); +} + +void PyBytesProxyHandler::finalize(JS::GCContext *gcx, JSObject *proxy) const { + PyObjectProxyHandler::finalize(gcx, proxy); + + JS::PersistentRootedObject *arrayBuffer = JS::GetMaybePtrFromReservedSlot(proxy, OtherSlot); + delete arrayBuffer; +} \ No newline at end of file diff --git a/src/PyDictProxyHandler.cc b/src/PyDictProxyHandler.cc new file mode 100644 index 00000000..4295d2c4 --- /dev/null +++ b/src/PyDictProxyHandler.cc @@ -0,0 +1,111 @@ +/** + * @file PyDictProxyHandler.cc + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects for Dicts + * @date 2023-04-20 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + +#include "include/PyDictProxyHandler.hh" + +#include "include/jsTypeFactory.hh" +#include "include/pyTypeFactory.hh" + +#include +#include +#include +#include +#include +#include + +#include + + + +const char PyDictProxyHandler::family = 0; + +bool PyDictProxyHandler::ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const { + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *keys = PyDict_Keys(self); + + size_t length = PyList_Size(keys); + + return handleOwnPropertyKeys(cx, keys, length, props); +} + +bool PyDictProxyHandler::delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::ObjectOpResult &result) const { + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + if (PyDict_DelItem(self, attrName) < 0) { + return result.failCantDelete(); // raises JS exception + } + return result.succeed(); +} + +bool PyDictProxyHandler::has(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + bool *bp) const { + return hasOwn(cx, proxy, id, bp); +} + +bool PyDictProxyHandler::getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc +) const { + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *item = PyDict_GetItemWithError(self, attrName); // returns NULL without an exception set if the key wasn’t present. + + return handleGetOwnPropertyDescriptor(cx, id, desc, item); +} + +bool PyDictProxyHandler::set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::HandleValue v, JS::HandleValue receiver, + JS::ObjectOpResult &result) const { + JS::RootedValue rootedV(cx, v); + PyObject *attrName = idToKey(cx, id); + + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *value = pyTypeFactory(cx, rootedV); + if (PyDict_SetItem(self, attrName, value)) { + Py_DECREF(value); + return result.failCantSetInterposed(); // raises JS exception + } + Py_DECREF(value); + return result.succeed(); +} + +bool PyDictProxyHandler::enumerate(JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const { + return this->ownPropertyKeys(cx, proxy, props); +} + +bool PyDictProxyHandler::hasOwn(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + bool *bp) const { + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + *bp = PyDict_Contains(self, attrName) == 1; + return true; +} + +bool PyDictProxyHandler::getOwnEnumerablePropertyKeys( + JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const { + return this->ownPropertyKeys(cx, proxy, props); +} + +bool PyDictProxyHandler::defineProperty(JSContext *cx, JS::HandleObject proxy, + JS::HandleId id, + JS::Handle desc, + JS::ObjectOpResult &result) const { + // Block direct `Object.defineProperty` since we already have the `set` method + return result.failInvalidDescriptor(); +} + +bool PyDictProxyHandler::getBuiltinClass(JSContext *cx, JS::HandleObject proxy, + js::ESClass *cls) const { + *cls = js::ESClass::Object; + return true; +} \ No newline at end of file diff --git a/src/PyEventLoop.cc b/src/PyEventLoop.cc index d5b7f0a9..14b6f003 100644 --- a/src/PyEventLoop.cc +++ b/src/PyEventLoop.cc @@ -1,3 +1,14 @@ +/** + * @file PyEventLoop.cc + * @author Tom Tang (xmader@distributive.network) + * @brief Send jobs to the Python event-loop + * @date 2023-04-05 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + + #include "include/PyEventLoop.hh" #include @@ -6,36 +17,89 @@ * @brief Wrapper to decrement the counter of queueing event-loop jobs after the job finishes */ static PyObject *eventLoopJobWrapper(PyObject *jobFn, PyObject *Py_UNUSED(_)) { - PyObject *ret = PyObject_CallObject(jobFn, NULL); // jobFn() + PyObject *ret = PyObject_CallObject(jobFn, NULL); Py_XDECREF(ret); // don't care about its return value + + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); // Protects `decCounter()`. If the error indicator is set, Python cannot make further function calls. PyEventLoop::_locker->decCounter(); + PyErr_Restore(type, value, traceback); + if (PyErr_Occurred()) { return NULL; } else { Py_RETURN_NONE; } } -static PyMethodDef jobWrapperDef = {"eventLoopJobWrapper", eventLoopJobWrapper, METH_NOARGS, NULL}; +static PyMethodDef loopJobWrapperDef = {"eventLoopJobWrapper", eventLoopJobWrapper, METH_NOARGS, NULL}; + +static PyObject *_enqueueWithDelay(PyObject *_loop, PyEventLoop::AsyncHandle::id_t handleId, PyObject *jobFn, double delaySeconds, bool repeat); + +/** + * @brief Wrapper to remove the reference of the timer after the job finishes + */ +static PyObject *timerJobWrapper(PyObject *jobFn, PyObject *args) { + PyObject *_loop = PyTuple_GetItem(args, 0); + PyEventLoop::AsyncHandle::id_t handleId = PyLong_AsLong(PyTuple_GetItem(args, 1)); + double delaySeconds = PyFloat_AsDouble(PyTuple_GetItem(args, 2)); + bool repeat = (bool)PyLong_AsLong(PyTuple_GetItem(args, 3)); + + PyObject *ret = PyObject_CallObject(jobFn, NULL); // jobFn() + Py_XDECREF(ret); // don't care about its return value + + PyObject *errType, *errValue, *traceback; // we can't call any Python code unless the error indicator is clear + PyErr_Fetch(&errType, &errValue, &traceback); + // Making sure a `AsyncHandle::fromId` call is close to its `handle`'s use. + // We need to ensure the memory block doesn't move for reallocation before we can use the pointer, + // as we could have multiple new `setTimeout` calls to expand the `_timeoutIdMap` vector while running the job function in parallel. + auto handle = PyEventLoop::AsyncHandle::fromId(handleId); + if (repeat && !handle->cancelled()) { + _enqueueWithDelay(_loop, handleId, jobFn, delaySeconds, repeat); + } else { + handle->removeRef(); + } + + if (errType != NULL) { // PyErr_Occurred() + PyErr_Restore(errType, errValue, traceback); + return NULL; + } else { + Py_RETURN_NONE; + } +} +static PyMethodDef timerJobWrapperDef = {"timerJobWrapper", timerJobWrapper, METH_VARARGS, NULL}; PyEventLoop::AsyncHandle PyEventLoop::enqueue(PyObject *jobFn) { PyEventLoop::_locker->incCounter(); - PyObject *wrapper = PyCFunction_New(&jobWrapperDef, jobFn); + PyObject *wrapper = PyCFunction_New(&loopJobWrapperDef, jobFn); // Enqueue job to the Python event-loop // https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_soon - PyObject *asyncHandle = PyObject_CallMethod(_loop, "call_soon_threadsafe", "O", wrapper); // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue + PyObject *asyncHandle = PyObject_CallMethod(_loop, "call_soon_threadsafe", "O", wrapper); return PyEventLoop::AsyncHandle(asyncHandle); } -PyEventLoop::AsyncHandle PyEventLoop::enqueueWithDelay(PyObject *jobFn, double delaySeconds) { - PyEventLoop::_locker->incCounter(); - PyObject *wrapper = PyCFunction_New(&jobWrapperDef, jobFn); +static PyObject *_enqueueWithDelay(PyObject *_loop, PyEventLoop::AsyncHandle::id_t handleId, PyObject *jobFn, double delaySeconds, bool repeat) { + PyObject *wrapper = PyCFunction_New(&timerJobWrapperDef, jobFn); // Schedule job to the Python event-loop // https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_later - PyObject *asyncHandle = PyObject_CallMethod(_loop, "call_later", "dO", delaySeconds, wrapper); // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue - if (asyncHandle == nullptr) { + PyObject *asyncHandle = PyObject_CallMethod(_loop, "call_later", "dOOIdb", delaySeconds, wrapper, _loop, handleId, delaySeconds, repeat); // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue + if (!asyncHandle) { + return nullptr; // RuntimeError + } + + auto handle = PyEventLoop::AsyncHandle::fromId(handleId); + Py_XDECREF(handle->swap(asyncHandle)); + + return asyncHandle; +} + +PyEventLoop::AsyncHandle::id_t PyEventLoop::enqueueWithDelay(PyObject *jobFn, double delaySeconds, bool repeat) { + auto handleId = PyEventLoop::AsyncHandle::newEmpty(); + if (!_enqueueWithDelay(_loop, handleId, jobFn, delaySeconds, repeat)) { PyErr_Print(); // RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one } - return PyEventLoop::AsyncHandle(asyncHandle); + auto handle = PyEventLoop::AsyncHandle::fromId(handleId); + handle->addRef(); + return handleId; } PyEventLoop::Future PyEventLoop::createFuture() { @@ -63,6 +127,7 @@ PyEventLoop::Future PyEventLoop::ensureFuture(PyObject *awaitable) { Py_DECREF(args); Py_DECREF(kwargs); + Py_INCREF(futureObj); // needs to be kept alive as `PyEventLoop::Future` will decrease the reference count in its destructor return PyEventLoop::Future(futureObj); } @@ -75,10 +140,44 @@ PyEventLoop PyEventLoop::_loopNotFound() { /* static */ PyEventLoop PyEventLoop::_getLoopOnThread(PyThreadState *tstate) { // Modified from Python 3.9 `get_running_loop` https://github.com/python/cpython/blob/7cb3a44/Modules/_asynciomodule.c#L241-L278 - #if PY_VERSION_HEX >= 0x03090000 // Python version is greater than 3.9 - PyObject *ts_dict = _PyThreadState_GetDict(tstate); // borrowed reference + + PyObject *ts_dict; + #if PY_VERSION_HEX >= 0x030d0000 // Python version is greater than 3.13 + // The private `_PyThreadState_GetDict(tstate)` API gets removed in Python 3.13. + // However, simply replacing it with `PyThreadState_GetDict()` does not work, + // since the public `PyThreadState_GetDict()` API can only get from the current thread. + // We need to somehow get the thread dictionary on the main thread instead of the current thread. + // + // UPDATE: We don't need the thread dictionary anymore. + // To get the thread's running event-loop in Python 3.13 is as simple as `thread_state->asyncio_running_loop` + { + // Every `PyThreadState` is actually allocated with extra fields as a `_PyThreadStateImpl` struct + // See https://github.com/python/cpython/blob/v3.13.0rc1/Include/internal/pycore_tstate.h#L17-L24 + using PyThreadStateHolder = struct { // _PyThreadStateImpl + PyThreadState base; + #if PY_VERSION_HEX >= 0x030e0000 // Python version is greater than 3.14 + // the struct is changed with more additional fields, see https://github.com/python/cpython/blob/v3.14.0rc3/Include/internal/pycore_tstate.h#L24-L40 + Py_ssize_t refcount; + uintptr_t c_stack_top; + uintptr_t c_stack_soft_limit; + uintptr_t c_stack_hard_limit; + #endif + PyObject *asyncio_running_loop; // we only need the first few fields of `_PyThreadStateImpl` + }; + + // Modified from https://github.com/python/cpython/blob/v3.13.0rc1/Modules/_asynciomodule.c#L3205-L3210 + PyObject *loop = ((PyThreadStateHolder *)tstate)->asyncio_running_loop; + if (loop == NULL) { + return _loopNotFound(); + } + + Py_INCREF(loop); + return PyEventLoop(loop); + } + #elif PY_VERSION_HEX >= 0x03090000 // Python version is greater than 3.9 + ts_dict = _PyThreadState_GetDict(tstate); // borrowed reference #else // Python 3.8 - PyObject *ts_dict = tstate->dict; // see https://github.com/python/cpython/blob/v3.8.17/Modules/_asynciomodule.c#L244-L245 + ts_dict = tstate->dict; // see https://github.com/python/cpython/blob/v3.8.17/Modules/_asynciomodule.c#L244-L245 #endif if (ts_dict == NULL) { return _loopNotFound(); @@ -116,7 +215,7 @@ PyThreadState *PyEventLoop::_getMainThread() { // The last element in the linked-list of threads associated with the main interpreter should be the main thread // (The first element is the current thread, see https://github.com/python/cpython/blob/7cb3a44/Python/pystate.c#L291-L293) PyInterpreterState *interp = PyInterpreterState_Main(); - PyThreadState *tstate = PyInterpreterState_ThreadHead(interp); // https://docs.python.org/3/c-api/init.html#c.PyInterpreterState_ThreadHead + PyThreadState *tstate = PyInterpreterState_ThreadHead(interp); while (PyThreadState_Next(tstate) != nullptr) { tstate = PyThreadState_Next(tstate); } @@ -127,7 +226,7 @@ PyThreadState *PyEventLoop::_getMainThread() { PyThreadState *PyEventLoop::_getCurrentThread() { // `PyThreadState_Get` is used under the hood of the Python `asyncio.get_running_loop` method, // see https://github.com/python/cpython/blob/7cb3a44/Modules/_asynciomodule.c#L234 - return PyThreadState_Get(); // https://docs.python.org/3/c-api/init.html#c.PyThreadState_Get + return PyThreadState_Get(); } /* static */ @@ -141,19 +240,39 @@ PyEventLoop PyEventLoop::getRunningLoop() { } void PyEventLoop::AsyncHandle::cancel() { - PyObject *scheduled = PyObject_GetAttrString(_handle, "_scheduled"); // this attribute only exists on asyncio.TimerHandle returned by loop.call_later - // NULL if no such attribute (on a strict asyncio.Handle returned by loop.call_soon) - bool finishedOrCanceled = scheduled && scheduled == Py_False; // the job function has already been executed or canceled - if (!finishedOrCanceled) { - PyEventLoop::_locker->decCounter(); + if (!_finishedOrCancelled()) { + removeRef(); // automatically unref at finish } - Py_XDECREF(scheduled); // https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Handle.cancel PyObject *ret = PyObject_CallMethod(_handle, "cancel", NULL); // returns None Py_XDECREF(ret); } +/* static */ +bool PyEventLoop::AsyncHandle::cancelAll() { + for (AsyncHandle &handle: _timeoutIdMap) { + handle.cancel(); + } + return true; +} + +bool PyEventLoop::AsyncHandle::cancelled() { + // https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Handle.cancelled + PyObject *ret = PyObject_CallMethod(_handle, "cancelled", NULL); // returns Python bool + bool cancelled = ret == Py_True; + Py_XDECREF(ret); + return cancelled; +} + +bool PyEventLoop::AsyncHandle::_finishedOrCancelled() { + PyObject *scheduled = PyObject_GetAttrString(_handle, "_scheduled"); // this attribute only exists on asyncio.TimerHandle returned by loop.call_later + // NULL if no such attribute (on a strict asyncio.Handle returned by loop.call_soon) + bool notScheduled = scheduled && scheduled == Py_False; // not scheduled means the job function has already been executed or canceled + Py_XDECREF(scheduled); + return notScheduled; +} + void PyEventLoop::Future::setResult(PyObject *result) { // https://docs.python.org/3/library/asyncio-future.html#asyncio.Future.set_result PyObject *ret = PyObject_CallMethod(_future, "set_result", "O", result); // returns None diff --git a/src/PyIterableProxyHandler.cc b/src/PyIterableProxyHandler.cc new file mode 100644 index 00000000..0a95603f --- /dev/null +++ b/src/PyIterableProxyHandler.cc @@ -0,0 +1,301 @@ +/** + * @file PyIterableProxyHandler.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects for Iterables + * @date 2024-04-08 + * + * @copyright Copyright (c) 2024 Distributive Corp. + * + */ + + +#include "include/PyIterableProxyHandler.hh" + +#include "include/jsTypeFactory.hh" + +#include + +#include + + + +const char PyIterableProxyHandler::family = 0; + + +static bool iter_next(JSContext *cx, JS::CallArgs args, PyObject *it) { + JS::RootedObject result(cx, JS_NewPlainObject(cx)); + + PyObject *(*iternext)(PyObject *) = *Py_TYPE(it)->tp_iternext; + + PyObject *item = iternext(it); + + if (item == NULL) { + if (PyErr_Occurred()) { + if (PyErr_ExceptionMatches(PyExc_StopIteration) || + PyErr_ExceptionMatches(PyExc_SystemError)) { // TODO this handles a result like SystemError: Objects/dictobject.c:1778: bad argument to internal function. Why are we getting that? + PyErr_Clear(); + } + else { + return false; + } + } + + JS::RootedValue done(cx, JS::BooleanValue(true)); + if (!JS_SetProperty(cx, result, "done", done)) return false; + args.rval().setObject(*result); + return result; + } + + JS::RootedValue done(cx, JS::BooleanValue(false)); + if (!JS_SetProperty(cx, result, "done", done)) return false; + + JS::RootedValue value(cx, jsTypeFactory(cx, item)); + if (!JS_SetProperty(cx, result, "value", value)) return false; + + args.rval().setObject(*result); + return true; +} + +static bool iterable_next(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JS::RootedObject thisObj(cx); + if (!args.computeThis(cx, &thisObj)) return false; + + PyObject *it = JS::GetMaybePtrFromReservedSlot(thisObj, PyObjectSlot); + + return iter_next(cx, args, it); +} + +static bool toPrimitive(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + _PyUnicodeWriter writer; + + _PyUnicodeWriter_Init(&writer); + + PyObject *s = PyObject_Repr(self); + + if (s == nullptr) { + args.rval().setString(JS_NewStringCopyZ(cx, "")); + return true; + } + + int res = _PyUnicodeWriter_WriteStr(&writer, s); + Py_DECREF(s); + + if (res < 0) { + args.rval().setString(JS_NewStringCopyZ(cx, "")); + return true; + } + + PyObject *repr = _PyUnicodeWriter_Finish(&writer); + + args.rval().set(jsTypeFactory(cx, repr)); + return true; +} + +static bool iterable_valueOf(JSContext *cx, unsigned argc, JS::Value *vp) { + return toPrimitive(cx, argc, vp); +} + +static JSMethodDef iterable_methods[] = { + {"next", iterable_next, 0}, + {"valueOf", iterable_valueOf, 0}, + {NULL, NULL, 0} +}; + + +// IterableIterator + +enum { + IterableIteratorSlotIterableObject, + IterableIteratorSlotCount +}; + +static JSClass iterableIteratorClass = {"IterableIterator", JSCLASS_HAS_RESERVED_SLOTS(IterableIteratorSlotCount)}; + +static bool iterator_next(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JS::RootedObject thisObj(cx); + if (!args.computeThis(cx, &thisObj)) return false; + + PyObject *it = JS::GetMaybePtrFromReservedSlot(thisObj, IterableIteratorSlotIterableObject); + + return iter_next(cx, args, it); +} + +static JSFunctionSpec iterable_iterator_methods[] = { + JS_FN("next", iterator_next, 0, JSPROP_ENUMERATE), + JS_FS_END +}; + +static bool IterableIteratorConstructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.isConstructing()) { + JS_ReportErrorASCII(cx, "You must call this constructor with 'new'"); + return false; + } + + JS::RootedObject thisObj(cx, JS_NewObjectForConstructor(cx, &iterableIteratorClass, args)); + if (!thisObj) { + return false; + } + + args.rval().setObject(*thisObj); + return true; +} + +static bool DefineIterableIterator(JSContext *cx, JS::HandleObject global) { + JS::RootedObject iteratorPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Iterator, &iteratorPrototype)) { + return false; + } + + JS::RootedObject protoObj(cx, + JS_InitClass(cx, global, + nullptr, iteratorPrototype, + "IterableIterator", + IterableIteratorConstructor, 0, + nullptr, iterable_iterator_methods, + nullptr, nullptr) + ); + + return protoObj; // != nullptr +} + +static bool iterable_values(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::RootedObject global(cx, JS::GetNonCCWObjectGlobal(proxy)); + + JS::RootedValue constructor_val(cx); + if (!JS_GetProperty(cx, global, "IterableIterator", &constructor_val)) return false; + if (!constructor_val.isObject()) { + if (!DefineIterableIterator(cx, global)) { + return false; + } + + if (!JS_GetProperty(cx, global, "IterableIterator", &constructor_val)) return false; + if (!constructor_val.isObject()) { + JS_ReportErrorASCII(cx, "IterableIterator is not a constructor"); + return false; + } + } + JS::RootedObject constructor(cx, &constructor_val.toObject()); + + JS::RootedObject obj(cx); + if (!JS::Construct(cx, constructor_val, JS::HandleValueArray::empty(), &obj)) return false; + if (!obj) return false; + + JS::SetReservedSlot(obj, IterableIteratorSlotIterableObject, JS::PrivateValue((void *)self)); + + args.rval().setObject(*obj); + return true; +} + +bool PyIterableProxyHandler::getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc +) const { + // see if we're calling a function + if (id.isString()) { + for (size_t index = 0;; index++) { + bool isThatFunction; + const char *methodName = iterable_methods[index].name; + if (methodName == NULL) { + break; + } + else if (JS_StringEqualsAscii(cx, id.toString(), methodName, &isThatFunction) && isThatFunction) { + JSFunction *newFunction = JS_NewFunction(cx, iterable_methods[index].call, iterable_methods[index].nargs, 0, NULL); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + } + } + + // "constructor" property + bool isConstructorProperty; + if (id.isString() && JS_StringEqualsLiteral(cx, id.toString(), "constructor", &isConstructorProperty) && isConstructorProperty) { + JS::RootedObject rootedObjectPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Object, &rootedObjectPrototype)) { + return false; + } + + JS::RootedValue Object_Prototype_Constructor(cx); + if (!JS_GetProperty(cx, rootedObjectPrototype, "constructor", &Object_Prototype_Constructor)) { + return false; + } + + JS::RootedObject rootedObjectPrototypeConstructor(cx, Object_Prototype_Constructor.toObjectOrNull()); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*rootedObjectPrototypeConstructor), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + + // symbol property + if (id.isSymbol()) { + JS::RootedSymbol rootedSymbol(cx, id.toSymbol()); + JS::SymbolCode symbolCode = JS::GetSymbolCode(rootedSymbol); + + if (symbolCode == JS::SymbolCode::iterator) { + JSFunction *newFunction = JS_NewFunction(cx, iterable_values, 0, 0, NULL); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + else if (symbolCode == JS::SymbolCode::toPrimitive) { + JSFunction *newFunction = JS_NewFunction(cx, toPrimitive, 0, 0, nullptr); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + } + + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *item = PyObject_GetAttr(self, attrName); + if (!item && PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); // clear error, we will be returning undefined in this case + } + + return handleGetOwnPropertyDescriptor(cx, id, desc, item); +} \ No newline at end of file diff --git a/src/PyListProxyHandler.cc b/src/PyListProxyHandler.cc new file mode 100644 index 00000000..72da8d4e --- /dev/null +++ b/src/PyListProxyHandler.cc @@ -0,0 +1,2176 @@ +/** + * @file PyListProxyHandler.cc + * @author Philippe Laporte (philippe@distributive.network) + * @brief Struct for creating JS proxy objects for Lists + * @date 2023-12-01 + * + * @copyright Copyright (c) 2023-2024 Distributive Corp. + * + */ + +#include "include/PyListProxyHandler.hh" +#include "include/PyBaseProxyHandler.hh" + +#include "include/jsTypeFactory.hh" +#include "include/JSArrayProxy.hh" +#include "include/JSFunctionProxy.hh" +#include "include/pyTypeFactory.hh" + +#include +#include +#include +#include +#include +#include + +#include +#include "include/pyshim.hh" + + +const char PyListProxyHandler::family = 0; + +// private util +// if function is a proxy for a python method, mutate it into a new python method bound to thisObject +static bool makeNewPyMethod(JSContext *cx, JS::MutableHandleValue function, JS::HandleObject thisObject) { + if (!JS_IsNativeFunction(&(function.toObject()), callPyFunc)) { + return true; // we don't need to mutate function if it is not a proxy for a python function + } + + PyObject *method = (PyObject *)js::GetFunctionNativeReserved(&(function.toObject()), 0).toPrivate(); + if (!PyMethod_Check(method)) { + PyErr_Format(PyExc_TypeError, "unbound python functions do not have a 'self' to bind"); + return false; + } + + PyObject *func = PyMethod_Function(method); + JS::RootedValue thisValue(cx); + thisValue.setObject(*thisObject); + PyObject *newSelf = pyTypeFactory(cx, thisValue); + function.set(jsTypeFactory(cx, PyMethod_New(func, newSelf))); + Py_DECREF(newSelf); + + return true; +} + +static bool array_reverse(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + if (PyList_GET_SIZE(self) > 1) { + if (PyList_Reverse(self) < 0) { + return false; + } + } + + // return ref to self + args.rval().set(jsTypeFactory(cx, self)); + return true; +} + +static bool array_pop(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + if (PyList_GET_SIZE(self) == 0) { + args.rval().setUndefined(); + return true; + } + + PyObject *result = PyObject_CallMethod(self, "pop", NULL); + + if (!result) { + PyErr_Clear(); + args.rval().setUndefined(); + return true; + } + + args.rval().set(jsTypeFactory(cx, result)); + Py_DECREF(result); + return true; +} + +static bool array_push(JSContext *cx, unsigned argc, JS::Value *vp) { // surely the function name is in there...review JSAPI examples + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + unsigned numArgs = args.length(); + JS::RootedValue elementVal(cx); + for (unsigned index = 0; index < numArgs; index++) { + elementVal.set(args[index].get()); + PyObject *value = pyTypeFactory(cx, elementVal); + if (PyList_Append(self, value) < 0) { + Py_DECREF(value); + return false; + } + Py_DECREF(value); + } + + args.rval().setInt32(PyList_GET_SIZE(self)); + return true; +} + +static bool array_shift(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfSize = PyList_GET_SIZE(self); + + if (selfSize == 0) { + args.rval().setUndefined(); + return true; + } + + PyObject *result = PyList_GetItem(self, 0); + if (!result) { + return false; + } + if (PySequence_DelItem(self, 0) < 0) { + return false; + } + + args.rval().set(jsTypeFactory(cx, result)); + return true; +} + +static bool array_unshift(JSContext *cx, unsigned argc, JS::Value *vp) { // surely the function name is in there...review JSAPI examples + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::RootedValue elementVal(cx); + for (int index = args.length() - 1; index >= 0; index--) { + elementVal.set(args[index].get()); + PyObject *value = pyTypeFactory(cx, elementVal); + if (PyList_Insert(self, 0, value) < 0) { + Py_DECREF(value); + return false; + } + Py_DECREF(value); + } + + args.rval().setInt32(PyList_GET_SIZE(self)); + return true; +} + +// private util +static inline uint64_t normalizeSliceTerm(int64_t value, uint64_t length) { + if (value < 0) { + value += length; + if (value < 0) { + return 0; + } + } + else if (double(value) > double(length)) { + return length; + } + return value; +} + +static bool array_slice(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "slice", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfLength = PyList_GET_SIZE(self); + + uint64_t start = 0; + uint64_t stop = selfLength; + if (args.length() > 0) { + int64_t d; + + if (!JS::ToInt64(cx, args[0], &d)) { + return false; + } + + start = normalizeSliceTerm(d, selfLength); + + if (args.hasDefined(1)) { + if (!JS::ToInt64(cx, args[1], &d)) { + return false; + } + + stop = normalizeSliceTerm(d, selfLength); + } + } + + PyObject *result = PyList_GetSlice(self, (Py_ssize_t)start, (Py_ssize_t)stop); + if (!result) { + return false; + } + + args.rval().set(jsTypeFactory(cx, result)); + Py_DECREF(result); + return true; +} + +static bool array_indexOf(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "indexOf", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfLength = PyList_GET_SIZE(self); + + if (selfLength == 0) { + args.rval().setInt32(-1); + return true; + } + + uint64_t start = 0; + if (args.length() > 1) { + int64_t n; + if (!JS::ToInt64(cx, args[1], &n)) { + return false; + } + + if (n >= selfLength) { + args.rval().setInt32(-1); + return true; + } + + if (n >= 0) { + start = uint64_t(n); + } + else { + int64_t d = selfLength + n; + if (d >= 0) { + start = d; + } + } + } + + JS::RootedValue elementVal(cx, args[0].get()); + PyObject *value = pyTypeFactory(cx, elementVal); + PyObject *result = PyObject_CallMethod(self, "index", "Oi", value, start); + Py_DECREF(value); + + if (!result) { + PyErr_Clear(); + args.rval().setInt32(-1); + return true; + } + + args.rval().set(jsTypeFactory(cx, result)); + Py_DECREF(result); + return true; +} + +static bool array_splice(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + uint64_t selfLength = (uint64_t)PyList_GET_SIZE(self); + + int64_t relativeStart; + if (!JS::ToInt64(cx, args.get(0), &relativeStart)) { + return false; + } + + /* actualStart is the index after which elements will be + deleted and/or new elements will be added */ + uint64_t actualStart; + if (relativeStart < 0) { + actualStart = uint64_t(std::max(double(selfLength) + relativeStart, 0.0)); + } else { + actualStart = uint64_t(std::min(double(relativeStart), double(selfLength))); + } + + unsigned int argsLength = args.length(); + + /* insertCount is the number of elements being added */ + uint32_t insertCount; + if (argsLength < 2) { + insertCount = 0; + } + else { + insertCount = argsLength - 2; + } + + /* actualDeleteCount is the number of elements being deleted */ + uint64_t actualDeleteCount; + if (argsLength < 1) { + actualDeleteCount = 0; + } + else if (argsLength < 2) { + actualDeleteCount = selfLength - actualStart; + } + else { + int64_t deleteCount; + if (!JS::ToInt64(cx, args.get(1), &deleteCount)) { + return false; + } + + actualDeleteCount = uint64_t(std::min(std::max(0.0, double(deleteCount)), double(selfLength - actualStart))); + } + + // get deleted items for return value + PyObject *deleted = PyList_GetSlice(self, actualStart, actualStart + actualDeleteCount); + if (!deleted) { + return false; + } + + // build list for SetSlice call + PyObject *inserted = PyList_New(insertCount); + if (!inserted) { + return false; + } + + JS::RootedValue elementVal(cx); + for (int index = 0; index < insertCount; index++) { + elementVal.set(args[index + 2].get()); + PyObject *value = pyTypeFactory(cx, elementVal); + if (PyList_SetItem(inserted, index, value) < 0) { + return false; + } + } + + if (PyList_SetSlice(self, actualStart, actualStart + actualDeleteCount, inserted) < 0) { + return false; + } + + args.rval().set(jsTypeFactory(cx, deleted)); + Py_DECREF(deleted); + return true; +} + +static bool array_fill(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "fill", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + uint64_t selfLength = (uint64_t)PyList_GET_SIZE(self); + + unsigned int argsLength = args.length(); + + int64_t relativeStart; + if (argsLength > 1) { + if (!JS::ToInt64(cx, args.get(1), &relativeStart)) { + return false; + } + } else { + relativeStart = 0; + } + + uint64_t actualStart; + if (relativeStart < 0) { + actualStart = uint64_t(std::max(double(selfLength) + relativeStart, 0.0)); + } else { + actualStart = uint64_t(std::min(double(relativeStart), double(selfLength))); + } + + int64_t relativeEnd; + if (argsLength > 2) { + if (!JS::ToInt64(cx, args.get(2), &relativeEnd)) { + return false; + } + } else { + relativeEnd = selfLength; + } + + uint64_t actualEnd; + if (relativeEnd < 0) { + actualEnd = uint64_t(std::max(double(selfLength) + relativeEnd, 0.0)); + } else { + actualEnd = uint64_t(std::min(double(relativeEnd), double(selfLength))); + } + + JS::RootedValue fillValue(cx, args[0].get()); + PyObject *fillValueItem = pyTypeFactory(cx, fillValue); + for (int index = actualStart; index < actualEnd; index++) { + // Since each call of `PyList_SetItem` steals a reference (even if its to the same object), + // We need multiple references to it for it to steal. + Py_INCREF(fillValueItem); + if (PyList_SetItem(self, index, fillValueItem) < 0) { + return false; + } + } + + Py_DECREF(fillValueItem); + + // return ref to self + args.rval().set(jsTypeFactory(cx, self)); + return true; +} + +static bool array_copyWithin(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + int64_t selfLength = (uint64_t)PyList_GET_SIZE(self); + + unsigned int argsLength = args.length(); + + int64_t relativeTarget; + if (argsLength > 0) { + if (!JS::ToInt64(cx, args.get(0), &relativeTarget)) { + return false; + } + } else { + relativeTarget = 0; + } + + int64_t actualTarget; + if (relativeTarget < 0) { + actualTarget = int64_t(std::max(double(selfLength) + relativeTarget, 0.0)); + } else { + actualTarget = int64_t(std::min(double(relativeTarget), double(selfLength))); + } + + int64_t relativeStart; + if (argsLength > 1) { + if (!JS::ToInt64(cx, args.get(1), &relativeStart)) { + return false; + } + } else { + relativeStart = 0; + } + + int64_t actualStart; + if (relativeStart < 0) { + actualStart = int64_t(std::max(double(selfLength) + relativeStart, 0.0)); + } else { + actualStart = int64_t(std::min(double(relativeStart), double(selfLength))); + } + + int64_t relativeEnd; + if (argsLength > 2) { + if (!JS::ToInt64(cx, args.get(2), &relativeEnd)) { + return false; + } + } else { + relativeEnd = selfLength; + } + + int64_t actualEnd; + if (relativeEnd < 0) { + actualEnd = int64_t(std::max(double(selfLength) + relativeEnd, 0.0)); + } else { + actualEnd = int64_t(std::min(double(relativeEnd), double(selfLength))); + } + + int64_t count = int64_t(std::min(actualEnd - actualStart, selfLength - actualTarget)); + + if (actualStart < actualTarget && actualTarget < actualStart + count) { + actualStart = actualStart + count - 1; + actualTarget = actualTarget + count - 1; + + while (count > 0) { + PyObject *itemStart = PyList_GetItem(self, actualStart); + if (PyList_SetItem(self, actualTarget, itemStart) < 0) { + return false; + } + + actualStart--; + actualTarget--; + count--; + } + } else { + while (count > 0) { + PyObject *itemStart = PyList_GetItem(self, actualStart); + if (PyList_SetItem(self, actualTarget, itemStart) < 0) { + return false; + } + + actualStart++; + actualTarget++; + count--; + } + } + + // return ref to self + args.rval().set(jsTypeFactory(cx, self)); + return true; +} + +static bool array_concat(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfSize = PyList_GET_SIZE(self); + PyObject *result = PyList_GetSlice(self, 0, selfSize); + + unsigned numArgs = args.length(); + JS::RootedValue elementVal(cx); + for (unsigned index = 0; index < numArgs; index++) { + elementVal.set(args[index].get()); + + PyObject *item = pyTypeFactory(cx, elementVal); + if (PyObject_TypeCheck(item, &JSArrayProxyType)) { + // flatten the array only a depth 1 + Py_ssize_t itemLength = JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)item); + for (Py_ssize_t flatIndex = 0; flatIndex < itemLength; flatIndex++) { + if (!JS_GetElement(cx, *(((JSArrayProxy *)item)->jsArray), flatIndex, &elementVal)) { + Py_DECREF(item); + return false; + } + PyObject *value = pyTypeFactory(cx, elementVal); + if (PyList_Append(result, value) < 0) { + Py_DECREF(item); + Py_DECREF(value); + return false; + } + Py_DECREF(value); + } + } + else if (PyObject_TypeCheck(item, &PyList_Type)) { + // flatten the array only at depth 1 + Py_ssize_t itemLength = PyList_GET_SIZE(item); + for (Py_ssize_t flatIndex = 0; flatIndex < itemLength; flatIndex++) { + if (PyList_Append(result, PyList_GetItem(item, flatIndex)) < 0) { + Py_DECREF(item); + return false; + } + } + } + else { + PyObject *value = pyTypeFactory(cx, elementVal); + if (PyList_Append(result, value) < 0) { + Py_DECREF(item); + Py_DECREF(value); + return false; + } + Py_DECREF(value); + } + + Py_DECREF(item); + } + + args.rval().set(jsTypeFactory(cx, result)); + Py_DECREF(result); + return true; +} + +static bool array_lastIndexOf(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "lastIndexOf", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfLength = PyList_GET_SIZE(self); + + if (selfLength == 0) { + args.rval().setInt32(-1); + return true; + } + + uint64_t start = selfLength - 1; + if (args.length() > 1) { + int64_t n; + if (!JS::ToInt64(cx, args[1], &n)) { + return false; + } + + if (n < 0) { + double d = double(selfLength) + n; + if (d < 0) { + args.rval().setInt32(-1); + return true; + } + start = uint64_t(d); + } else if (n < double(start)) { + start = uint64_t(n); + } + } + + JS::RootedValue elementVal(cx, args[0].get()); + PyObject *element = pyTypeFactory(cx, elementVal); + for (int64_t index = start; index >= 0; index--) { + PyObject *item = PyList_GetItem(self, index); + Py_INCREF(item); + int cmp = PyObject_RichCompareBool(item, element, Py_EQ); + Py_DECREF(item); + if (cmp < 0) { + Py_XDECREF(element); + return false; + } + else if (cmp == 1) { + Py_XDECREF(element); + args.rval().setInt32(index); + return true; + } + } + Py_XDECREF(element); + + args.rval().setInt32(-1); + return true; +} + +static bool array_includes(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "includes", 1)) { + return false; + } + + if (!array_indexOf(cx, argc, vp)) { + return false; + } + + args.rval().setBoolean(args.rval().get().toInt32() >= 0 ? true : false); + return true; +} + +static bool array_forEach(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "forEach", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "forEach: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + Py_ssize_t len = PyList_GET_SIZE(self); + + JS::RootedObject rootedThisArg(cx); + + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + for (Py_ssize_t index = 0; index < len; index++) { + jArgs[0].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +static bool array_map(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "map", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + // Decompiling the faulty arg is not accessible through the JSAPI so we do the best effort for the error message + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "map: callback"); + return false; + } + + Py_ssize_t len = PyList_GET_SIZE(self); + + JSObject *retArray = JS::NewArrayObject(cx, len); + JS::RootedObject rootedRetArray(cx, retArray); + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + for (Py_ssize_t index = 0; index < len; index++) { + jArgs[0].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + + JS_SetElement(cx, rootedRetArray, index, rval); + } + + args.rval().setObject(*retArray); + return true; +} + +static bool array_filter(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "filter", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "filter: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + JS::RootedValueVector retVector(cx); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + Py_ssize_t len = PyList_GET_SIZE(self); + for (Py_ssize_t index = 0, toIndex = 0; index < len; index++) { + JS::Value item = jsTypeFactory(cx, PyList_GetItem(self, index)); + jArgs[0].set(item); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + + if (rval.toBoolean()) { + if (!retVector.append(item)) { + return false; + } + } + } + + JS::HandleValueArray jsValueArray(retVector); + JSObject *retArray = JS::NewArrayObject(cx, jsValueArray); + + args.rval().setObject(*retArray); + return true; +} + +static bool array_reduce(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "reduce", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "reduce: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue *accumulator; + + Py_ssize_t len = PyList_GET_SIZE(self); + + if (args.length() > 1) { + accumulator = new JS::RootedValue(cx, args[1].get()); + } + else { + if (len == 0) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_EMPTY_ARRAY_REDUCE); + return false; + } + accumulator = new JS::RootedValue(cx, jsTypeFactory(cx, PyList_GetItem(self, 0))); + } + + for (Py_ssize_t index = args.length() > 1 ? 0 : 1; index < len; index++) { + jArgs[0].set(*accumulator); + jArgs[1].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[2].setInt32(index); + jArgs[3].set(selfValue); + + if (!JS_CallFunctionValue(cx, nullptr, callBack, jArgs, accumulator)) { + delete accumulator; + return false; + } + } + + args.rval().set(accumulator->get()); + delete accumulator; + return true; +} + +static bool array_reduceRight(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "reduceRight", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "reduceRight: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue accumulator(cx); + + Py_ssize_t len = PyList_GET_SIZE(self); + + if (args.length() > 1) { + accumulator.set(args[1].get()); + } + else { + if (len == 0) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_EMPTY_ARRAY_REDUCE); + return false; + } + accumulator.set(jsTypeFactory(cx, PyList_GetItem(self, len - 1))); + } + + for (int64_t index = args.length() > 1 ? len - 1 : len - 2; index >= 0; index--) { + jArgs[0].set(accumulator); + jArgs[1].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[2].setInt32(index); + jArgs[3].set(selfValue); + + if (!JS_CallFunctionValue(cx, nullptr, callBack, jArgs, &accumulator)) { + return false; + } + } + + args.rval().set(accumulator.get()); + return true; +} + +static bool array_some(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "some", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "some: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + Py_ssize_t len = PyList_GET_SIZE(self); + for (Py_ssize_t index = 0, toIndex = 0; index < len; index++) { + jArgs[0].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + + if (rval.toBoolean()) { + args.rval().setBoolean(true); + return true; + } + } + + args.rval().setBoolean(false); + return true; +} + +static bool array_every(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "every", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "every: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + Py_ssize_t len = PyList_GET_SIZE(self); + for (Py_ssize_t index = 0, toIndex = 0; index < len; index++) { + jArgs[0].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + + if (!rval.toBoolean()) { + args.rval().setBoolean(false); + return true; + } + } + + args.rval().setBoolean(true); + return true; +} + +static bool array_find(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "find", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "find: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + Py_ssize_t len = PyList_GET_SIZE(self); + for (Py_ssize_t index = 0, toIndex = 0; index < len; index++) { + JS::Value item = jsTypeFactory(cx, PyList_GetItem(self, index)); + jArgs[0].set(item); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + + if (rval.toBoolean()) { + args.rval().set(item); + return true; + } + } + + args.rval().setUndefined(); + return true; +} + +static bool array_findIndex(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "findIndex", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "findIndex: callback"); + return false; + } + + JS::RootedValue selfValue(cx, jsTypeFactory(cx, self)); + JS::RootedValue callBack(cx, callbackfn); + + JS::Rooted> jArgs(cx); + JS::RootedValue rval(cx); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + Py_ssize_t len = PyList_GET_SIZE(self); + for (Py_ssize_t index = 0, toIndex = 0; index < len; index++) { + jArgs[0].set(jsTypeFactory(cx, PyList_GetItem(self, index))); + jArgs[1].setInt32(index); + jArgs[2].set(selfValue); + + if (!JS_CallFunctionValue(cx, rootedThisArg, callBack, jArgs, &rval)) { + return false; + } + + if (rval.toBoolean()) { + args.rval().setInt32(index); + return true; + } + } + + args.rval().setInt32(-1); + return true; +} + +// private +static uint32_t FlattenIntoArray(JSContext *cx, + JSObject *retArray, PyObject *source, + Py_ssize_t sourceLen, uint32_t start, uint32_t depth) { + + uint32_t targetIndex = start; + + JS::RootedValue elementVal(cx); + + for (uint32_t sourceIndex = 0; sourceIndex < sourceLen; sourceIndex++) { + if (PyObject_TypeCheck(source, &JSArrayProxyType)) { + JS_GetElement(cx, *(((JSArrayProxy *)source)->jsArray), sourceIndex, &elementVal); + } + else if (PyObject_TypeCheck(source, &PyList_Type)) { + elementVal.set(jsTypeFactory(cx, PyList_GetItem(source, sourceIndex))); + } + + PyObject *element = pyTypeFactory(cx, elementVal); + + bool shouldFlatten; + if (depth > 0) { + shouldFlatten = PyObject_TypeCheck(element, &JSArrayProxyType) || PyObject_TypeCheck(element, &PyList_Type); + } else { + shouldFlatten = false; + } + + if (shouldFlatten) { + Py_ssize_t elementLen; + if (PyObject_TypeCheck(element, &JSArrayProxyType)) { + elementLen = JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)element); + } + else if (PyObject_TypeCheck(element, &PyList_Type)) { + elementLen = PyList_GET_SIZE(element); + } + + targetIndex = FlattenIntoArray(cx, + retArray, + element, + elementLen, + targetIndex, + depth - 1 + ); + } + else { + JS::RootedObject rootedRetArray(cx, retArray); + + uint32_t length; + JS::GetArrayLength(cx, rootedRetArray, &length); + if (targetIndex >= length) { + JS::SetArrayLength(cx, rootedRetArray, targetIndex + 1); + } + + JS_SetElement(cx, rootedRetArray, targetIndex, elementVal); + + targetIndex++; + } + + Py_DECREF(element); + } + + return targetIndex; +} + +// private +static uint32_t FlattenIntoArrayWithCallBack(JSContext *cx, + JSObject *retArray, PyObject *source, + Py_ssize_t sourceLen, uint32_t start, uint32_t depth, + JS::HandleValue callBack, JS::HandleObject thisArg) { + + uint32_t targetIndex = start; + + JS::RootedValue sourceValue(cx, jsTypeFactory(cx, source)); + JS::Rooted> jArgs(cx); + JS::RootedValue elementVal(cx); + JS::RootedValue retVal(cx); + + for (uint32_t sourceIndex = 0; sourceIndex < sourceLen; sourceIndex++) { + if (PyObject_TypeCheck(source, &JSArrayProxyType)) { + JS_GetElement(cx, *(((JSArrayProxy *)source)->jsArray), sourceIndex, &elementVal); + } + else if (PyObject_TypeCheck(source, &PyList_Type)) { + elementVal.set(jsTypeFactory(cx, PyList_GetItem(source, sourceIndex))); + } + + jArgs[0].set(elementVal); + jArgs[1].setInt32(sourceIndex); + jArgs[2].set(sourceValue); + if (!JS_CallFunctionValue(cx, thisArg, callBack, jArgs, &retVal)) { + return false; + } + + PyObject *element = pyTypeFactory(cx, retVal); + + bool shouldFlatten; + if (depth > 0) { + shouldFlatten = PyObject_TypeCheck(element, &JSArrayProxyType) || PyObject_TypeCheck(element, &PyList_Type); + } else { + shouldFlatten = false; + } + + Py_ssize_t elementLen; + if (PyObject_TypeCheck(element, &JSArrayProxyType)) { + elementLen = JSArrayProxyMethodDefinitions::JSArrayProxy_length((JSArrayProxy *)element); + } + else if (PyObject_TypeCheck(element, &PyList_Type)) { + elementLen = PyList_GET_SIZE(element); + } + + if (shouldFlatten) { + targetIndex = FlattenIntoArrayWithCallBack(cx, + retArray, + element, + elementLen, + targetIndex, + depth - 1, + callBack, + thisArg + ); + } + else { + JS::RootedObject rootedRetArray(cx, retArray); + + uint32_t length; + JS::GetArrayLength(cx, rootedRetArray, &length); + + if (PyObject_TypeCheck(element, &JSArrayProxyType) || PyObject_TypeCheck(element, &PyList_Type)) { + // flatten array callBack result to depth 1 + JS::RootedValue elementIndexVal(cx); + for (uint32_t elementIndex = 0; elementIndex < elementLen; elementIndex++, targetIndex++) { + if (PyObject_TypeCheck(element, &JSArrayProxyType)) { + JS_GetElement(cx, *(((JSArrayProxy *)element)->jsArray), elementIndex, &elementIndexVal); + } + else { + elementIndexVal.set(jsTypeFactory(cx, PyList_GetItem(element, elementIndex))); + } + + if (targetIndex >= length) { + JS::SetArrayLength(cx, rootedRetArray, length = targetIndex + 1); + } + + JS_SetElement(cx, rootedRetArray, targetIndex, elementIndexVal); + } + + return targetIndex; + } + else { + if (targetIndex >= length) { + JS::SetArrayLength(cx, rootedRetArray, targetIndex + 1); + } + + JS_SetElement(cx, rootedRetArray, targetIndex, retVal); + + targetIndex++; + } + } + + Py_DECREF(element); + } + + return targetIndex; +} + +static bool array_flat(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t sourceLen = PyList_GET_SIZE(self); + + uint32_t depthNum; + if (args.length() > 0) { + depthNum = args[0].get().toInt32(); + } + else { + depthNum = 1; + } + + JSObject *retArray = JS::NewArrayObject(cx, sourceLen); // min end length + + FlattenIntoArray(cx, retArray, self, sourceLen, 0, depthNum); + + args.rval().setObject(*retArray); + return true; +} + +static bool array_flatMap(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "flatMap", 1)) { + return false; + } + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t sourceLen = PyList_GET_SIZE(self); + + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION, "flatMap: callback"); + return false; + } + + JS::RootedValue callBack(cx, callbackfn); + + JS::RootedObject rootedThisArg(cx); + if (args.length() > 1) { + JS::Value thisArg = args[1].get(); + if (!thisArg.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_NOT_OBJORNULL, "'this' argument"); + return false; + } + + // TODO support null, currently gets TypeError + rootedThisArg.set(thisArg.toObjectOrNull()); + // check if callback is a PyMethod, need to make a new method bound to thisArg + if (!makeNewPyMethod(cx, &callBack, rootedThisArg)) { + return false; + } + } + else { + rootedThisArg.set(nullptr); + } + + JSObject *retArray = JS::NewArrayObject(cx, sourceLen); // min end length + + FlattenIntoArrayWithCallBack(cx, retArray, self, sourceLen, 0, 1, callBack, rootedThisArg); + + args.rval().setObject(*retArray); + return true; +} + +static bool array_join(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfLength = PyList_GET_SIZE(self); + + if (selfLength == 0) { + args.rval().setString(JS_NewStringCopyZ(cx, "")); + return true; + } + + JS::RootedString rootedSeparator(cx); + if (args.hasDefined(0)) { + rootedSeparator.set(JS::ToString(cx, args[0])); + } + else { + rootedSeparator.set(JS_NewStringCopyZ(cx, ",")); + } + + JSString *writer = JS_NewStringCopyZ(cx, ""); + JS::RootedString rootedWriter(cx); + + for (Py_ssize_t index = 0; index < selfLength; index++) { + rootedWriter.set(writer); + if (index > 0) { + writer = JS_ConcatStrings(cx, rootedWriter, rootedSeparator); + rootedWriter.set(writer); + } + + JS::RootedValue element(cx, jsTypeFactory(cx, PyList_GetItem(self, index))); + if (!element.isNullOrUndefined()) { + JS::RootedValue rval(cx); + + JS::RootedObject retObject(cx); + + if (!JS_ValueToObject(cx, element, &retObject)) { + return false; + } + + if (!JS_CallFunctionName(cx, retObject, "toString", JS::HandleValueArray::empty(), &rval)) { + return false; + } + + JS::RootedString retString(cx, rval.toString()); + writer = JS_ConcatStrings(cx, rootedWriter, retString); + } + } + + args.rval().setString(writer); + return true; +} + +static bool array_toString(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_join(cx, argc, vp); +} + +static bool array_toLocaleString(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t selfLength = PyList_GET_SIZE(self); + + if (selfLength == 0) { + args.rval().setString(JS_NewStringCopyZ(cx, "")); + return true; + } + + JS::RootedString rootedSeparator(cx, JS_NewStringCopyZ(cx, ",")); + + JSString *writer = JS_NewStringCopyZ(cx, ""); + JS::RootedString rootedWriter(cx); + + JS::HandleValueArray jArgs(args); + + for (Py_ssize_t index = 0; index < selfLength; index++) { + rootedWriter.set(writer); + if (index > 0) { + writer = JS_ConcatStrings(cx, rootedWriter, rootedSeparator); + rootedWriter.set(writer); + } + + JS::RootedValue element(cx, jsTypeFactory(cx, PyList_GetItem(self, index))); + if (!element.isNullOrUndefined()) { + JS::RootedValue rval(cx); + + JS::RootedObject retObject(cx); + + if (!JS_ValueToObject(cx, element, &retObject)) { + return false; + } + + if (!JS_CallFunctionName(cx, retObject, "toLocaleString", jArgs, &rval)) { + return false; + } + + JS::RootedString retString(cx, rval.toString()); + writer = JS_ConcatStrings(cx, rootedWriter, retString); + } + } + + args.rval().setString(writer); + return true; +} + +static bool array_valueOf(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + // return ref to self + args.rval().set(jsTypeFactory(cx, self)); + return true; +} + + +////// Sorting + + +static void swapItems(PyObject *list, int i, int j) { + if (i != j) { + PyObject *list_i = PyList_GetItem(list, i); + PyObject *list_j = PyList_GetItem(list, j); + Py_INCREF(list_i); + Py_INCREF(list_j); + PyList_SetItem(list, i, list_j); + PyList_SetItem(list, j, list_i); + } +} + +static int invokeCallBack(PyObject *list, int index, JS::HandleValue leftValue, JSContext *cx, JS::HandleFunction callBack) { + JS::Rooted> jArgs(cx); + + jArgs[0].set(jsTypeFactory(cx, PyList_GetItem(list, index))); + jArgs[1].set(leftValue); + + JS::RootedValue retVal(cx); + if (!JS_CallFunction(cx, nullptr, callBack, jArgs, &retVal)) { + throw "JS_CallFunction failed"; + } + + if (!retVal.isNumber()) { + PyErr_Format(PyExc_TypeError, "incorrect compare function return type"); + return 0; + } + + return retVal.toInt32(); +} + +// Adapted from Kernigan&Ritchie's C book +static void quickSort(PyObject *list, int left, int right, JSContext *cx, JS::HandleFunction callBack) { + + if (left >= right) { + // base case + return; + } + + swapItems(list, left, (left + right) / 2); + + JS::RootedValue leftValue(cx, jsTypeFactory(cx, PyList_GetItem(list, left))); + + int last = left; + for (int index = left + 1; index <= right; index++) { + int result = invokeCallBack(list, index, leftValue, cx, callBack); + if (PyErr_Occurred()) { + return; + } + if (result < 0) { + swapItems(list, ++last, index); + } + } + + swapItems(list, left, last); + + quickSort(list, left, last - 1, cx, callBack); + + quickSort(list, last + 1, right, cx, callBack); +} + +// private +static bool js_sort_compare_default(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedValue leftVal(cx, args[0]); + JS::RootedValue rightVal(cx, args[1]); + + // check for undefined + if (leftVal.isNullOrUndefined()) { + if (rightVal.isNullOrUndefined()) { + args.rval().setInt32(0); + } + else { + args.rval().setInt32(1); + } + return true; + } + else if (rightVal.isNullOrUndefined()) { + args.rval().setInt32(-1); + return true; + } + + JS::RootedObject leftObject(cx); + if (!JS_ValueToObject(cx, leftVal, &leftObject)) { + return false; + } + JS::RootedValue leftToStringVal(cx); + if (!JS_CallFunctionName(cx, leftObject, "toString", JS::HandleValueArray::empty(), &leftToStringVal)) { + return false; + } + + JS::RootedObject rightObject(cx); + if (!JS_ValueToObject(cx, rightVal, &rightObject)) { + return false; + } + JS::RootedValue rightToStringVal(cx); + if (!JS_CallFunctionName(cx, rightObject, "toString", JS::HandleValueArray::empty(), &rightToStringVal)) { + return false; + } + + int32_t cmpResult; + if (!JS_CompareStrings(cx, leftToStringVal.toString(), rightToStringVal.toString(), &cmpResult)) { + return false; + } + + args.rval().setInt32(cmpResult); + return true; +} + +static bool array_sort(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + Py_ssize_t len = PyList_GET_SIZE(self); + + if (len > 1) { + if (args.length() < 1) { + JS::RootedFunction funObj(cx, JS_NewFunction(cx, js_sort_compare_default, 2, 0, NULL)); + + try { + quickSort(self, 0, len - 1, cx, funObj); + } catch (const char *message) { + return false; + } + } + else { + JS::Value callbackfn = args[0].get(); + + if (!callbackfn.isObject() || !JS::IsCallable(&callbackfn.toObject())) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, JSMSG_BAD_SORT_ARG); + return false; + } + + JS::RootedValue callBack(cx, callbackfn); + JS::RootedFunction rootedFun(cx, JS_ValueToFunction(cx, callBack)); + try { + quickSort(self, 0, len - 1, cx, rootedFun); + } catch (const char *message) { + return false; + } + } + } + + if (PyErr_Occurred()) { + return false; + } + + // return ref to self + args.rval().set(jsTypeFactory(cx, self)); + return true; +} + + + +// ListIterator + + +#define ITEM_KIND_KEY 0 +#define ITEM_KIND_VALUE 1 +#define ITEM_KIND_KEY_AND_VALUE 2 + +enum { + ListIteratorSlotIteratedObject, + ListIteratorSlotNextIndex, + ListIteratorSlotItemKind, + ListIteratorSlotCount +}; + +static JSClass listIteratorClass = {"ListIterator", JSCLASS_HAS_RESERVED_SLOTS(ListIteratorSlotCount)}; + +static bool iterator_next(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JS::RootedObject thisObj(cx); + if (!args.computeThis(cx, &thisObj)) return false; + + PyObject *self = JS::GetMaybePtrFromReservedSlot(thisObj, ListIteratorSlotIteratedObject); + + JS::RootedValue rootedNextIndex(cx, JS::GetReservedSlot(thisObj, ListIteratorSlotNextIndex)); + JS::RootedValue rootedItemKind(cx, JS::GetReservedSlot(thisObj, ListIteratorSlotItemKind)); + + int32_t nextIndex; + int32_t itemKind; + if (!JS::ToInt32(cx, rootedNextIndex, &nextIndex) || !JS::ToInt32(cx, rootedItemKind, &itemKind)) return false; + + JS::RootedObject result(cx, JS_NewPlainObject(cx)); + + Py_ssize_t len = PyList_GET_SIZE(self); + + if (nextIndex >= len) { + // UnsafeSetReservedSlot(obj, ITERATOR_SLOT_TARGET, null); // TODO lose ref + JS::RootedValue done(cx, JS::BooleanValue(true)); + if (!JS_SetProperty(cx, result, "done", done)) return false; + args.rval().setObject(*result); + return result; + } + + JS::SetReservedSlot(thisObj, ListIteratorSlotNextIndex, JS::Int32Value(nextIndex + 1)); + + JS::RootedValue done(cx, JS::BooleanValue(false)); + if (!JS_SetProperty(cx, result, "done", done)) return false; + + if (itemKind == ITEM_KIND_VALUE) { + PyObject *item = PyList_GetItem(self, nextIndex); + if (!item) { + return false; + } + JS::RootedValue value(cx, jsTypeFactory(cx, item)); + if (!JS_SetProperty(cx, result, "value", value)) return false; + } + else if (itemKind == ITEM_KIND_KEY_AND_VALUE) { + JS::Rooted> items(cx); + + JS::RootedValue rootedNextIndex(cx, JS::Int32Value(nextIndex)); + items[0].set(rootedNextIndex); + + PyObject *item = PyList_GetItem(self, nextIndex); + if (!item) { + return false; + } + JS::RootedValue value(cx, jsTypeFactory(cx, item)); + items[1].set(value); + + JS::RootedValue pair(cx); + JSObject *array = JS::NewArrayObject(cx, items); + pair.setObject(*array); + if (!JS_SetProperty(cx, result, "value", pair)) return false; + } + else { // itemKind == ITEM_KIND_KEY + JS::RootedValue value(cx, JS::Int32Value(nextIndex)); + if (!JS_SetProperty(cx, result, "value", value)) return false; + } + + args.rval().setObject(*result); + return true; +} + +static JSFunctionSpec list_iterator_methods[] = { + JS_FN("next", iterator_next, 0, JSPROP_ENUMERATE), + JS_FS_END +}; + +static bool ListIteratorConstructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.isConstructing()) { + JS_ReportErrorASCII(cx, "You must call this constructor with 'new'"); + return false; + } + + JS::RootedObject thisObj(cx, JS_NewObjectForConstructor(cx, &listIteratorClass, args)); + if (!thisObj) { + return false; + } + + args.rval().setObject(*thisObj); + return true; +} + +static bool DefineListIterator(JSContext *cx, JS::HandleObject global) { + JS::RootedObject iteratorPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Iterator, &iteratorPrototype)) { + return false; + } + + JS::RootedObject protoObj(cx, + JS_InitClass(cx, global, + nullptr, iteratorPrototype, + "ListIterator", + ListIteratorConstructor, 0, + nullptr, list_iterator_methods, + nullptr, nullptr) + ); + + return protoObj; // != nullptr +} + +// private util +static bool array_iterator_func(JSContext *cx, unsigned argc, JS::Value *vp, int itemKind) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject proxy(cx, JS::ToObject(cx, args.thisv())); + if (!proxy) { + return false; + } + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + + JS::RootedObject global(cx, JS::GetNonCCWObjectGlobal(proxy)); + + JS::RootedValue constructor_val(cx); + if (!JS_GetProperty(cx, global, "ListIterator", &constructor_val)) return false; + if (!constructor_val.isObject()) { + if (!DefineListIterator(cx, global)) { + return false; + } + + if (!JS_GetProperty(cx, global, "ListIterator", &constructor_val)) return false; + if (!constructor_val.isObject()) { + JS_ReportErrorASCII(cx, "ListIterator is not a constructor"); + return false; + } + } + JS::RootedObject constructor(cx, &constructor_val.toObject()); + + JS::RootedObject obj(cx); + if (!JS::Construct(cx, constructor_val, JS::HandleValueArray::empty(), &obj)) return false; + if (!obj) return false; + + JS::SetReservedSlot(obj, ListIteratorSlotIteratedObject, JS::PrivateValue((void *)self)); + JS::SetReservedSlot(obj, ListIteratorSlotNextIndex, JS::Int32Value(0)); + JS::SetReservedSlot(obj, ListIteratorSlotItemKind, JS::Int32Value(itemKind)); + + args.rval().setObject(*obj); + return true; +} + +static bool array_entries(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_iterator_func(cx, argc, vp, ITEM_KIND_KEY_AND_VALUE); +} + +static bool array_keys(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_iterator_func(cx, argc, vp, ITEM_KIND_KEY); +} + +static bool array_values(JSContext *cx, unsigned argc, JS::Value *vp) { + return array_iterator_func(cx, argc, vp, ITEM_KIND_VALUE); +} + + +static JSMethodDef array_methods[] = { + {"reverse", array_reverse, 0}, + {"pop", array_pop, 0}, + {"push", array_push, 1}, + {"shift", array_shift, 0}, + {"unshift", array_unshift, 1}, + {"concat", array_concat, 1}, + {"slice", array_slice, 2}, + {"indexOf", array_indexOf, 1}, + {"lastIndexOf", array_lastIndexOf, 1}, + {"splice", array_splice, 2}, + {"sort", array_sort, 1}, + {"fill", array_fill, 3}, + {"copyWithin", array_copyWithin, 3}, + {"includes", array_includes, 1}, + {"forEach", array_forEach, 1}, + {"map", array_map, 1}, + {"filter", array_filter, 1}, + {"reduce", array_reduce, 1}, + {"reduceRight", array_reduceRight, 1}, + {"some", array_some, 1}, + {"every", array_every, 1}, + {"find", array_find, 1}, + {"findIndex", array_findIndex, 1}, + {"flat", array_flat, 1}, + {"flatMap", array_flatMap, 1}, + {"join", array_join, 1}, + {"toString", array_toString, 0}, + {"toLocaleString", array_toLocaleString, 0}, + {"valueOf", array_valueOf, 0}, + {"entries", array_entries, 0}, + {"keys", array_keys, 0}, + {"values", array_values, 0}, + {NULL, NULL, 0} +}; + + +bool PyListProxyHandler::getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc +) const { + // see if we're calling a function + if (id.isString()) { + for (size_t index = 0;; index++) { + bool isThatFunction; + const char *methodName = array_methods[index].name; + if (methodName == NULL) { // reached end of list + break; + } + else if (JS_StringEqualsAscii(cx, id.toString(), methodName, &isThatFunction) && isThatFunction) { + JSFunction *newFunction = JS_NewFunction(cx, array_methods[index].call, array_methods[index].nargs, 0, NULL); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + } + } + + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + // "length" property + bool isLengthProperty; + if (id.isString() && JS_StringEqualsLiteral(cx, id.toString(), "length", &isLengthProperty) && isLengthProperty) { + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::Int32Value(PyList_Size(self)) + ) + )); + return true; + } + + // "constructor" property + bool isConstructorProperty; + if (id.isString() && JS_StringEqualsLiteral(cx, id.toString(), "constructor", &isConstructorProperty) && isConstructorProperty) { + JS::RootedObject rootedArrayPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Array, &rootedArrayPrototype)) { + return false; + } + + JS::RootedValue Array_Prototype_Constructor(cx); + if (!JS_GetProperty(cx, rootedArrayPrototype, "constructor", &Array_Prototype_Constructor)) { + return false; + } + + JS::RootedObject rootedArrayPrototypeConstructor(cx, Array_Prototype_Constructor.toObjectOrNull()); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*rootedArrayPrototypeConstructor), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + + // symbol property + if (id.isSymbol()) { + JS::RootedSymbol rootedSymbol(cx, id.toSymbol()); + + if (JS::GetSymbolCode(rootedSymbol) == JS::SymbolCode::iterator) { + JSFunction *newFunction = JS_NewFunction(cx, array_values, 0, 0, NULL); + if (!newFunction) return false; + JS::RootedObject funObj(cx, JS_GetFunctionObject(newFunction)); + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), + {JS::PropertyAttribute::Enumerable} + ) + )); + return true; + } + } + + // item + Py_ssize_t index; + PyObject *item; + if (idToIndex(cx, id, &index) && (item = PyList_GetItem(self, index))) { + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + jsTypeFactory(cx, item), + {JS::PropertyAttribute::Writable, JS::PropertyAttribute::Enumerable} + ) + )); + } else { // item not found in list, or not an int-like property key + desc.set(mozilla::Nothing()); + } + return true; +} + +void PyListProxyHandler::finalize(JS::GCContext *gcx, JSObject *proxy) const { + // We cannot call Py_DECREF here when shutting down as the thread state is gone. + // Then, when shutting down, there is only on reference left, and we don't need + // to free the object since the entire process memory is being released. + if (!Py_IsFinalizing()) { + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + Py_DECREF(self); + } +} + +bool PyListProxyHandler::defineProperty( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::Handle desc, JS::ObjectOpResult &result +) const { + Py_ssize_t index; + if (!idToIndex(cx, id, &index)) { // not an int-like property key + return result.failBadIndex(); + } + + if (desc.isAccessorDescriptor()) { // containing getter/setter + return result.failNotDataDescriptor(); + } + if (!desc.hasValue()) { + return result.failInvalidDescriptor(); + } + + JS::RootedValue itemV(cx, desc.value()); + PyObject *item = pyTypeFactory(cx, itemV); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + if (PyList_SetItem(self, index, item) < 0) { + // we are out-of-bounds and need to expand + Py_ssize_t len = PyList_GET_SIZE(self); + // fill the space until the inserted index + for (Py_ssize_t i = len; i < index; i++) { + PyList_Append(self, Py_None); + } + + PyList_Append(self, item); + + // clear pending exception + PyErr_Clear(); + } + + return result.succeed(); +} + +bool PyListProxyHandler::ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const { + // Modified from https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/dom/base/RemoteOuterWindowProxy.cpp#l137 + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + int32_t length = PyList_Size(self); + if (!props.reserve(length + 1)) { + return false; + } + // item indexes + for (int32_t i = 0; i < length; ++i) { + props.infallibleAppend(JS::PropertyKey::Int(i)); + } + // the "length" property + props.infallibleAppend(JS::PropertyKey::NonIntAtom(JS_AtomizeString(cx, "length"))); + return true; +} + +bool PyListProxyHandler::delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult &result) const { + Py_ssize_t index; + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + if (!idToIndex(cx, id, &index)) { + return result.failBadIndex(); + } + + // Set to undefined instead of actually deleting it + if (PyList_SetItem(self, index, Py_None) < 0) { + return result.failCantDelete(); + } + return result.succeed(); +} + +bool PyListProxyHandler::isArray(JSContext *cx, JS::HandleObject proxy, JS::IsArrayAnswer *answer) const { + *answer = JS::IsArrayAnswer::Array; + return true; +} + +bool PyListProxyHandler::getBuiltinClass(JSContext *cx, JS::HandleObject proxy, js::ESClass *cls) const { + *cls = js::ESClass::Array; + return true; +} \ No newline at end of file diff --git a/src/PyObjectProxyHandler.cc b/src/PyObjectProxyHandler.cc new file mode 100644 index 00000000..0671774c --- /dev/null +++ b/src/PyObjectProxyHandler.cc @@ -0,0 +1,200 @@ +/** + * @file PyObjectProxyHandler.cc + * @author Caleb Aikens (caleb@distributive.network) + * @brief Struct for creating JS proxy objects for all objects + * @date 2024-01-30 + * + * @copyright Copyright (c) 2023 Distributive Corp. + * + */ + +#include "include/PyObjectProxyHandler.hh" + +#include "include/jsTypeFactory.hh" +#include "include/pyTypeFactory.hh" + +#include +#include +#include +#include +#include +#include + +#include +#include "include/pyshim.hh" + +const char PyObjectProxyHandler::family = 0; + +bool PyObjectProxyHandler::handleOwnPropertyKeys(JSContext *cx, PyObject *keys, size_t length, JS::MutableHandleIdVector props) { + if (!props.reserve(length)) { + return false; // out of memory + } + + for (size_t i = 0; i < length; i++) { + PyObject *key = PyList_GetItem(keys, i); + JS::RootedId jsId(cx); + if (!keyToId(key, &jsId)) { + continue; // skip over keys that are not str or int + } + props.infallibleAppend(jsId); + } + return true; +} + +bool PyObjectProxyHandler::handleGetOwnPropertyDescriptor(JSContext *cx, JS::HandleId id, + JS::MutableHandle> desc, PyObject *item) { + // see if we're calling a function + if (id.isString()) { + JS::UniqueChars idString = JS_EncodeStringToUTF8(cx, JS::RootedString(cx, id.toString())); + const char *methodName = idString.get(); + + if (!strcmp(methodName, "toString") || !strcmp(methodName, "toLocaleString") || !strcmp(methodName, "valueOf")) { + JS::RootedObject objectPrototype(cx); + if (!JS_GetClassPrototype(cx, JSProto_Object, &objectPrototype)) { + return false; + } + + JS::RootedValue Object_Prototype_Method(cx); + if (!JS_GetProperty(cx, objectPrototype, methodName, &Object_Prototype_Method)) { + return false; + } + + JS::RootedObject rootedObjectPrototypeConstructor(cx, Object_Prototype_Method.toObjectOrNull()); + + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + JS::ObjectValue(*rootedObjectPrototypeConstructor), + {JS::PropertyAttribute::Enumerable} + ) + )); + + return true; + } + } + + if (!item) { // NULL if the key is not present + desc.set(mozilla::Nothing()); // JS objects return undefined for nonpresent keys + } else { + desc.set(mozilla::Some( + JS::PropertyDescriptor::Data( + jsTypeFactory(cx, item), + {JS::PropertyAttribute::Writable, JS::PropertyAttribute::Enumerable} + ) + )); + } + return true; +} + +void PyObjectProxyHandler::finalize(JS::GCContext *gcx, JSObject *proxy) const { + // We cannot call Py_DECREF here when shutting down as the thread state is gone. + // Then, when shutting down, there is only on reference left, and we don't need + // to free the object since the entire process memory is being released. + if (!Py_IsFinalizing()) { + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + Py_DECREF(self); + } +} + +bool PyObjectProxyHandler::ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const { + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *keys = PyObject_Dir(self); + + if (keys != nullptr) { + size_t keysLength = PyList_Size(keys); + + PyObject *nonDunderKeys = PyList_New(0); + for (size_t i = 0; i < keysLength; i++) { + PyObject *key = PyList_GetItem(keys, i); + if (PyObject_CallMethod(key, "startswith", "(s)", "__") == Py_False) { // if key starts with "__", ignore it + PyList_Append(nonDunderKeys, key); + } + } + + return handleOwnPropertyKeys(cx, nonDunderKeys, PyList_Size(nonDunderKeys), props); + } + else { + if (PyErr_Occurred()) { + PyErr_Clear(); + } + + return handleOwnPropertyKeys(cx, PyList_New(0), 0, props); + } +} + +bool PyObjectProxyHandler::delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::ObjectOpResult &result) const { + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + if (PyObject_SetAttr(self, attrName, NULL) < 0) { + return result.failCantDelete(); // raises JS exception + } + return result.succeed(); +} + +bool PyObjectProxyHandler::has(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + bool *bp) const { + return hasOwn(cx, proxy, id, bp); +} + +bool PyObjectProxyHandler::getOwnPropertyDescriptor( + JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::MutableHandle> desc +) const { + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *item = PyObject_GetAttr(self, attrName); + if (!item && PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); // clear error, we will be returning undefined in this case + } + + return handleGetOwnPropertyDescriptor(cx, id, desc, item); +} + +bool PyObjectProxyHandler::set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + JS::HandleValue v, JS::HandleValue receiver, + JS::ObjectOpResult &result) const { + JS::RootedValue rootedV(cx, v); + PyObject *attrName = idToKey(cx, id); + + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + PyObject *value = pyTypeFactory(cx, rootedV); + if (PyObject_SetAttr(self, attrName, value)) { + Py_DECREF(value); + return result.failCantSetInterposed(); // raises JS exception + } + Py_DECREF(value); + return result.succeed(); +} + +bool PyObjectProxyHandler::enumerate(JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const { + return this->ownPropertyKeys(cx, proxy, props); +} + +bool PyObjectProxyHandler::hasOwn(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, + bool *bp) const { + PyObject *attrName = idToKey(cx, id); + PyObject *self = JS::GetMaybePtrFromReservedSlot(proxy, PyObjectSlot); + *bp = PyObject_HasAttr(self, attrName) == 1; + return true; +} + +bool PyObjectProxyHandler::getOwnEnumerablePropertyKeys( + JSContext *cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const { + return this->ownPropertyKeys(cx, proxy, props); +} + +bool PyObjectProxyHandler::defineProperty(JSContext *cx, JS::HandleObject proxy, + JS::HandleId id, + JS::Handle desc, + JS::ObjectOpResult &result) const { + // Block direct `Object.defineProperty` since we already have the `set` method + return result.failInvalidDescriptor(); +} + +bool PyObjectProxyHandler::getBuiltinClass(JSContext *cx, JS::HandleObject proxy, + js::ESClass *cls) const { + *cls = js::ESClass::Object; + return true; +} \ No newline at end of file diff --git a/src/PyProxyHandler.cc b/src/PyProxyHandler.cc deleted file mode 100644 index db4c9011..00000000 --- a/src/PyProxyHandler.cc +++ /dev/null @@ -1,265 +0,0 @@ -/** - * @file PyProxyHandler.cc - * @author Caleb Aikens (caleb@distributive.network) - * @brief Struct for creating JS proxy objects. Used by DictType for object coercion - * @version 0.1 - * @date 2023-04-20 - * - * Copyright (c) 2023 Distributive Corp. - * - */ - -#include "include/PyProxyHandler.hh" - -#include "include/jsTypeFactory.hh" -#include "include/pyTypeFactory.hh" -#include "include/StrType.hh" - -#include -#include -#include -#include - -#include - -PyObject *idToKey(JSContext *cx, JS::HandleId id) { - JS::RootedValue idv(cx, js::IdToValue(id)); - JS::RootedString idStr(cx); - if (!id.isSymbol()) { // `JS::ToString` returns `nullptr` for JS symbols - idStr = JS::ToString(cx, idv); - } else { - // TODO (Tom Tang): Revisit this once we have Symbol coercion support - // FIXME (Tom Tang): key collision for symbols without a description string, or pure strings look like "Symbol(xxx)" - idStr = JS_ValueToSource(cx, idv); - } - - // We convert all types of property keys to string - auto chars = JS_EncodeStringToUTF8(cx, idStr); - return PyUnicode_FromString(chars.get()); -} - -bool idToIndex(JSContext *cx, JS::HandleId id, Py_ssize_t *index) { - if (id.isInt()) { // int-like strings have already been automatically converted to ints - *index = id.toInt(); - return true; - } else { - return false; // fail - } -} - -const char PyProxyHandler::family = 0; - -bool PyProxyHandler::ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const { - PyObject *keys = PyDict_Keys(pyObject); - size_t length = PyList_Size(keys); - if (!props.reserve(length)) { - return false; // out of memory - } - - for (size_t i = 0; i < length; i++) { - PyObject *key = PyList_GetItem(keys, i); - JS::RootedId jsId(cx); - if (!keyToId(key, &jsId)) { - // TODO (Caleb Aikens): raise exception here - return false; // key is not a str or int - } - props.infallibleAppend(jsId); - } - return true; -} - -bool PyProxyHandler::delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::ObjectOpResult &result) const { - PyObject *attrName = idToKey(cx, id); - if (PyDict_DelItem(pyObject, attrName) < 0) { - return result.failCantDelete(); // raises JS exception - } - return result.succeed(); -} - -bool PyProxyHandler::has(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - bool *bp) const { - return hasOwn(cx, proxy, id, bp); -} - -bool PyProxyHandler::get(JSContext *cx, JS::HandleObject proxy, - JS::HandleValue receiver, JS::HandleId id, - JS::MutableHandleValue vp) const { - PyObject *attrName = idToKey(cx, id); - PyObject *p = PyDict_GetItemWithError(pyObject, attrName); - if (!p) { // NULL if the key is not present - vp.setUndefined(); // JS objects return undefined for nonpresent keys - } else { - vp.set(jsTypeFactory(cx, p)); - } - return true; -} - -bool PyProxyHandler::getOwnPropertyDescriptor( - JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::MutableHandle> desc -) const { - PyObject *attrName = idToKey(cx, id); - PyObject *item = PyDict_GetItemWithError(pyObject, attrName); - if (!item) { // NULL if the key is not present - desc.set(mozilla::Nothing()); // JS objects return undefined for nonpresent keys - } else { - desc.set(mozilla::Some( - JS::PropertyDescriptor::Data( - jsTypeFactory(cx, item), - {JS::PropertyAttribute::Writable, JS::PropertyAttribute::Enumerable} - ) - )); - } - return true; -} - -bool PyProxyHandler::set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::HandleValue v, JS::HandleValue receiver, - JS::ObjectOpResult &result) const { - JS::RootedValue *rootedV = new JS::RootedValue(cx, v); - PyObject *attrName = idToKey(cx, id); - JS::RootedObject *global = new JS::RootedObject(cx, JS::GetNonCCWObjectGlobal(proxy)); - if (PyDict_SetItem(pyObject, attrName, pyTypeFactory(cx, global, rootedV)->getPyObject())) { - return result.failCantSetInterposed(); // raises JS exception - } - return result.succeed(); -} - -bool PyProxyHandler::enumerate(JSContext *cx, JS::HandleObject proxy, - JS::MutableHandleIdVector props) const { - return this->ownPropertyKeys(cx, proxy, props); -} - -bool PyProxyHandler::hasOwn(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - bool *bp) const { - PyObject *attrName = idToKey(cx, id); - *bp = PyDict_Contains(pyObject, attrName) == 1; - return true; -} - -bool PyProxyHandler::getOwnEnumerablePropertyKeys( - JSContext *cx, JS::HandleObject proxy, - JS::MutableHandleIdVector props) const { - return this->ownPropertyKeys(cx, proxy, props); -} - -// @TODO (Caleb Aikens) implement this -void PyProxyHandler::finalize(JS::GCContext *gcx, JSObject *proxy) const {} - -bool PyProxyHandler::defineProperty(JSContext *cx, JS::HandleObject proxy, - JS::HandleId id, - JS::Handle desc, - JS::ObjectOpResult &result) const { - // Block direct `Object.defineProperty` since we already have the `set` method - return result.failInvalidDescriptor(); -} - -bool PyBaseProxyHandler::getPrototypeIfOrdinary(JSContext *cx, JS::HandleObject proxy, - bool *isOrdinary, - JS::MutableHandleObject protop) const { - // We don't have a custom [[GetPrototypeOf]] - *isOrdinary = true; - protop.set(js::GetStaticPrototype(proxy)); - return true; -} - -bool PyBaseProxyHandler::preventExtensions(JSContext *cx, JS::HandleObject proxy, - JS::ObjectOpResult &result) const { - result.succeed(); - return true; -} - -bool PyBaseProxyHandler::isExtensible(JSContext *cx, JS::HandleObject proxy, - bool *extensible) const { - *extensible = false; - return true; -} - -const char PyListProxyHandler::family = 0; - -bool PyListProxyHandler::getOwnPropertyDescriptor( - JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::MutableHandle> desc -) const { - // We're trying to get the "length" property - bool isLengthProperty; - if (id.isString() && JS_StringEqualsLiteral(cx, id.toString(), "length", &isLengthProperty) && isLengthProperty) { - // proxy.length = len(pyObject) - desc.set(mozilla::Some( - JS::PropertyDescriptor::Data( - JS::Int32Value(PySequence_Size(pyObject)) - ) - )); - return true; - } - - // We're trying to get an item - Py_ssize_t index; - PyObject *item; - if (idToIndex(cx, id, &index) && (item = PySequence_GetItem(pyObject, index))) { - desc.set(mozilla::Some( - JS::PropertyDescriptor::Data( - jsTypeFactory(cx, item), - {JS::PropertyAttribute::Writable, JS::PropertyAttribute::Enumerable} - ) - )); - } else { // item not found in list, or not an int-like property key - desc.set(mozilla::Nothing()); - } - return true; -} - -bool PyListProxyHandler::defineProperty( - JSContext *cx, JS::HandleObject proxy, JS::HandleId id, - JS::Handle desc, JS::ObjectOpResult &result -) const { - Py_ssize_t index; - if (!idToIndex(cx, id, &index)) { // not an int-like property key - return result.failBadIndex(); - } - - if (desc.isAccessorDescriptor()) { // containing getter/setter - return result.failNotDataDescriptor(); - } - if (!desc.hasValue()) { - return result.failInvalidDescriptor(); - } - - // FIXME (Tom Tang): memory leak - JS::RootedObject *global = new JS::RootedObject(cx, JS::GetNonCCWObjectGlobal(proxy)); - JS::RootedValue *itemV = new JS::RootedValue(cx, desc.value()); - PyObject *item = pyTypeFactory(cx, global, itemV)->getPyObject(); - if (PySequence_SetItem(pyObject, index, item) < 0) { - return result.failBadIndex(); - } - return result.succeed(); -} - -bool PyListProxyHandler::ownPropertyKeys(JSContext *cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const { - // Modified from https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/dom/base/RemoteOuterWindowProxy.cpp#l137 - int32_t length = PySequence_Size(pyObject); - if (!props.reserve(length + 1)) { - return false; - } - // item indexes - for (int32_t i = 0; i < length; ++i) { - props.infallibleAppend(JS::PropertyKey::Int(i)); - } - // the "length" property - props.infallibleAppend(JS::PropertyKey::NonIntAtom(JS_AtomizeString(cx, "length"))); - return true; -} - -bool PyListProxyHandler::delete_(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult &result) const { - Py_ssize_t index; - if (!idToIndex(cx, id, &index)) { - return result.failBadIndex(); // report failure - } - - // Set to undefined instead of actually deleting it - if (PySequence_SetItem(pyObject, index, Py_None) < 0) { - return result.failCantDelete(); // report failure - } - return result.succeed(); // report success -} diff --git a/src/PyType.cc b/src/PyType.cc deleted file mode 100644 index 5725b054..00000000 --- a/src/PyType.cc +++ /dev/null @@ -1,20 +0,0 @@ -#include "include/PyType.hh" - -#include "include/TypeEnum.hh" - -#include - -PyType::PyType() {} - -PyType::PyType(PyObject *object) { - Py_XINCREF(object); - pyObject = object; -} - -PyObject *PyType::getPyObject() { - return pyObject; -} - -PyType::~PyType() { - Py_XDECREF(pyObject); -} \ No newline at end of file diff --git a/src/StrType.cc b/src/StrType.cc index 44cf0491..a5c65788 100644 --- a/src/StrType.cc +++ b/src/StrType.cc @@ -1,8 +1,16 @@ -#include "include/StrType.hh" - -#include "include/PyType.hh" +/** + * @file StrType.cc + * @author Caleb Aikens (caleb@distributive.network), Giovanni Tedesco (giovanni@distributive.network) and Philippe Laporte (philippe@distributive.network) + * @brief Struct for representing python strings + * @date 2022-08-08 + * + * @copyright Copyright (c) 2022,2024 Distributive Corp. + * + */ -#include +#include "include/StrType.hh" +#include "include/JSStringProxy.hh" +#include "include/jsTypeFactory.hh" #include #include @@ -45,47 +53,122 @@ static bool containsSurrogatePair(const char16_t *chars, size_t length) { return false; } -StrType::StrType(PyObject *object) : PyType(object) {} +/** + * @brief check if the Latin-1 encoded `chars` only contain ascii characters + */ +static bool containsOnlyAscii(const JS::Latin1Char *chars, size_t length) { + for (size_t i = 0; i < length; i++) { + if (chars[i] >= 128) return false; + } + return true; +} + +/** + * @brief creates new UCS4-encoded pyObject string. This must be called by the user if the original JSString contains any surrogate pairs + * + * @return PyObject* - the UCS4-encoding of the pyObject string + * + */ +static PyObject *asUCS4(PyObject *pyString) { + if (PyUnicode_KIND(pyString) != PyUnicode_2BYTE_KIND) { + // return a new reference to match the behaviour of `PyUnicode_FromKindAndData` + Py_INCREF(pyString); + return pyString; + } + + uint16_t *chars = PY_UNICODE_OBJECT_DATA_UCS2(pyString); + size_t length = PY_UNICODE_OBJECT_LENGTH(pyString); + + uint32_t *ucs4String = new uint32_t[length]; + size_t ucs4Length = 0; + + for (size_t i = 0; i < length; i++, ucs4Length++) { + if (Py_UNICODE_IS_LOW_SURROGATE(chars[i])) { // character is an unpaired low surrogate + delete[] ucs4String; + return NULL; + } else if (Py_UNICODE_IS_HIGH_SURROGATE(chars[i])) { // character is a high surrogate + if ((i + 1 < length) && Py_UNICODE_IS_LOW_SURROGATE(chars[i+1])) { // next character is a low surrogate + ucs4String[ucs4Length] = Py_UNICODE_JOIN_SURROGATES(chars[i], chars[i+1]); + i++; // skip over low surrogate + } + else { // next character is not a low surrogate + delete[] ucs4String; + return NULL; + } + } else { // character is not a surrogate, and is in the BMP + ucs4String[ucs4Length] = chars[i]; + } + } -StrType::StrType(char *string) : PyType(Py_BuildValue("s", string)) {} + PyObject *ret = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ucs4String, ucs4Length); + delete[] ucs4String; + return ret; +} -StrType::StrType(JSContext *cx, JSString *str) { +PyObject *StrType::proxifyString(JSContext *cx, JS::HandleValue strVal) { + JS::RootedString str(cx, strVal.toString()); JSLinearString *lstr = JS_EnsureLinearString(cx, str); JS::AutoCheckCannotGC nogc; PyObject *p; size_t length = JS::GetLinearStringLength(lstr); - pyObject = (PyObject *)PyObject_New(PyUnicodeObject, &PyUnicode_Type); // new reference - Py_INCREF(pyObject); // XXX: Why? + JSStringProxy *pyString = PyObject_New(JSStringProxy, &JSStringProxyType); // new reference + + if (pyString == NULL) { + return NULL; + } + + JS::RootedObject obj(cx); + pyString->jsString = new JS::PersistentRootedValue(cx); + pyString->jsString->setString((JSString *)lstr); + jsStringProxies.insert(pyString); // Initialize as legacy string (https://github.com/python/cpython/blob/v3.12.0b1/Include/cpython/unicodeobject.h#L78-L93) // see https://github.com/python/cpython/blob/v3.11.3/Objects/unicodeobject.c#L1230-L1245 - PY_UNICODE_OBJECT_HASH(pyObject) = -1; - PY_UNICODE_OBJECT_STATE(pyObject).interned = 0; - PY_UNICODE_OBJECT_STATE(pyObject).compact = 0; - PY_UNICODE_OBJECT_STATE(pyObject).ascii = 0; - PY_UNICODE_OBJECT_UTF8(pyObject) = NULL; - PY_UNICODE_OBJECT_UTF8_LENGTH(pyObject) = 0; + PY_UNICODE_OBJECT_HASH(pyString) = -1; + PY_UNICODE_OBJECT_STATE(pyString).interned = 0; + PY_UNICODE_OBJECT_STATE(pyString).compact = 0; + PY_UNICODE_OBJECT_STATE(pyString).ascii = 0; + PY_UNICODE_OBJECT_UTF8(pyString) = NULL; + PY_UNICODE_OBJECT_UTF8_LENGTH(pyString) = 0; if (JS::LinearStringHasLatin1Chars(lstr)) { // latin1 spidermonkey, latin1 python const JS::Latin1Char *chars = JS::GetLatin1LinearStringChars(nogc, lstr); + if ((PY_VERSION_HEX) >= 0x030d0000) { // Python version is greater than 3.13 + // Short path to temporarily fix the issue with Python 3.13+ compact unicode representation. + // It would error with `ValueError: embedded null character`, which is caused by the fact that + // most Python C APIs assume the string buffer is null-terminated, so we need to create a copy. + PyObject *copied = PyUnicode_FromKindAndData(PyUnicode_1BYTE_KIND, chars, length); + Py_DECREF(pyString); + return copied; + } - PY_UNICODE_OBJECT_DATA_ANY(pyObject) = (void *)chars; - PY_UNICODE_OBJECT_KIND(pyObject) = PyUnicode_1BYTE_KIND; - PY_UNICODE_OBJECT_LENGTH(pyObject) = length; + PY_UNICODE_OBJECT_DATA_ANY(pyString) = (void *)chars; + PY_UNICODE_OBJECT_KIND(pyString) = PyUnicode_1BYTE_KIND; + PY_UNICODE_OBJECT_LENGTH(pyString) = length; #if PY_UNICODE_HAS_WSTR - PY_UNICODE_OBJECT_WSTR(pyObject) = NULL; - PY_UNICODE_OBJECT_WSTR_LENGTH(pyObject) = 0; - PY_UNICODE_OBJECT_READY(pyObject) = 1; + PY_UNICODE_OBJECT_WSTR(pyString) = NULL; + PY_UNICODE_OBJECT_WSTR_LENGTH(pyString) = 0; + PY_UNICODE_OBJECT_READY(pyString) = 1; + #endif + + #ifdef Py_DEBUG + // In a debug build of CPython, it needs to be a well-formed PyUnicodeObject, otherwise a `_PyObject_AssertFailed` error will be raised. + // See: `_PyUnicode_CheckConsistency` https://github.com/python/cpython/blob/v3.11.3/Objects/unicodeobject.c#L594-L600, #L552-L553 + if (containsOnlyAscii(chars, length)) { + PY_UNICODE_OBJECT_STATE(pyString).ascii = 1; + PY_UNICODE_OBJECT_UTF8(pyString) = (char *)chars; // XXX: most APIs (e.g. PyUnicode_AsUTF8) assume this is a \0 terminated string + PY_UNICODE_OBJECT_UTF8_LENGTH(pyString) = length; + } #endif } else { // utf16 spidermonkey, ucs2 python const char16_t *chars = JS::GetTwoByteLinearStringChars(nogc, lstr); - PY_UNICODE_OBJECT_DATA_ANY(pyObject) = (void *)chars; - PY_UNICODE_OBJECT_KIND(pyObject) = PyUnicode_2BYTE_KIND; - PY_UNICODE_OBJECT_LENGTH(pyObject) = length; + PY_UNICODE_OBJECT_DATA_ANY(pyString) = (void *)chars; + PY_UNICODE_OBJECT_KIND(pyString) = PyUnicode_2BYTE_KIND; + PY_UNICODE_OBJECT_LENGTH(pyString) = length; #if PY_UNICODE_HAS_WSTR // python unicode objects take advantage of a possible performance gain on systems where @@ -94,63 +177,51 @@ StrType::StrType(JSContext *cx, JSString *str) { // On systems where sizeof(wchar_t) == 4, i.e. Unixy systems, a similar performance gain happens if the // string is using UCS4 encoding [this is automatically handled by asUCS4()] if (sizeof(wchar_t) == 2) { - PY_UNICODE_OBJECT_WSTR(pyObject) = (wchar_t *)chars; - PY_UNICODE_OBJECT_WSTR_LENGTH(pyObject) = length; + PY_UNICODE_OBJECT_WSTR(pyString) = (wchar_t *)chars; + PY_UNICODE_OBJECT_WSTR_LENGTH(pyString) = length; } else { - PY_UNICODE_OBJECT_WSTR(pyObject) = NULL; - PY_UNICODE_OBJECT_WSTR_LENGTH(pyObject) = 0; + PY_UNICODE_OBJECT_WSTR(pyString) = NULL; + PY_UNICODE_OBJECT_WSTR_LENGTH(pyString) = 0; } - PY_UNICODE_OBJECT_READY(pyObject) = 1; + PY_UNICODE_OBJECT_READY(pyString) = 1; #endif if (containsSurrogatePair(chars, length)) { // We must convert to UCS4 here because Python does not support decoding string containing surrogate pairs to bytes - PyObject *ucs4Obj = asUCS4(pyObject); // convert to a new PyUnicodeObject with UCS4 data + PyObject *ucs4Obj = asUCS4((PyObject *)pyString); // convert to a new PyUnicodeObject with UCS4 data if (!ucs4Obj) { - // conversion fails, keep the original `pyObject` - return; + // conversion fails, keep the original `pyString` + return (PyObject *)pyString; } - Py_DECREF(pyObject); // cleanup the old `pyObject` - Py_INCREF(ucs4Obj); // XXX: Same as the above `Py_INCREF(pyObject);`. Why double freed on GC? - pyObject = ucs4Obj; + Py_DECREF(pyString); + return ucs4Obj; + } + if ((PY_VERSION_HEX) >= 0x030d0000) { // Python 3.13+, fix `ValueError: embedded null character` + PyObject *copied = PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND, chars, length); // create a copy of the string buffer + Py_DECREF(pyString); + return copied; } } -} -const char *StrType::getValue() const { - return PyUnicode_AsUTF8(pyObject); + return (PyObject *)pyString; } -/* static */ -PyObject *StrType::asUCS4(PyObject *pyObject) { - if (PyUnicode_KIND(pyObject) != PyUnicode_2BYTE_KIND) { - // return a new reference to match the behaviour of `PyUnicode_FromKindAndData` - Py_INCREF(pyObject); - return pyObject; - } - - uint16_t *chars = PY_UNICODE_OBJECT_DATA_UCS2(pyObject); - size_t length = PY_UNICODE_OBJECT_LENGTH(pyObject); - - uint32_t ucs4String[length]; - size_t ucs4Length = 0; - - for (size_t i = 0; i < length; i++, ucs4Length++) { - if (Py_UNICODE_IS_LOW_SURROGATE(chars[i])) { // character is an unpaired low surrogate - return NULL; - } else if (Py_UNICODE_IS_HIGH_SURROGATE(chars[i])) { // character is a high surrogate - if ((i + 1 < length) && Py_UNICODE_IS_LOW_SURROGATE(chars[i+1])) { // next character is a low surrogate - ucs4String[ucs4Length] = Py_UNICODE_JOIN_SURROGATES(chars[i], chars[i+1]); - i++; // skip over low surrogate - } - else { // next character is not a low surrogate - return NULL; - } - } else { // character is not a surrogate, and is in the BMP - ucs4String[ucs4Length] = chars[i]; +PyObject *StrType::getPyObject(JSContext *cx, JS::HandleValue str) { + const PythonExternalString *callbacks; + const char16_t *ucs2Buffer{}; + const JS::Latin1Char *latin1Buffer{}; + + if ( + JS::IsExternalUCString(str.toString(), (const JSExternalStringCallbacks **)&callbacks, &ucs2Buffer) || + JS::IsExternalStringLatin1(str.toString(), (const JSExternalStringCallbacks **)&callbacks, &latin1Buffer) + ) { + if (callbacks == &PythonExternalStringCallbacks) { + PyObject *pyString = ucs2Buffer ? callbacks->getPyString(ucs2Buffer) : callbacks->getPyString(latin1Buffer); + Py_INCREF(pyString); + return pyString; } } - return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ucs4String, ucs4Length); -} \ No newline at end of file + return proxifyString(cx, str); +} diff --git a/src/TupleType.cc b/src/TupleType.cc deleted file mode 100644 index b2cda6bb..00000000 --- a/src/TupleType.cc +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @file TupleType.cc - * @author Giovanni Tedesco - * @brief Implementation for the methods of the Tuple Type struct - * @version 0.1 - * @date 2022-08-19 - * - * @copyright Copyright (c) 2022 - * - */ - -#include "include/TupleType.hh" - -#include "include/PyType.hh" -#include "include/pyTypeFactory.hh" - -#include - -#include - -TupleType::TupleType(PyObject *object) : PyType(object) {} - -PyType *TupleType::get(int index) const { - return pyTypeFactory(PyTuple_GetItem(this->pyObject, index)); -} - -int TupleType::len() const { - return PyTuple_Size(this->pyObject); -} \ No newline at end of file diff --git a/src/internalBinding.cc b/src/internalBinding.cc index 791783f7..b52af4b0 100644 --- a/src/internalBinding.cc +++ b/src/internalBinding.cc @@ -59,10 +59,6 @@ PyObject *getInternalBindingPyFn(JSContext *cx) { JSObject *jsFn = (JSObject *)createInternalBinding(cx); // Convert to a Python function - // FIXME (Tom Tang): memory leak, not free-ed - JS::RootedObject *thisObj = new JS::RootedObject(cx, nullptr); - JS::RootedValue *jsFnVal = new JS::RootedValue(cx, JS::ObjectValue(*jsFn)); - PyObject *pyFn = pyTypeFactory(cx, thisObj, jsFnVal)->getPyObject(); - - return pyFn; -} + JS::RootedValue jsFnVal(cx, JS::ObjectValue(*jsFn)); + return pyTypeFactory(cx, jsFnVal); +} \ No newline at end of file diff --git a/src/internalBinding/timers.cc b/src/internalBinding/timers.cc index ec4d5e6c..567df548 100644 --- a/src/internalBinding/timers.cc +++ b/src/internalBinding/timers.cc @@ -2,13 +2,20 @@ * @file timers.cc * @author Tom Tang (xmader@distributive.network) * @brief Implement functions in `internalBinding("timers")` + * + * @copyright Copyright (c) 2023 Distributive Corp. */ #include "include/internalBinding.hh" #include "include/pyTypeFactory.hh" +#include "include/jsTypeFactory.hh" #include "include/PyEventLoop.hh" +#include "include/setSpiderMonkeyException.hh" #include +#include + +using AsyncHandle = PyEventLoop::AsyncHandle; /** * See function declarations in python/pythonmonkey/builtin_modules/internal-binding.d.ts : @@ -16,52 +23,131 @@ */ static bool enqueueWithDelay(JSContext *cx, unsigned argc, JS::Value *vp) { + if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) { + // quit, exit or sys.exit was called (and raised SystemExit) + return false; + } + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JS::HandleValue jobArgVal = args.get(0); double delaySeconds = args.get(1).toNumber(); + bool repeat = args.get(2).toBoolean(); + JS::HandleValue debugInfo = args.get(3); // Convert to a Python function - // FIXME (Tom Tang): memory leak, not free-ed - JS::RootedObject *thisv = new JS::RootedObject(cx, nullptr); - JS::RootedValue *jobArg = new JS::RootedValue(cx, jobArgVal); - PyObject *job = pyTypeFactory(cx, thisv, jobArg)->getPyObject(); - + JS::RootedValue jobArg(cx, jobArgVal); + PyObject *job = pyTypeFactory(cx, jobArg); // Schedule job to the running Python event-loop PyEventLoop loop = PyEventLoop::getRunningLoop(); if (!loop.initialized()) return false; - PyEventLoop::AsyncHandle handle = loop.enqueueWithDelay(job, delaySeconds); + PyEventLoop::AsyncHandle::id_t handleId = loop.enqueueWithDelay(job, delaySeconds, repeat); + Py_DECREF(job); + + // Set debug info for the WTFPythonMonkey tool + auto handle = PyEventLoop::AsyncHandle::fromId(handleId); + handle->setDebugInfo(pyTypeFactory(cx, debugInfo)); // Return the `timeoutID` to use in `clearTimeout` - args.rval().setDouble((double)PyEventLoop::AsyncHandle::getUniqueId(std::move(handle))); + args.rval().setNumber(handleId); return true; } -// TODO (Tom Tang): move argument checks to the JavaScript side static bool cancelByTimeoutId(JSContext *cx, unsigned argc, JS::Value *vp) { - using AsyncHandle = PyEventLoop::AsyncHandle; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - JS::HandleValue timeoutIdArg = args.get(0); + double timeoutID = args.get(0).toNumber(); args.rval().setUndefined(); - // silently does nothing when an invalid timeoutID is passed in - if (!timeoutIdArg.isInt32()) { - return true; - } - // Retrieve the AsyncHandle by `timeoutID` - int32_t timeoutID = timeoutIdArg.toInt32(); AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID); if (!handle) return true; // does nothing on invalid timeoutID - // Cancel this job on Python event-loop + // Cancel this job on the Python event-loop handle->cancel(); + handle->removeRef(); + + return true; +} + +static bool timerHasRef(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + double timeoutID = args.get(0).toNumber(); + + // Retrieve the AsyncHandle by `timeoutID` + AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID); + if (!handle) return false; // error no such timeoutID + + args.rval().setBoolean(handle->hasRef()); + return true; +} + +static bool timerAddRef(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + double timeoutID = args.get(0).toNumber(); + + // Retrieve the AsyncHandle by `timeoutID` + AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID); + if (!handle) return false; // error no such timeoutID + + handle->addRef(); + + args.rval().setUndefined(); + return true; +} + +static bool timerRemoveRef(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + double timeoutID = args.get(0).toNumber(); + + // Retrieve the AsyncHandle by `timeoutID` + AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID); + if (!handle) return false; // error no such timeoutID + + handle->removeRef(); + + args.rval().setUndefined(); + return true; +} + +static bool getDebugInfo(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + double timeoutID = args.get(0).toNumber(); + + // Retrieve the AsyncHandle by `timeoutID` + AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID); + if (!handle) return false; // error no such timeoutID + + JS::Value debugInfo = jsTypeFactory(cx, handle->getDebugInfo()); + args.rval().set(debugInfo); + return true; +} + +static bool getAllRefedTimersDebugInfo(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedVector results(cx); + for (AsyncHandle &timer: AsyncHandle::getAllTimers()) { + if (!timer.hasRef()) continue; // we only need ref'ed timers + + JS::Value debugInfo = jsTypeFactory(cx, timer.getDebugInfo()); + if (!results.append(debugInfo)) { + // out of memory + setSpiderMonkeyException(cx); + return false; + } + } + args.rval().setObjectOrNull(JS::NewArrayObject(cx, results)); return true; } JSFunctionSpec InternalBinding::timers[] = { JS_FN("enqueueWithDelay", enqueueWithDelay, /* nargs */ 2, 0), JS_FN("cancelByTimeoutId", cancelByTimeoutId, 1, 0), + JS_FN("timerHasRef", timerHasRef, 1, 0), + JS_FN("timerAddRef", timerAddRef, 1, 0), + JS_FN("timerRemoveRef", timerRemoveRef, 1, 0), + JS_FN("getDebugInfo", getDebugInfo, 1, 0), + JS_FN("getAllRefedTimersDebugInfo", getAllRefedTimersDebugInfo, 1, 0), JS_FS_END }; diff --git a/src/internalBinding/utils.cc b/src/internalBinding/utils.cc index 2104a264..64d77f9b 100644 --- a/src/internalBinding/utils.cc +++ b/src/internalBinding/utils.cc @@ -2,6 +2,8 @@ * @file utils.cc * @author Tom Tang (xmader@distributive.network) * @brief Implement functions in `internalBinding("utils")` + * + * @copyright Copyright (c) 2023 Distributive Corp. */ #include "include/internalBinding.hh" diff --git a/src/jsTypeFactory.cc b/src/jsTypeFactory.cc index 62dd2dde..d1fcfb60 100644 --- a/src/jsTypeFactory.cc +++ b/src/jsTypeFactory.cc @@ -1,49 +1,115 @@ /** * @file jsTypeFactory.cc - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief - * @version 0.1 * @date 2023-02-15 * - * @copyright Copyright (c) 2023 + * @copyright 2023-2024 Distributive Corp. * */ #include "include/jsTypeFactory.hh" #include "include/modules/pythonmonkey/pythonmonkey.hh" -#include "include/PyType.hh" -#include "include/FuncType.hh" +#include "include/JSFunctionProxy.hh" +#include "include/JSMethodProxy.hh" #include "include/JSObjectProxy.hh" -#include "include/PyProxyHandler.hh" +#include "include/JSArrayProxy.hh" +#include "include/PyDictProxyHandler.hh" +#include "include/JSStringProxy.hh" +#include "include/PyListProxyHandler.hh" +#include "include/PyObjectProxyHandler.hh" +#include "include/PyIterableProxyHandler.hh" #include "include/pyTypeFactory.hh" -#include "include/StrType.hh" #include "include/IntType.hh" #include "include/PromiseType.hh" #include "include/DateType.hh" #include "include/ExceptionType.hh" #include "include/BufferType.hh" +#include "include/setSpiderMonkeyException.hh" #include #include +#include #include +#include #include -#include // https://docs.python.org/3/c-api/datetime.html +#include +#include "include/pyshim.hh" + +#include #define HIGH_SURROGATE_START 0xD800 #define LOW_SURROGATE_START 0xDC00 #define LOW_SURROGATE_END 0xDFFF #define BMP_END 0x10000 -struct PythonExternalString : public JSExternalStringCallbacks { - void finalize(char16_t *chars) const override {} - size_t sizeOfBuffer(const char16_t *chars, mozilla::MallocSizeOf mallocSizeOf) const override { - return 0; +static PyDictProxyHandler pyDictProxyHandler; +static PyObjectProxyHandler pyObjectProxyHandler; +static PyListProxyHandler pyListProxyHandler; +static PyIterableProxyHandler pyIterableProxyHandler; + +std::unordered_map externalStringObjToRefCountMap; // a map of python string objects to the number of JSExternalStrings that depend on it, used when finalizing JSExternalStrings + +PyObject *PythonExternalString::getPyString(const char16_t *chars) +{ + for (auto it: externalStringObjToRefCountMap) { + if (PyUnicode_DATA(it.first) == (void *)chars) { // PyUnicode_<2/1>BYTE_DATA are just type casts of PyUnicode_DATA + return it.first; + } + } + + return NULL; // this shouldn't be reachable +} + +PyObject *PythonExternalString::getPyString(const JS::Latin1Char *chars) +{ + return PythonExternalString::getPyString((const char16_t *)chars); +} + +void PythonExternalString::finalize(char16_t *chars) const +{ + // We cannot call Py_DECREF here when shutting down as the thread state is gone. + // Then, when shutting down, there is only on reference left, and we don't need + // to free the object since the entire process memory is being released. + if (Py_IsFinalizing()) { return; } + + for (auto it = externalStringObjToRefCountMap.cbegin(), next_it = it; it != externalStringObjToRefCountMap.cend(); it = next_it) { + next_it++; + if (PyUnicode_DATA(it->first) == (void *)chars) { + Py_DECREF(it->first); + externalStringObjToRefCountMap[it->first] = externalStringObjToRefCountMap[it->first] - 1; + + if (externalStringObjToRefCountMap[it->first] == 0) { + externalStringObjToRefCountMap.erase(it); + } + } + } +} + +void PythonExternalString::finalize(JS::Latin1Char *chars) const +{ + PythonExternalString::finalize((char16_t *)chars); +} + +size_t PythonExternalString::sizeOfBuffer(const char16_t *chars, mozilla::MallocSizeOf mallocSizeOf) const +{ + for (auto it: externalStringObjToRefCountMap) { + if (PyUnicode_DATA(it.first) == (void *)chars) { + return PyUnicode_GetLength(it.first); + } } -}; -static constexpr PythonExternalString PythonExternalStringCallbacks; + return 0; // // this shouldn't be reachable +} + +size_t PythonExternalString::sizeOfBuffer(const JS::Latin1Char *chars, mozilla::MallocSizeOf mallocSizeOf) const +{ + return PythonExternalString::sizeOfBuffer((const char16_t *)chars, mallocSizeOf); +} + +PythonExternalString PythonExternalStringCallbacks = {}; size_t UCS4ToUTF16(const uint32_t *chars, size_t length, uint16_t **outStr) { uint16_t *utf16String = (uint16_t *)malloc(sizeof(uint16_t) * length*2); @@ -75,8 +141,8 @@ JS::Value jsTypeFactory(JSContext *cx, PyObject *object) { returnType.setBoolean(PyLong_AsLong(object)); } else if (PyLong_Check(object)) { - if (PyObject_IsInstance(object, PythonMonkey_BigInt)) { // pm.bigint is a subclass of the builtin int type - JS::BigInt *bigint = IntType(object).toJsBigInt(cx); + if (PyObject_IsInstance(object, getPythonMonkeyBigInt())) { // pm.bigint is a subclass of the builtin int type + JS::BigInt *bigint = IntType::toJsBigInt(cx, object); returnType.setBigInt(bigint); } else if (_PyLong_NumBits(object) <= 53) { // num <= JS Number.MAX_SAFE_INTEGER, the mantissa of a float64 is 53 bits (with 52 explicitly stored and the highest bit always being 1) int64_t num = PyLong_AsLongLong(object); @@ -88,6 +154,9 @@ JS::Value jsTypeFactory(JSContext *cx, PyObject *object) { else if (PyFloat_Check(object)) { returnType.setNumber(PyFloat_AsDouble(object)); } + else if (PyObject_TypeCheck(object, &JSStringProxyType)) { + returnType.setString(((JSStringProxy *)object)->jsString->toString()); + } else if (PyUnicode_Check(object)) { switch (PyUnicode_KIND(object)) { case (PyUnicode_4BYTE_KIND): { @@ -100,98 +169,145 @@ JS::Value jsTypeFactory(JSContext *cx, PyObject *object) { break; } case (PyUnicode_2BYTE_KIND): { - JSString *str = JS_NewExternalString(cx, (char16_t *)PyUnicode_2BYTE_DATA(object), PyUnicode_GET_LENGTH(object), &PythonExternalStringCallbacks); + externalStringObjToRefCountMap[object] = externalStringObjToRefCountMap[object] + 1; + Py_INCREF(object); + JSString *str = JS_NewExternalUCString(cx, (char16_t *)PyUnicode_2BYTE_DATA(object), PyUnicode_GET_LENGTH(object), &PythonExternalStringCallbacks); returnType.setString(str); break; } case (PyUnicode_1BYTE_KIND): { - - JSString *str = JS_NewExternalString(cx, (char16_t *)PyUnicode_1BYTE_DATA(object), PyUnicode_GET_LENGTH(object), &PythonExternalStringCallbacks); - /* TODO (Caleb Aikens): this is a hack to set the JSString::LATIN1_CHARS_BIT, because there isnt an API for latin1 JSExternalStrings. - * Ideally we submit a patch to Spidermonkey to make this part of their API with the following signature: - * JS_NewExternalString(JSContext *cx, const char *chars, size_t length, const JSExternalStringCallbacks *callbacks) - */ - // FIXME: JSExternalString are all treated as two-byte strings when GCed - // see https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/StringType-inl.h#l514 - // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/StringType.h#l1808 - *(std::atomic *)str |= 512; + externalStringObjToRefCountMap[object] = externalStringObjToRefCountMap[object] + 1; + Py_INCREF(object); + JSString *str = JS_NewExternalStringLatin1(cx, (JS::Latin1Char *)PyUnicode_1BYTE_DATA(object), PyUnicode_GET_LENGTH(object), &PythonExternalStringCallbacks); + // JSExternalString can now be properly treated as either one-byte or two-byte strings when GCed + // see https://hg.mozilla.org/releases/mozilla-esr128/file/tip/js/src/vm/StringType-inl.h#l785 returnType.setString(str); break; } } - memoizePyTypeAndGCThing(new StrType(object), returnType); - } - else if (PyCFunction_Check(object) && PyCFunction_GetFunction(object) == callJSFunc) { - // If it's a wrapped JS function by us, return the underlying JS function rather than wrapping it again - PyObject *jsCxThisFuncTuple = PyCFunction_GetSelf(object); - JS::RootedValue *jsFunc = (JS::RootedValue *)PyLong_AsVoidPtr(PyTuple_GetItem(jsCxThisFuncTuple, 2)); - returnType.set(*jsFunc); } - else if (PyFunction_Check(object) || PyCFunction_Check(object)) { + else if (PyMethod_Check(object) || PyFunction_Check(object) || PyCFunction_Check(object)) { // can't determine number of arguments for PyCFunctions, so just assume potentially unbounded uint16_t nargs = 0; if (PyFunction_Check(object)) { - // https://docs.python.org/3.11/reference/datamodel.html?highlight=co_argcount PyCodeObject *bytecode = (PyCodeObject *)PyFunction_GetCode(object); // borrowed reference nargs = bytecode->co_argcount; } JSFunction *jsFunc = js::NewFunctionWithReserved(cx, callPyFunc, nargs, 0, NULL); - JSObject *jsFuncObject = JS_GetFunctionObject(jsFunc); - + JS::RootedObject jsFuncObject(cx, JS_GetFunctionObject(jsFunc)); // We put the address of the PyObject in the JSFunction's 0th private slot so we can access it later js::SetFunctionNativeReserved(jsFuncObject, 0, JS::PrivateValue((void *)object)); returnType.setObject(*jsFuncObject); - memoizePyTypeAndGCThing(new FuncType(object), returnType); Py_INCREF(object); // otherwise the python function object would be double-freed on GC in Python 3.11+ + + // add function to jsFunctionRegistry, to DECREF the PyObject when the JSFunction is finalized + JS::RootedValueArray<2> registerArgs(GLOBAL_CX); + registerArgs[0].setObject(*jsFuncObject); + registerArgs[1].setPrivate(object); + JS::RootedValue ignoredOutVal(GLOBAL_CX); + JS::RootedObject registry(GLOBAL_CX, jsFunctionRegistry); + if (!JS_CallFunctionName(GLOBAL_CX, registry, "register", registerArgs, &ignoredOutVal)) { + setSpiderMonkeyException(GLOBAL_CX); + return returnType; + } } else if (PyExceptionInstance_Check(object)) { - JSObject *error = ExceptionType(object).toJsError(cx); - returnType.setObject(*error); + JSObject *error = ExceptionType::toJsError(cx, object, nullptr); + if (error) { + returnType.setObject(*error); + } + else { + returnType.setUndefined(); + } } else if (PyDateTime_Check(object)) { - JSObject *dateObj = DateType(object).toJsDate(cx); + JSObject *dateObj = DateType::toJsDate(cx, object); returnType.setObject(*dateObj); } else if (PyObject_CheckBuffer(object)) { - BufferType *pmBuffer = new BufferType(object); - JSObject *typedArray = pmBuffer->toJsTypedArray(cx); // may return null + JSObject *typedArray = BufferType::toJsTypedArray(cx, object); // may return null returnType.setObjectOrNull(typedArray); - memoizePyTypeAndGCThing(pmBuffer, returnType); } else if (PyObject_TypeCheck(object, &JSObjectProxyType)) { - returnType.setObject(*((JSObjectProxy *)object)->jsObject); + returnType.setObject(**((JSObjectProxy *)object)->jsObject); + } + else if (PyObject_TypeCheck(object, &JSMethodProxyType)) { + JS::RootedObject func(cx, *((JSMethodProxy *)object)->jsFunc); + PyObject *self = ((JSMethodProxy *)object)->self; + + JS::Rooted> args(cx); + args[0].set(jsTypeFactory(cx, self)); + JS::Rooted boundFunction(cx); + if (!JS_CallFunctionName(cx, func, "bind", args, &boundFunction)) { + setSpiderMonkeyException(GLOBAL_CX); + return returnType; + } + returnType.set(boundFunction); + // add function to jsFunctionRegistry, to DECREF the PyObject when the JSFunction is finalized + JS::RootedValueArray<2> registerArgs(GLOBAL_CX); + registerArgs[0].set(boundFunction); + registerArgs[1].setPrivate(object); + JS::RootedValue ignoredOutVal(GLOBAL_CX); + JS::RootedObject registry(GLOBAL_CX, jsFunctionRegistry); + if (!JS_CallFunctionName(GLOBAL_CX, registry, "register", registerArgs, &ignoredOutVal)) { + setSpiderMonkeyException(GLOBAL_CX); + return returnType; + } + + Py_INCREF(object); + } + else if (PyObject_TypeCheck(object, &JSFunctionProxyType)) { + returnType.setObject(**((JSFunctionProxy *)object)->jsFunc); + } + else if (PyObject_TypeCheck(object, &JSArrayProxyType)) { + returnType.setObject(**((JSArrayProxy *)object)->jsArray); } else if (PyDict_Check(object) || PyList_Check(object)) { JS::RootedValue v(cx); JSObject *proxy; if (PyList_Check(object)) { - proxy = js::NewProxyObject(cx, new PyListProxyHandler(object), v, NULL); + JS::RootedObject arrayPrototype(cx); + JS_GetClassPrototype(cx, JSProto_Array, &arrayPrototype); // so that instanceof will work, not that prototype methods will + proxy = js::NewProxyObject(cx, &pyListProxyHandler, v, arrayPrototype.get()); } else { - proxy = js::NewProxyObject(cx, new PyProxyHandler(object), v, NULL); + JS::RootedObject objectPrototype(cx); + JS_GetClassPrototype(cx, JSProto_Object, &objectPrototype); // so that instanceof will work, not that prototype methods will + proxy = js::NewProxyObject(cx, &pyDictProxyHandler, v, objectPrototype.get()); } + Py_INCREF(object); + JS::SetReservedSlot(proxy, PyObjectSlot, JS::PrivateValue(object)); returnType.setObject(*proxy); } else if (object == Py_None) { returnType.setUndefined(); } - else if (object == PythonMonkey_Null) { + else if (object == getPythonMonkeyNull()) { returnType.setNull(); } else if (PythonAwaitable_Check(object)) { - PromiseType *p = new PromiseType(object); - JSObject *promise = p->toJsPromise(cx); // may return null - returnType.setObjectOrNull(promise); - // nested awaitables would have already been GCed if finished - // memoizePyTypeAndGCThing(p, returnType); + returnType.setObjectOrNull(PromiseType::toJsPromise(cx, object)); + } + else if (PyIter_Check(object)) { + JS::RootedValue v(cx); + JS::RootedObject objectPrototype(cx); + JS_GetClassPrototype(cx, JSProto_Object, &objectPrototype); // so that instanceof will work, not that prototype methods will + JSObject *proxy = js::NewProxyObject(cx, &pyIterableProxyHandler, v, objectPrototype.get()); + PyObject *iterable = PyObject_GetIter(object); + Py_INCREF(iterable); + JS::SetReservedSlot(proxy, PyObjectSlot, JS::PrivateValue(iterable)); + returnType.setObject(*proxy); } else { - std::string errorString("pythonmonkey cannot yet convert python objects of type: "); - errorString += Py_TYPE(object)->tp_name; - PyErr_SetString(PyExc_TypeError, errorString.c_str()); + JS::RootedValue v(cx); + JS::RootedObject objectPrototype(cx); + JS_GetClassPrototype(cx, JSProto_Object, &objectPrototype); // so that instanceof will work, not that prototype methods will + JSObject *proxy = js::NewProxyObject(cx, &pyObjectProxyHandler, v, objectPrototype.get()); + Py_INCREF(object); + JS::SetReservedSlot(proxy, PyObjectSlot, JS::PrivateValue(object)); + returnType.setObject(*proxy); } return returnType; - } JS::Value jsTypeFactorySafe(JSContext *cx, PyObject *object) { @@ -210,68 +326,127 @@ JS::Value jsTypeFactorySafe(JSContext *cx, PyObject *object) { return v; } -void setPyException(JSContext *cx) { +bool setPyException(JSContext *cx) { // Python `exit` and `sys.exit` only raise a SystemExit exception to end the program // We definitely don't want to catch it in JS if (PyErr_ExceptionMatches(PyExc_SystemExit)) { - return; + return false; } PyObject *type, *value, *traceback; - PyErr_Fetch(&type, &value, &traceback); // also clears the error indicator + PyErr_Fetch(&type, &value, &traceback); + + JSObject *jsException = ExceptionType::toJsError(cx, value, traceback); - JSObject *jsException = ExceptionType(value).toJsError(cx); - JS::RootedValue jsExceptionValue(cx, JS::ObjectValue(*jsException)); - JS_SetPendingException(cx, jsExceptionValue); + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + + if (jsException) { + JS::RootedValue jsExceptionValue(cx, JS::ObjectValue(*jsException)); + JS_SetPendingException(cx, jsExceptionValue); + } + return true; } bool callPyFunc(JSContext *cx, unsigned int argc, JS::Value *vp) { JS::CallArgs callargs = JS::CallArgsFromVp(argc, vp); // get the python function from the 0th reserved slot - JS::Value pyFuncVal = js::GetFunctionNativeReserved(&(callargs.callee()), 0); - PyObject *pyFunc = (PyObject *)(pyFuncVal.toPrivate()); - - JS::RootedObject *thisv = new JS::RootedObject(cx); - JS_ValueToObject(cx, callargs.thisv(), thisv); - - if (!callargs.length()) { - #if PY_VERSION_HEX >= 0x03090000 - PyObject *pyRval = PyObject_CallNoArgs(pyFunc); - #else - PyObject *pyRval = _PyObject_CallNoArg(pyFunc); // in Python 3.8, the API is only available under the name with a leading underscore - #endif - if (PyErr_Occurred()) { // Check if an exception has already been set in Python error stack - setPyException(cx); - return false; + PyObject *pyFunc = (PyObject *)js::GetFunctionNativeReserved(&(callargs.callee()), 0).toPrivate(); + Py_INCREF(pyFunc); + PyObject *pyRval = NULL; + PyObject *pyArgs = NULL; + Py_ssize_t nNormalArgs = 0; // number of positional non-default arguments + Py_ssize_t nDefaultArgs = 0; // number of positional default arguments + bool varargs = false; + bool unknownNargs = false; + + if (PyCFunction_Check(pyFunc)) { + const int funcFlags = ((PyCFunctionObject *)pyFunc)->m_ml->ml_flags; + if (funcFlags & METH_NOARGS) { // 0 arguments + nNormalArgs = 0; + } + else if (funcFlags & METH_O) { // 1 argument + nNormalArgs = 1; + } + else { // unknown number of arguments + nNormalArgs = 0; + unknownNargs = true; + varargs = true; } - // @TODO (Caleb Aikens) need to check for python exceptions here - callargs.rval().set(jsTypeFactory(cx, pyRval)); - return true; + } + else { + nNormalArgs = 1; + PyObject *f = pyFunc; + if (PyMethod_Check(pyFunc)) { + f = PyMethod_Function(pyFunc); // borrowed reference + nNormalArgs -= 1; // don't include the implicit `self` of the method as an argument + } + PyCodeObject *bytecode = (PyCodeObject *)PyFunction_GetCode(f); // borrowed reference + PyObject *defaults = PyFunction_GetDefaults(f); // borrowed reference + nDefaultArgs = defaults ? PyTuple_Size(defaults) : 0; + nNormalArgs += bytecode->co_argcount - nDefaultArgs - 1; + if (bytecode->co_flags & CO_VARARGS) { + varargs = true; + } + } + + // use faster calling if no arguments are needed + if (((nNormalArgs + nDefaultArgs) <= 0 && !varargs)) { + pyRval = PyObject_CallObject(pyFunc, NULL); + if (PyErr_Occurred() && setPyException(cx)) { // Check if an exception has already been set in Python error stack + goto failure; + } + goto success; } // populate python args tuple - PyObject *pyArgs = PyTuple_New(callargs.length()); - for (size_t i = 0; i < callargs.length(); i++) { - JS::RootedValue *jsArg = new JS::RootedValue(cx, callargs[i]); - PyType *pyArg = pyTypeFactory(cx, thisv, jsArg); - if (!pyArg) return false; // error occurred - PyObject *pyArgObj = pyArg->getPyObject(); + Py_ssize_t argTupleLength; + if (unknownNargs) { // pass all passed arguments + argTupleLength = callargs.length(); + } + else if (varargs) { // if passed arguments is less than number of non-default positionals, rest will be set to `None` + argTupleLength = std::max((Py_ssize_t)callargs.length(), nNormalArgs); + } + else if (nNormalArgs > callargs.length()) { // if passed arguments is less than number of non-default positionals, rest will be set to `None` + argTupleLength = nNormalArgs; + } + else { // passed arguments greater than non-default positionals, so we may be replacing default positional arguments + argTupleLength = std::min((Py_ssize_t)callargs.length(), nNormalArgs+nDefaultArgs); + } + pyArgs = PyTuple_New(argTupleLength); + + for (size_t i = 0; i < callargs.length() && i < argTupleLength; i++) { + JS::RootedValue jsArg(cx, callargs[i]); + PyObject *pyArgObj = pyTypeFactory(cx, jsArg); if (!pyArgObj) return false; // error occurred PyTuple_SetItem(pyArgs, i, pyArgObj); } - PyObject *pyRval = PyObject_Call(pyFunc, pyArgs, NULL); - if (PyErr_Occurred()) { - setPyException(cx); - return false; + // set unspecified args to None, to match JS behaviour of setting unspecified args to undefined + for (Py_ssize_t i = callargs.length(); i < argTupleLength; i++) { + PyTuple_SetItem(pyArgs, i, Py_None); } - // @TODO (Caleb Aikens) need to check for python exceptions here - callargs.rval().set(jsTypeFactory(cx, pyRval)); - if (PyErr_Occurred()) { - setPyException(cx); - return false; + + pyRval = PyObject_Call(pyFunc, pyArgs, NULL); + if (PyErr_Occurred() && setPyException(cx)) { + goto failure; } + goto success; +success: + if (pyRval) { // can be NULL if SystemExit was raised + callargs.rval().set(jsTypeFactory(cx, pyRval)); + Py_DECREF(pyRval); + } + Py_DECREF(pyFunc); + Py_XDECREF(pyArgs); return true; + +failure: + Py_XDECREF(pyRval); + Py_DECREF(pyFunc); + Py_XDECREF(pyArgs); + return false; } \ No newline at end of file diff --git a/src/modules/pythonmonkey/pythonmonkey.cc b/src/modules/pythonmonkey/pythonmonkey.cc index af98ec1d..8408b594 100644 --- a/src/modules/pythonmonkey/pythonmonkey.cc +++ b/src/modules/pythonmonkey/pythonmonkey.cc @@ -1,26 +1,27 @@ /** * @file pythonmonkey.cc - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief This file defines the pythonmonkey module, along with its various functions. - * @version 0.1 * @date 2023-03-29 * - * @copyright Copyright (c) 2023 Distributive Corp. + * @copyright Copyright (c) 2023-2024 Distributive Corp. * */ #include "include/modules/pythonmonkey/pythonmonkey.hh" - -#include "include/BoolType.hh" #include "include/setSpiderMonkeyException.hh" -#include "include/DateType.hh" -#include "include/FloatType.hh" -#include "include/FuncType.hh" +#include "include/JSFunctionProxy.hh" +#include "include/JSMethodProxy.hh" +#include "include/JSArrayIterProxy.hh" +#include "include/JSArrayProxy.hh" +#include "include/JSObjectIterProxy.hh" +#include "include/JSObjectKeysProxy.hh" +#include "include/JSObjectValuesProxy.hh" +#include "include/JSObjectItemsProxy.hh" #include "include/JSObjectProxy.hh" -#include "include/PyType.hh" +#include "include/JSStringProxy.hh" #include "include/pyTypeFactory.hh" -#include "include/StrType.hh" #include "include/PyEventLoop.hh" #include "include/internalBinding.hh" @@ -40,105 +41,286 @@ #include #include +#include "include/pyshim.hh" #include #include +#include + +JS::PersistentRootedObject jsFunctionRegistry; + +/** + * @brief During a GC, string buffers may have moved, so we need to re-point our JSStringProxies + * The char buffer pointer obtained by previous `JS::Get{Latin1,TwoByte}LinearStringChars` calls remains valid only as long as no GC occurs. + */ +void updateCharBufferPointers() { + if (Py_IsFinalizing()) { + return; // do not move char pointers around if python is finalizing + } + + JS::AutoCheckCannotGC nogc; + for (const JSStringProxy *jsStringProxy: jsStringProxies) { + JSLinearString *str = JS_ASSERT_STRING_IS_LINEAR(jsStringProxy->jsString->toString()); + void *updatedCharBufPtr; // pointer to the moved char buffer after a GC + if (JS::LinearStringHasLatin1Chars(str)) { + updatedCharBufPtr = (void *)JS::GetLatin1LinearStringChars(nogc, str); + } else { // utf16 / ucs2 string + updatedCharBufPtr = (void *)JS::GetTwoByteLinearStringChars(nogc, str); + } + ((PyUnicodeObject *)(jsStringProxy))->data.any = updatedCharBufPtr; + } +} + +void pythonmonkeyGCCallback(JSContext *cx, JSGCStatus status, JS::GCReason reason, void *data) { + if (status == JSGCStatus::JSGC_END) { + JS::ClearKeptObjects(GLOBAL_CX); + while (JOB_QUEUE->runFinalizationRegistryCallbacks(GLOBAL_CX)); + updateCharBufferPointers(); + } +} + +void nurseryCollectionCallback(JSContext *cx, JS::GCNurseryProgress progress, JS::GCReason reason, void *data) { + if (progress == JS::GCNurseryProgress::GC_NURSERY_COLLECTION_END) { + updateCharBufferPointers(); + } +} + +bool functionRegistryCallback(JSContext *cx, unsigned int argc, JS::Value *vp) { + JS::CallArgs callargs = JS::CallArgsFromVp(argc, vp); + Py_DECREF((PyObject *)callargs[0].toPrivate()); + return true; +} + +static void cleanupFinalizationRegistry(JSFunction *callback, JSObject *global [[maybe_unused]], void *user_data [[maybe_unused]]) { + JOB_QUEUE->queueFinalizationRegistryCallback(callback); +} + +static PyObject *PythonMonkey_Null; +static PyObject *PythonMonkey_BigInt; + +PyObject *getPythonMonkeyNull() { + if (!PythonMonkey_Null) { + PythonMonkey_Null = PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "null"); + } + return PythonMonkey_Null; +} + +PyObject *getPythonMonkeyBigInt() { + if (!PythonMonkey_BigInt) { + PythonMonkey_BigInt = PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "bigint"); + } + return PythonMonkey_BigInt; +} + -typedef std::unordered_map *>>::iterator PyToGCIterator; typedef struct { PyObject_HEAD } NullObject; -std::unordered_map *>> PyTypeToGCThing; /**< data structure to hold memoized PyObject & GCThing data for handling GC*/ - static PyTypeObject NullType = { .ob_base = PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "pythonmonkey.null", .tp_basicsize = sizeof(NullObject), .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Javascript null object"), + .tp_doc = PyDoc_STR("Javascript null object") }; static PyTypeObject BigIntType = { - .tp_name = "pythonmonkey.bigint", - .tp_flags = Py_TPFLAGS_DEFAULT - | Py_TPFLAGS_LONG_SUBCLASS // https://docs.python.org/3/c-api/typeobj.html#Py_TPFLAGS_LONG_SUBCLASS - | Py_TPFLAGS_BASETYPE, // can be subclassed + .tp_name = PyLong_Type.tp_name, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_LONG_SUBCLASS, .tp_doc = PyDoc_STR("Javascript BigInt object"), - .tp_base = &PyLong_Type, // extending the builtin int type + .tp_base = &PyLong_Type }; PyTypeObject JSObjectProxyType = { .ob_base = PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "pythonmonkey.JSObjectProxy", + .tp_name = PyDict_Type.tp_name, .tp_basicsize = sizeof(JSObjectProxy), + .tp_itemsize = 0, .tp_dealloc = (destructor)JSObjectProxyMethodDefinitions::JSObjectProxy_dealloc, .tp_repr = (reprfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_repr, + .tp_as_number = &JSObjectProxy_number_methods, + .tp_as_sequence = &JSObjectProxy_sequence_methods, .tp_as_mapping = &JSObjectProxy_mapping_methods, .tp_getattro = (getattrofunc)JSObjectProxyMethodDefinitions::JSObjectProxy_get, .tp_setattro = (setattrofunc)JSObjectProxyMethodDefinitions::JSObjectProxy_assign, - .tp_flags = Py_TPFLAGS_DEFAULT - | Py_TPFLAGS_DICT_SUBCLASS, // https://docs.python.org/3/c-api/typeobj.html#Py_TPFLAGS_DICT_SUBCLASS + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DICT_SUBCLASS | Py_TPFLAGS_HAVE_GC, .tp_doc = PyDoc_STR("Javascript Object proxy dict"), + .tp_traverse = (traverseproc)JSObjectProxyMethodDefinitions::JSObjectProxy_traverse, + .tp_clear = (inquiry)JSObjectProxyMethodDefinitions::JSObjectProxy_clear, .tp_richcompare = (richcmpfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_richcompare, .tp_iter = (getiterfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_iter, - .tp_base = &PyDict_Type, - .tp_init = (initproc)JSObjectProxyMethodDefinitions::JSObjectProxy_init, - .tp_new = JSObjectProxyMethodDefinitions::JSObjectProxy_new, + .tp_iternext = (iternextfunc)JSObjectProxyMethodDefinitions::JSObjectProxy_iter_next, + .tp_methods = JSObjectProxy_methods, + .tp_base = &PyDict_Type +}; + +PyTypeObject JSStringProxyType = { + .tp_name = PyUnicode_Type.tp_name, + .tp_basicsize = sizeof(JSStringProxy), + .tp_dealloc = (destructor)JSStringProxyMethodDefinitions::JSStringProxy_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_UNICODE_SUBCLASS, + .tp_doc = PyDoc_STR("Javascript String proxy"), + .tp_methods = JSStringProxy_methods, + .tp_base = &PyUnicode_Type +}; + +PyTypeObject JSFunctionProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pythonmonkey.JSFunctionProxy", + .tp_basicsize = sizeof(JSFunctionProxy), + .tp_dealloc = (destructor)JSFunctionProxyMethodDefinitions::JSFunctionProxy_dealloc, + .tp_call = JSFunctionProxyMethodDefinitions::JSFunctionProxy_call, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("Javascript Function proxy object"), + .tp_new = JSFunctionProxyMethodDefinitions::JSFunctionProxy_new +}; + +PyTypeObject JSMethodProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "pythonmonkey.JSMethodProxy", + .tp_basicsize = sizeof(JSMethodProxy), + .tp_dealloc = (destructor)JSMethodProxyMethodDefinitions::JSMethodProxy_dealloc, + .tp_call = JSMethodProxyMethodDefinitions::JSMethodProxy_call, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("Javascript Method proxy object"), + .tp_new = JSMethodProxyMethodDefinitions::JSMethodProxy_new +}; + +PyTypeObject JSArrayProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = PyList_Type.tp_name, + .tp_basicsize = sizeof(JSArrayProxy), + .tp_itemsize = 0, + .tp_dealloc = (destructor)JSArrayProxyMethodDefinitions::JSArrayProxy_dealloc, + .tp_repr = (reprfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_repr, + .tp_as_sequence = &JSArrayProxy_sequence_methods, + .tp_as_mapping = &JSArrayProxy_mapping_methods, + .tp_getattro = (getattrofunc)JSArrayProxyMethodDefinitions::JSArrayProxy_get, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_LIST_SUBCLASS | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR("Javascript Array proxy list"), + .tp_traverse = (traverseproc)JSArrayProxyMethodDefinitions::JSArrayProxy_traverse, + .tp_clear = (inquiry)JSArrayProxyMethodDefinitions::JSArrayProxy_clear, + .tp_richcompare = (richcmpfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_richcompare, + .tp_iter = (getiterfunc)JSArrayProxyMethodDefinitions::JSArrayProxy_iter, + .tp_methods = JSArrayProxy_methods, + .tp_base = &PyList_Type +}; + +PyTypeObject JSArrayIterProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = PyListIter_Type.tp_name, + .tp_basicsize = sizeof(JSArrayIterProxy), + .tp_itemsize = 0, + .tp_dealloc = (destructor)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_dealloc, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR("Javascript Array proxy iterator"), + .tp_traverse = (traverseproc)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_traverse, + .tp_clear = (inquiry)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_clear, + .tp_iter = (getiterfunc)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_iter, + .tp_iternext = (iternextfunc)JSArrayIterProxyMethodDefinitions::JSArrayIterProxy_next, + .tp_methods = JSArrayIterProxy_methods, + .tp_base = &PyListIter_Type +}; + +PyTypeObject JSObjectIterProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = PyDictIterKey_Type.tp_name, + .tp_basicsize = sizeof(JSObjectIterProxy), + .tp_itemsize = 0, + .tp_dealloc = (destructor)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_dealloc, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR("Javascript Object proxy key iterator"), + .tp_traverse = (traverseproc)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_traverse, + .tp_clear = (inquiry)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_clear, + .tp_iter = (getiterfunc)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_iter, + .tp_iternext = (iternextfunc)JSObjectIterProxyMethodDefinitions::JSObjectIterProxy_nextkey, + .tp_methods = JSObjectIterProxy_methods, + .tp_base = &PyDictIterKey_Type +}; + +PyTypeObject JSObjectKeysProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = PyDictKeys_Type.tp_name, + .tp_basicsize = sizeof(JSObjectKeysProxy), + .tp_itemsize = 0, + .tp_dealloc = (destructor)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_dealloc, + .tp_repr = (reprfunc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_repr, + .tp_as_number = &JSObjectKeysProxy_number_methods, + .tp_as_sequence = &JSObjectKeysProxy_sequence_methods, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR("Javascript Object Keys proxy"), + .tp_traverse = (traverseproc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_traverse, + .tp_clear = (inquiry)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_clear, + .tp_richcompare = (richcmpfunc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_richcompare, + .tp_iter = (getiterfunc)JSObjectKeysProxyMethodDefinitions::JSObjectKeysProxy_iter, + .tp_methods = JSObjectKeysProxy_methods, + .tp_getset = JSObjectKeysProxy_getset, + .tp_base = &PyDictKeys_Type +}; + +PyTypeObject JSObjectValuesProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = PyDictValues_Type.tp_name, + .tp_basicsize = sizeof(JSObjectValuesProxy), + .tp_itemsize = 0, + .tp_dealloc = (destructor)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_dealloc, + .tp_repr = (reprfunc)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_repr, + .tp_as_sequence = &JSObjectValuesProxy_sequence_methods, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR("Javascript Object Values proxy"), + .tp_traverse = (traverseproc)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_traverse, + .tp_clear = (inquiry)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_clear, + .tp_iter = (getiterfunc)JSObjectValuesProxyMethodDefinitions::JSObjectValuesProxy_iter, + .tp_methods = JSObjectValuesProxy_methods, + .tp_getset = JSObjectValuesProxy_getset, + .tp_base = &PyDictValues_Type +}; + +PyTypeObject JSObjectItemsProxyType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = PyDictKeys_Type.tp_name, + .tp_basicsize = sizeof(JSObjectItemsProxy), + .tp_itemsize = 0, + .tp_dealloc = (destructor)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_dealloc, + .tp_repr = (reprfunc)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_repr, + // .tp_as_number = defaults are fine + .tp_as_sequence = &JSObjectItemsProxy_sequence_methods, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR("Javascript Object Items proxy"), + .tp_traverse = (traverseproc)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_traverse, + .tp_clear = (inquiry)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_clear, + // .tp_richcompare = TODO tuple support + .tp_iter = (getiterfunc)JSObjectItemsProxyMethodDefinitions::JSObjectItemsProxy_iter, + .tp_methods = JSObjectItemsProxy_methods, + .tp_getset = JSObjectItemsProxy_getset, + .tp_base = &PyDictKeys_Type }; static void cleanup() { + // Clean up the PythonMonkey module + Py_XDECREF(PythonMonkey_Null); + Py_XDECREF(PythonMonkey_BigInt); + + // Clean up SpiderMonkey delete autoRealm; delete global; + if (GLOBAL_CX) { + JS_DestroyContext(GLOBAL_CX); + GLOBAL_CX = nullptr; + } delete JOB_QUEUE; - if (GLOBAL_CX) JS_DestroyContext(GLOBAL_CX); JS_ShutDown(); } - -void memoizePyTypeAndGCThing(PyType *pyType, JS::Handle GCThing) { - JS::PersistentRooted *RootedGCThing = new JS::PersistentRooted(GLOBAL_CX, GCThing); - PyToGCIterator pyIt = PyTypeToGCThing.find(pyType); - - if (pyIt == PyTypeToGCThing.end()) { // if the PythonObject is not memoized - std::vector *> gcVector( - {{RootedGCThing}}); - PyTypeToGCThing.insert({{pyType, gcVector}}); - } - else { - pyIt->second.push_back(RootedGCThing); - } +static void cleanup(PyObject *) { + cleanup(); } -void handleSharedPythonMonkeyMemory(JSContext *cx, JSGCStatus status, JS::GCReason reason, void *data) { - if (status == JSGCStatus::JSGC_BEGIN) { - PyToGCIterator pyIt = PyTypeToGCThing.begin(); - while (pyIt != PyTypeToGCThing.end()) { - PyObject *pyObj = pyIt->first->getPyObject(); - // If the PyObject reference count is exactly 1, then the only reference to the object is the one - // we are holding, which means the object is ready to be free'd. - if (_PyGC_FINALIZED(pyObj) || pyObj->ob_refcnt == 1) { // PyObject_GC_IsFinalized is only available in Python 3.9+ - for (JS::PersistentRooted *rval: pyIt->second) { // for each related GCThing - bool found = false; - for (PyToGCIterator innerPyIt = PyTypeToGCThing.begin(); innerPyIt != PyTypeToGCThing.end(); innerPyIt++) { // for each other PyType pointer - if (innerPyIt != pyIt && std::find(innerPyIt->second.begin(), innerPyIt->second.end(), rval) != innerPyIt->second.end()) { // if the PyType is also related to the GCThing - found = true; - break; - } - } - // if this PyObject is the last PyObject that references this GCThing, then the GCThing can also be free'd - if (!found) { - delete rval; - } - } - pyIt = PyTypeToGCThing.erase(pyIt); - } - else { - pyIt++; - } - } - } -}; - static PyObject *collect(PyObject *self, PyObject *args) { JS_GC(GLOBAL_CX); Py_RETURN_NONE; @@ -146,46 +328,93 @@ static PyObject *collect(PyObject *self, PyObject *args) { static bool getEvalOption(PyObject *evalOptions, const char *optionName, const char **s_p) { PyObject *value; - - value = PyDict_GetItemString(evalOptions, optionName); - if (value) - *s_p = PyUnicode_AsUTF8(value); - return value != NULL; + if (PyObject_TypeCheck(evalOptions, &JSObjectProxyType)) { + value = PyMapping_GetItemString(evalOptions, optionName); + } else { + value = PyDict_GetItemString(evalOptions, optionName); + } + if (value && value != Py_None) { + *s_p = PyUnicode_AsUTF8(PyUnicode_FromObject(value)); + } + return value != NULL && value != Py_None; } static bool getEvalOption(PyObject *evalOptions, const char *optionName, unsigned long *l_p) { PyObject *value; - - value = PyDict_GetItemString(evalOptions, optionName); - if (value) - *l_p = PyLong_AsUnsignedLong(value); - return value != NULL; + if (PyObject_TypeCheck(evalOptions, &JSObjectProxyType)) { + value = PyMapping_GetItemString(evalOptions, optionName); + if (value && value != Py_None) { + *l_p = (unsigned long)PyFloat_AsDouble(value); + } + } else { + value = PyDict_GetItemString(evalOptions, optionName); + if (value && value != Py_None) { + *l_p = PyLong_AsUnsignedLong(value); + } + } + return value != NULL && value != Py_None; } static bool getEvalOption(PyObject *evalOptions, const char *optionName, bool *b_p) { PyObject *value; - - value = PyDict_GetItemString(evalOptions, optionName); - if (value) + if (PyObject_TypeCheck(evalOptions, &JSObjectProxyType)) { + value = PyMapping_GetItemString(evalOptions, optionName); + } else { + value = PyDict_GetItemString(evalOptions, optionName); + } + if (value && value != Py_None) { *b_p = PyObject_IsTrue(value) == 1 ? true : false; - return value != NULL; + } + return value != NULL && value != Py_None; } +/** + * Implement the pythonmonkey.eval function. From Python-land, that function has the following API: + * argument 0 - unicode string of JS code or open file containing JS code in UTF-8 + * argument 1 - a Dict of options which roughly correspond to the jsapi CompileOptions. A novel option, + * fromPythonFrame, sets the filename and line offset according to the pm.eval call in the + * Python source code. This allows us to embed non-trivial JS inside Python source files + * and still get stack dumps which point to the source code. + */ static PyObject *eval(PyObject *self, PyObject *args) { size_t argc = PyTuple_GET_SIZE(args); - StrType *code = new StrType(PyTuple_GetItem(args, 0)); - PyObject *evalOptions = argc == 2 ? PyTuple_GetItem(args, 1) : NULL; + if (argc > 2 || argc == 0) { + PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval accepts one or two arguments"); + return NULL; + } - if (argc == 0 || !PyUnicode_Check(code->getPyObject())) { - PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a string as its first argument"); + PyObject *code = NULL; + FILE *file = NULL; + PyObject *arg0 = PyTuple_GetItem(args, 0); + PyObject *arg1 = argc == 2 ? PyTuple_GetItem(args, 1) : NULL; + + if (PyUnicode_Check(arg0)) { + code = arg0; + } else if (1 /*PyFile_Check(arg0)*/) { + /* First argument is an open file. Open a stream with a dup of the underlying fd (so we can fclose + * the stream later). Future: seek to current Python file position IFF the fd is for a real file. + */ + int fd = PyObject_AsFileDescriptor(arg0); + int fd2 = fd == -1 ? -1 : dup(fd); + file = fd2 == -1 ? NULL : fdopen(fd, "rb"); + if (!file) { + PyErr_SetString(PyExc_TypeError, "error opening file stream"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects either a string or an open file as its first argument"); return NULL; } + PyObject *evalOptions = argc == 2 ? arg1 : NULL; if (evalOptions && !PyDict_Check(evalOptions)) { - PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a dict as its (optional) second argument"); + PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a dict as its second argument"); + if (file) + fclose(file); return NULL; } + // initialize JS context JSAutoRealm ar(GLOBAL_CX, *global); JS::CompileOptions options (GLOBAL_CX); options.setFileAndLine("evaluate", 1) @@ -200,7 +429,7 @@ static PyObject *eval(PyObject *self, PyObject *args) { if (getEvalOption(evalOptions, "filename", &s)) options.setFile(s); if (getEvalOption(evalOptions, "lineno", &l)) options.setLine(l); - if (getEvalOption(evalOptions, "column", &l)) options.setColumn(l); + if (getEvalOption(evalOptions, "column", &l)) options.setColumn(JS::ColumnNumberOneOrigin(l)); if (getEvalOption(evalOptions, "mutedErrors", &b)) options.setMutedErrors(b); if (getEvalOption(evalOptions, "noScriptRval", &b)) options.setNoScriptRval(b); if (getEvalOption(evalOptions, "selfHosting", &b)) options.setSelfHostingMode(b); @@ -223,48 +452,50 @@ static PyObject *eval(PyObject *self, PyObject *args) { #endif if (!getEvalOption(evalOptions, "filename", &s)) { if (filename && PyUnicode_Check(filename)) { - options.setFile(PyUnicode_AsUTF8(filename)); + PyObject *filenameStr = PyUnicode_FromObject(filename); // needs a strict Python str object (not a subtype) + options.setFile(PyUnicode_AsUTF8(filenameStr)); } } /* filename */ } /* fromPythonFrame */ } /* eval options */ - // initialize JS context - JS::SourceText source; - if (!source.init(GLOBAL_CX, code->getValue(), strlen(code->getValue()), JS::SourceOwnership::Borrowed)) { + + // compile the code to execute + JS::RootedScript script(GLOBAL_CX); + JS::Rooted rval(GLOBAL_CX); + if (code) { + JS::SourceText source; + Py_ssize_t codeLength; + const char *codeChars = PyUnicode_AsUTF8AndSize(code, &codeLength); + if (!source.init(GLOBAL_CX, codeChars, codeLength, JS::SourceOwnership::Borrowed)) { + setSpiderMonkeyException(GLOBAL_CX); + return NULL; + } + script = JS::Compile(GLOBAL_CX, options, source); + } else { + assert(file); + script = JS::CompileUtf8File(GLOBAL_CX, options, file); + fclose(file); + } + + if (!script) { setSpiderMonkeyException(GLOBAL_CX); return NULL; } - delete code; - // evaluate source code - JS::Rooted *rval = new JS::Rooted(GLOBAL_CX); - if (!JS::Evaluate(GLOBAL_CX, options, source, rval)) { + // execute the compiled code; last expr goes to rval + if (!JS_ExecuteScript(GLOBAL_CX, script, &rval)) { setSpiderMonkeyException(GLOBAL_CX); return NULL; } // translate to the proper python type - PyType *returnValue = pyTypeFactory(GLOBAL_CX, global, rval); + PyObject *returnValue = pyTypeFactory(GLOBAL_CX, rval); if (PyErr_Occurred()) { return NULL; } - // TODO: Find a better way to destroy the root when necessary (when the returned Python object is GCed). - js::ESClass cls = js::ESClass::Other; // placeholder if `rval` is not a JSObject - if (rval->isObject()) { - JS::GetBuiltinClass(GLOBAL_CX, JS::RootedObject(GLOBAL_CX, &rval->toObject()), &cls); - if (JS_ObjectIsBoundFunction(&rval->toObject())) { - cls = js::ESClass::Function; // In SpiderMonkey 115 ESR, bound function is no longer a JSFunction but a js::BoundFunctionObject. - } - } - bool rvalIsFunction = cls == js::ESClass::Function; // function object - bool rvalIsString = rval->isString() || cls == js::ESClass::String; // string primitive or boxed String object - if (!(rvalIsFunction || rvalIsString)) { // rval may be a JS function or string which must be kept alive. - delete rval; - } - if (returnValue) { - return returnValue->getPyObject(); + return returnValue; } else { Py_RETURN_NONE; @@ -282,30 +513,34 @@ static PyObject *waitForEventLoop(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED return PyObject_CallMethod(waiter, "wait", NULL); } -static PyObject *isCompilableUnit(PyObject *self, PyObject *args) { - StrType *buffer = new StrType(PyTuple_GetItem(args, 0)); - const char *bufferUtf8; - bool compilable; +static PyObject *closeAllPending(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(_)) { + if (!PyEventLoop::AsyncHandle::cancelAll()) return NULL; + Py_RETURN_NONE; +} - if (!PyUnicode_Check(buffer->getPyObject())) { +static PyObject *isCompilableUnit(PyObject *self, PyObject *args) { + PyObject *item = PyTuple_GetItem(args, 0); + if (!PyUnicode_Check(item)) { PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a string as its first argument"); return NULL; } - bufferUtf8 = buffer->getValue(); - compilable = JS_Utf8BufferIsCompilableUnit(GLOBAL_CX, *global, bufferUtf8, strlen(bufferUtf8)); + Py_ssize_t bufferLength; + const char *bufferUtf8 = PyUnicode_AsUTF8AndSize(item, &bufferLength); - if (compilable) + if (JS_Utf8BufferIsCompilableUnit(GLOBAL_CX, *global, bufferUtf8, bufferLength)) { Py_RETURN_TRUE; - else + } else { Py_RETURN_FALSE; + } } PyMethodDef PythonMonkeyMethods[] = { {"eval", eval, METH_VARARGS, "Javascript evaluator in Python"}, {"wait", waitForEventLoop, METH_NOARGS, "The event-loop shield. Blocks until all asynchronous jobs finish."}, + {"stop", closeAllPending, METH_NOARGS, "Cancel all pending event-loop jobs."}, {"isCompilableUnit", isCompilableUnit, METH_VARARGS, "Hint if a string might be compilable Javascript"}, - {"collect", collect, METH_VARARGS, "Calls the spidermonkey garbage collector"}, + {"collect", collect, METH_VARARGS, "Calls the Spidermonkey garbage collector"}, {NULL, NULL, 0, NULL} }; @@ -329,7 +564,6 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) PyErr_SetString(SpiderMonkeyError, "Spidermonkey could not be initialized."); return NULL; } - Py_AtExit(cleanup); GLOBAL_CX = JS_NewContext(JS::DefaultHeapMaxBytes); if (!GLOBAL_CX) { @@ -343,7 +577,7 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) .setAsyncStack(true) .setSourcePragmas(true); - JOB_QUEUE = new JobQueue(); + JOB_QUEUE = new JobQueue(GLOBAL_CX); if (!JOB_QUEUE->init(GLOBAL_CX)) { PyErr_SetString(SpiderMonkeyError, "Spidermonkey could not create the event-loop."); return NULL; @@ -354,7 +588,14 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return NULL; } - JS::RealmOptions options; + JS_SetGCParameter(GLOBAL_CX, JSGC_MAX_BYTES, (uint32_t)-1); + + JS_SetGCCallback(GLOBAL_CX, pythonmonkeyGCCallback, NULL); + JS::AddGCNurseryCollectionCallback(GLOBAL_CX, nurseryCollectionCallback, NULL); + + JS::RealmCreationOptions creationOptions = JS::RealmCreationOptions(); + JS::RealmBehaviors behaviours = JS::RealmBehaviors(); + JS::RealmOptions options = JS::RealmOptions(creationOptions, behaviours); static JSClass globalClass = {"global", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps}; global = new JS::RootedObject(GLOBAL_CX, JS_NewGlobalObject(GLOBAL_CX, &globalClass, nullptr, JS::FireOnNewGlobalHook, options)); if (!global) { @@ -365,15 +606,19 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) JS::RootedObject debuggerGlobal(GLOBAL_CX, JS_NewGlobalObject(GLOBAL_CX, &globalClass, nullptr, JS::FireOnNewGlobalHook, options)); { JSAutoRealm r(GLOBAL_CX, debuggerGlobal); - JS_DefineProperty(GLOBAL_CX, debuggerGlobal, "mainGlobal", *global, JSPROP_READONLY); JS_DefineDebuggerObject(GLOBAL_CX, debuggerGlobal); } + { + JSAutoRealm r(GLOBAL_CX, *global); + JS::Rooted desc(GLOBAL_CX, JS::PropertyDescriptor::Data( + JS::ObjectValue(*debuggerGlobal) + )); + JS_WrapPropertyDescriptor(GLOBAL_CX, &desc); + JS_DefineUCProperty(GLOBAL_CX, *global, u"debuggerGlobal", 14, desc); + } autoRealm = new JSAutoRealm(GLOBAL_CX, *global); - JS_SetGCCallback(GLOBAL_CX, handleSharedPythonMonkeyMemory, NULL); - JS_DefineProperty(GLOBAL_CX, *global, "debuggerGlobal", debuggerGlobal, JSPROP_READONLY); - // XXX: SpiderMonkey bug??? // In https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/js/src/jit/CacheIR.cpp#l317, trying to use the callback returned by `js::GetDOMProxyShadowsCheck()` even it's unset (nullptr) // Temporarily solved by explicitly setting the `domProxyShadowsCheck` callback here @@ -382,24 +627,52 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return JS::DOMProxyShadowsResult::ShadowCheckFailed; }, nullptr); - PyObject *pyModule; if (PyType_Ready(&NullType) < 0) return NULL; if (PyType_Ready(&BigIntType) < 0) return NULL; if (PyType_Ready(&JSObjectProxyType) < 0) return NULL; + if (PyType_Ready(&JSStringProxyType) < 0) + return NULL; + if (PyType_Ready(&JSFunctionProxyType) < 0) + return NULL; + if (PyType_Ready(&JSMethodProxyType) < 0) + return NULL; + if (PyType_Ready(&JSArrayProxyType) < 0) + return NULL; + if (PyType_Ready(&JSArrayIterProxyType) < 0) + return NULL; + if (PyType_Ready(&JSObjectIterProxyType) < 0) + return NULL; + if (PyType_Ready(&JSObjectKeysProxyType) < 0) + return NULL; + if (PyType_Ready(&JSObjectValuesProxyType) < 0) + return NULL; + if (PyType_Ready(&JSObjectItemsProxyType) < 0) + return NULL; - pyModule = PyModule_Create(&pythonmonkey); + PyObject *pyModule = PyModule_Create(&pythonmonkey); if (pyModule == NULL) return NULL; + // Clean up SpiderMonkey when the PythonMonkey module gets destroyed (module.___cleanup is GCed) + // The `cleanup` function will be called automatically when this PyCapsule gets GCed + // We cannot use `Py_AtExit(cleanup);` because the GIL is unavailable after Python finalization, no more Python APIs can be called. + PyObject *autoDestructor = PyCapsule_New(&pythonmonkey, NULL, cleanup); + if (PyModule_AddObject(pyModule, "___cleanup", autoDestructor) < 0) { + Py_DECREF(autoDestructor); + Py_DECREF(pyModule); + return NULL; + } + Py_INCREF(&NullType); if (PyModule_AddObject(pyModule, "null", (PyObject *)&NullType) < 0) { Py_DECREF(&NullType); Py_DECREF(pyModule); return NULL; } + Py_INCREF(&BigIntType); if (PyModule_AddObject(pyModule, "bigint", (PyObject *)&BigIntType) < 0) { Py_DECREF(&BigIntType); @@ -414,7 +687,70 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return NULL; } - if (PyModule_AddObject(pyModule, "SpiderMonkeyError", SpiderMonkeyError)) { + Py_INCREF(&JSStringProxyType); + if (PyModule_AddObject(pyModule, "JSStringProxy", (PyObject *)&JSStringProxyType) < 0) { + Py_DECREF(&JSStringProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSArrayProxyType); + if (PyModule_AddObject(pyModule, "JSArrayProxy", (PyObject *)&JSArrayProxyType) < 0) { + Py_DECREF(&JSArrayProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSFunctionProxyType); + if (PyModule_AddObject(pyModule, "JSFunctionProxy", (PyObject *)&JSFunctionProxyType) < 0) { + Py_DECREF(&JSFunctionProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSArrayIterProxyType); + if (PyModule_AddObject(pyModule, "JSArrayIterProxy", (PyObject *)&JSArrayIterProxyType) < 0) { + Py_DECREF(&JSArrayIterProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSMethodProxyType); + if (PyModule_AddObject(pyModule, "JSMethodProxy", (PyObject *)&JSMethodProxyType) < 0) { + Py_DECREF(&JSMethodProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSObjectIterProxyType); + if (PyModule_AddObject(pyModule, "JSObjectIterProxy", (PyObject *)&JSObjectIterProxyType) < 0) { + Py_DECREF(&JSObjectIterProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSObjectKeysProxyType); + if (PyModule_AddObject(pyModule, "JSObjectKeysProxy", (PyObject *)&JSObjectKeysProxyType) < 0) { + Py_DECREF(&JSObjectKeysProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSObjectValuesProxyType); + if (PyModule_AddObject(pyModule, "JSObjectValuesProxy", (PyObject *)&JSObjectValuesProxyType) < 0) { + Py_DECREF(&JSObjectValuesProxyType); + Py_DECREF(pyModule); + return NULL; + } + + Py_INCREF(&JSObjectItemsProxyType); + if (PyModule_AddObject(pyModule, "JSObjectItemsProxy", (PyObject *)&JSObjectItemsProxyType) < 0) { + Py_DECREF(&JSObjectItemsProxyType); + Py_DECREF(pyModule); + return NULL; + } + + if (PyModule_AddObject(pyModule, "SpiderMonkeyError", SpiderMonkeyError) < 0) { Py_DECREF(pyModule); return NULL; } @@ -429,5 +765,23 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return NULL; } + // initialize FinalizationRegistry of JSFunctions to Python Functions + JS::RootedValue FinalizationRegistry(GLOBAL_CX); + JS::RootedObject registryObject(GLOBAL_CX); + + JS_GetProperty(GLOBAL_CX, *global, "FinalizationRegistry", &FinalizationRegistry); + JS::Rooted> args(GLOBAL_CX); + JSFunction *registryCallback = JS_NewFunction(GLOBAL_CX, functionRegistryCallback, 1, 0, NULL); + JS::RootedObject registryCallbackObject(GLOBAL_CX, JS_GetFunctionObject(registryCallback)); + args[0].setObject(*registryCallbackObject); + if (!JS::Construct(GLOBAL_CX, FinalizationRegistry, args, ®istryObject)) { + setSpiderMonkeyException(GLOBAL_CX); + return NULL; + } + jsFunctionRegistry.init(GLOBAL_CX); + jsFunctionRegistry.set(registryObject); + + JS::SetHostCleanupFinalizationRegistryCallback(GLOBAL_CX, cleanupFinalizationRegistry, NULL); + return pyModule; -} +} \ No newline at end of file diff --git a/src/pyTypeFactory.cc b/src/pyTypeFactory.cc index 5a4aec6e..c11f0c1f 100644 --- a/src/pyTypeFactory.cc +++ b/src/pyTypeFactory.cc @@ -1,11 +1,10 @@ /** * @file pyTypeFactory.cc - * @author Caleb Aikens (caleb@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) and Philippe Laporte (philippe@distributive.network) * @brief Function for wrapping arbitrary PyObjects into the appropriate PyType class, and coercing JS types to python types - * @version 0.1 * @date 2023-03-29 * - * @copyright Copyright (c) 2023 + * @copyright 2023-2024 Distributive Corp. * */ @@ -24,11 +23,13 @@ #include "include/NoneType.hh" #include "include/NullType.hh" #include "include/PromiseType.hh" -#include "include/PyProxyHandler.hh" -#include "include/PyType.hh" +#include "include/PyDictProxyHandler.hh" +#include "include/PyListProxyHandler.hh" +#include "include/PyObjectProxyHandler.hh" +#include "include/PyIterableProxyHandler.hh" +#include "include/PyBytesProxyHandler.hh" #include "include/setSpiderMonkeyException.hh" #include "include/StrType.hh" -#include "include/TupleType.hh" #include "include/modules/pythonmonkey/pythonmonkey.hh" #include @@ -36,181 +37,101 @@ #include #include -#include +PyObject *pyTypeFactory(JSContext *cx, JS::HandleValue rval) { + std::string errorString; -// TODO (Caleb Aikens) get below properties -static PyMethodDef callJSFuncDef = {"JSFunctionCallable", callJSFunc, METH_VARARGS, NULL}; - -PyType *pyTypeFactory(PyObject *object) { - PyType *pyType; - - if (PyLong_Check(object)) { - pyType = new IntType(object); - } - else if (PyUnicode_Check(object)) { - pyType = new StrType(object); - } - else if (PyFunction_Check(object)) { - pyType = new FuncType(object); - } - else if (PyDict_Check(object)) { - pyType = new DictType(object); + if (rval.isUndefined()) { + return NoneType::getPyObject(); } - else if (PyList_Check(object)) { - pyType = new ListType(object); + else if (rval.isNull()) { + return NullType::getPyObject(); } - else if (PyTuple_Check(object)) { - pyType = new TupleType(object); - } - else { - return nullptr; - } - - return pyType; -} - -PyType *pyTypeFactory(JSContext *cx, JS::Rooted *thisObj, JS::Rooted *rval) { - if (rval->isUndefined()) { - return new NoneType(); + else if (rval.isBoolean()) { + return BoolType::getPyObject(rval.toBoolean()); } - else if (rval->isNull()) { - return new NullType(); + else if (rval.isNumber()) { + return FloatType::getPyObject(rval.toNumber()); } - else if (rval->isBoolean()) { - return new BoolType(rval->toBoolean()); + else if (rval.isString()) { + return StrType::getPyObject(cx, rval); } - else if (rval->isNumber()) { - return new FloatType(rval->toNumber()); + else if (rval.isSymbol()) { + errorString = "symbol type is not handled by PythonMonkey yet.\n"; } - else if (rval->isString()) { - StrType *s = new StrType(cx, rval->toString()); - memoizePyTypeAndGCThing(s, *rval); // TODO (Caleb Aikens) consider putting this in the StrType constructor - return s; + else if (rval.isBigInt()) { + return IntType::getPyObject(cx, rval.toBigInt()); } - else if (rval->isSymbol()) { - printf("symbol type is not handled by PythonMonkey yet"); - } - else if (rval->isBigInt()) { - return new IntType(cx, rval->toBigInt()); - } - else if (rval->isObject()) { + else if (rval.isObject()) { JS::Rooted obj(cx); - JS_ValueToObject(cx, *rval, &obj); + JS_ValueToObject(cx, rval, &obj); + if (JS::GetClass(obj)->isProxyObject()) { - if (js::GetProxyHandler(obj)->family() == &PyProxyHandler::family) { // this is one of our proxies for python dicts - return new DictType(((PyProxyHandler *)js::GetProxyHandler(obj))->pyObject); - } - if (js::GetProxyHandler(obj)->family() == &PyListProxyHandler::family) { // this is one of our proxies for python lists - return new ListType(((PyListProxyHandler *)js::GetProxyHandler(obj))->pyObject); + if (js::GetProxyHandler(obj)->family() == &PyDictProxyHandler::family || // this is one of our proxies for python dicts + js::GetProxyHandler(obj)->family() == &PyListProxyHandler::family || // this is one of our proxies for python lists + js::GetProxyHandler(obj)->family() == &PyIterableProxyHandler::family || // this is one of our proxies for python iterables + js::GetProxyHandler(obj)->family() == &PyObjectProxyHandler::family || // this is one of our proxies for python iterables + js::GetProxyHandler(obj)->family() == &PyBytesProxyHandler::family) { // this is one of our proxies for python bytes objects + + PyObject *pyObject = JS::GetMaybePtrFromReservedSlot(obj, PyObjectSlot); + Py_INCREF(pyObject); + return pyObject; } } + js::ESClass cls; JS::GetBuiltinClass(cx, obj, &cls); if (JS_ObjectIsBoundFunction(obj)) { cls = js::ESClass::Function; // In SpiderMonkey 115 ESR, bound function is no longer a JSFunction but a js::BoundFunctionObject. // js::ESClass::Function only assigns to JSFunction objects by JS::GetBuiltinClass. } + + JS::RootedValue unboxed(cx); switch (cls) { - case js::ESClass::Boolean: { - // TODO (Caleb Aikens): refactor out all `js::Unbox` calls - // TODO (Caleb Aikens): refactor using recursive call to `pyTypeFactory` - JS::RootedValue unboxed(cx); - js::Unbox(cx, obj, &unboxed); - return new BoolType(unboxed.toBoolean()); - } - case js::ESClass::Date: { - return new DateType(cx, obj); - } - case js::ESClass::Promise: { - return new PromiseType(cx, obj); - } - case js::ESClass::Error: { - return new ExceptionType(cx, obj); - } + case js::ESClass::Boolean: + case js::ESClass::Number: + case js::ESClass::BigInt: + case js::ESClass::String: + js::Unbox(cx, obj, &unboxed); + return pyTypeFactory(cx, unboxed); + case js::ESClass::Date: + return DateType::getPyObject(cx, obj); + case js::ESClass::Promise: + return PromiseType::getPyObject(cx, obj); + case js::ESClass::Error: + return ExceptionType::getPyObject(cx, obj); case js::ESClass::Function: { - PyObject *pyFunc; if (JS_IsNativeFunction(obj, callPyFunc)) { // It's a wrapped python function by us // Get the underlying python function from the 0th reserved slot JS::Value pyFuncVal = js::GetFunctionNativeReserved(obj, 0); - pyFunc = (PyObject *)(pyFuncVal.toPrivate()); + PyObject *pyFunc = (PyObject *)(pyFuncVal.toPrivate()); + Py_INCREF(pyFunc); + return pyFunc; } else { - // FIXME (Tom Tang): `jsCxThisFuncTuple` and the tuple items are not going to be GCed - PyObject *jsCxThisFuncTuple = PyTuple_Pack(3, PyLong_FromVoidPtr(cx), PyLong_FromVoidPtr(thisObj), PyLong_FromVoidPtr(rval)); - pyFunc = PyCFunction_New(&callJSFuncDef, jsCxThisFuncTuple); + return FuncType::getPyObject(cx, rval); } - FuncType *f = new FuncType(pyFunc); - memoizePyTypeAndGCThing(f, *rval); // TODO (Caleb Aikens) consider putting this in the FuncType constructor - return f; - } - case js::ESClass::Number: { - JS::RootedValue unboxed(cx); - js::Unbox(cx, obj, &unboxed); - return new FloatType(unboxed.toNumber()); } - case js::ESClass::BigInt: { - JS::RootedValue unboxed(cx); - js::Unbox(cx, obj, &unboxed); - return new IntType(cx, unboxed.toBigInt()); - } - case js::ESClass::String: { - JS::RootedValue unboxed(cx); - js::Unbox(cx, obj, &unboxed); - StrType *s = new StrType(cx, unboxed.toString()); - memoizePyTypeAndGCThing(s, *rval); // TODO (Caleb Aikens) consider putting this in the StrType constructor - return s; - } - default: { - if (BufferType::isSupportedJsTypes(obj)) { // TypedArray or ArrayBuffer - // TODO (Tom Tang): ArrayBuffers have cls == js::ESClass::ArrayBuffer - return new BufferType(cx, obj); - } + case js::ESClass::Array: + return ListType::getPyObject(cx, obj); + default: + if (BufferType::isSupportedJsTypes(obj)) { // TypedArray or ArrayBuffer + // TODO (Tom Tang): ArrayBuffers have cls == js::ESClass::ArrayBuffer + return BufferType::getPyObject(cx, obj); } } - return new DictType(cx, *rval); + return DictType::getPyObject(cx, rval); } - else if (rval->isMagic()) { - printf("magic type is not handled by PythonMonkey yet\n"); + else if (rval.isMagic()) { + errorString = "magic type is not handled by PythonMonkey yet.\n"; } - std::string errorString("pythonmonkey cannot yet convert Javascript value of: "); - JS::RootedString str(cx, JS::ToString(cx, *rval)); - errorString += JS_EncodeStringToUTF8(cx, str).get(); + errorString += "pythonmonkey cannot yet convert Javascript value of: "; + JSString *valToStr = JS::ToString(cx, rval); + if (!valToStr) { // `JS::ToString` returns `nullptr` for JS symbols, see https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/js/src/vm/StringType.cpp#l2208 + // TODO (Tom Tang): Revisit this once we have Symbol coercion support + valToStr = JS_ValueToSource(cx, rval); + } + JS::RootedString valToStrRooted(cx, valToStr); + errorString += JS_EncodeStringToUTF8(cx, valToStrRooted).get(); PyErr_SetString(PyExc_TypeError, errorString.c_str()); return NULL; -} - -PyType *pyTypeFactorySafe(JSContext *cx, JS::Rooted *thisObj, JS::Rooted *rval) { - PyType *v = pyTypeFactory(cx, thisObj, rval); - if (PyErr_Occurred()) { - // Clear Python error - PyErr_Clear(); - // Return `pythonmonkey.null` on error - return new NullType(); - } - return v; -} - -PyObject *callJSFunc(PyObject *jsCxThisFuncTuple, PyObject *args) { - // TODO (Caleb Aikens) convert PyObject *args to JS::Rooted JSargs - JSContext *cx = (JSContext *)PyLong_AsVoidPtr(PyTuple_GetItem(jsCxThisFuncTuple, 0)); - JS::RootedObject *thisObj = (JS::RootedObject *)PyLong_AsVoidPtr(PyTuple_GetItem(jsCxThisFuncTuple, 1)); - JS::RootedValue *jsFunc = (JS::RootedValue *)PyLong_AsVoidPtr(PyTuple_GetItem(jsCxThisFuncTuple, 2)); - - JS::RootedVector jsArgsVector(cx); - for (size_t i = 0; i < PyTuple_Size(args); i++) { - JS::Value jsValue = jsTypeFactory(cx, PyTuple_GetItem(args, i)); - if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control - return NULL; // Fail-fast - } - jsArgsVector.append(jsValue); - } - - JS::HandleValueArray jsArgs(jsArgsVector); - JS::Rooted *jsReturnVal = new JS::Rooted(cx); - if (!JS_CallFunctionValue(cx, *thisObj, *jsFunc, jsArgs, jsReturnVal)) { - setSpiderMonkeyException(cx); - return NULL; - } - - return pyTypeFactory(cx, thisObj, jsReturnVal)->getPyObject(); } \ No newline at end of file diff --git a/src/setSpiderMonkeyException.cc b/src/setSpiderMonkeyException.cc index 0412a847..6d9d10fa 100644 --- a/src/setSpiderMonkeyException.cc +++ b/src/setSpiderMonkeyException.cc @@ -2,16 +2,16 @@ * @file setSpiderMonkeyException.cc * @author Caleb Aikens (caleb@distributive.network) * @brief Call this function whenever a JS_* function call fails in order to set an appropriate python exception (remember to also return NULL) - * @version 0.1 * @date 2023-02-28 * - * @copyright Copyright (c) 2023 + * @copyright Copyright (c) 2023 Distributive Corp. * */ #include "include/modules/pythonmonkey/pythonmonkey.hh" #include "include/setSpiderMonkeyException.hh" #include "include/StrType.hh" +#include "include/DictType.hh" #include #include @@ -19,7 +19,7 @@ #include #include -PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack) { +PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionStack, bool printStack) { JS::ErrorReportBuilder reportBuilder(cx); if (!reportBuilder.init(cx, exceptionStack, JS::ErrorReportBuilder::WithSideEffects /* may call the `toString` method if an object is thrown */)) { return PyUnicode_FromString("Spidermonkey set an exception, but could not initialize the error report."); @@ -38,11 +38,15 @@ PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionS std::stringstream outStrStream; JSErrorReport *errorReport = reportBuilder.report(); - if (errorReport && errorReport->filename) { // `errorReport->filename` (the source file name) can be null + if (errorReport && !!errorReport->filename) { // `errorReport->filename` (the source file name) can be null std::string offsetSpaces(errorReport->tokenOffset(), ' '); // number of spaces equal to tokenOffset std::string linebuf; // the offending JS line of code (can be empty) - outStrStream << "Error in file " << errorReport->filename << ", on line " << errorReport->lineno << ":\n"; + /* *INDENT-OFF* */ + outStrStream << "Error in file " << errorReport->filename.c_str() + << ", on line " << errorReport->lineno + << ", column " << errorReport->column.oneOriginValue() << ":\n"; + /* *INDENT-ON* */ if (errorReport->linebuf()) { std::wstring_convert, char16_t> convert; std::u16string u16linebuf(errorReport->linebuf()); @@ -57,11 +61,14 @@ PyObject *getExceptionString(JSContext *cx, const JS::ExceptionStack &exceptionS // print out the SpiderMonkey error message outStrStream << reportBuilder.toStringResult().c_str() << "\n"; - JS::HandleObject stackObj = exceptionStack.stack(); - if (stackObj) { // stack can be null - JS::RootedString stackStr(cx); - BuildStackString(cx, nullptr, stackObj, &stackStr, /* indent */ 2, js::StackFormat::SpiderMonkey); - outStrStream << "Stack Trace: \n" << StrType(cx, stackStr).getValue(); + if (printStack) { + JS::RootedObject stackObj(cx, exceptionStack.stack()); + if (stackObj.get()) { + JS::RootedString stackStr(cx); + BuildStackString(cx, nullptr, stackObj, &stackStr, 2, js::StackFormat::SpiderMonkey); + JS::UniqueChars stackStrUtf8 = JS_EncodeStringToUTF8(cx, stackStr); + outStrStream << "Stack Trace:\n" << stackStrUtf8.get(); + } } return PyUnicode_FromString(outStrStream.str().c_str()); @@ -80,11 +87,35 @@ void setSpiderMonkeyException(JSContext *cx) { PyErr_SetString(SpiderMonkeyError, "Spidermonkey set an exception, but was unable to retrieve it."); return; } + + // check if it is a Python Exception and already has a stack trace + bool printStack = true; + + JS::RootedValue exn(cx); + if (JS_GetPendingException(cx, &exn)) { + if (exn.isObject()) { + JS::RootedObject exnObj(cx, &exn.toObject()); + JS::RootedValue tmp(cx); + if (JS_GetProperty(cx, exnObj, "message", &tmp) && tmp.isString()) { + JS::RootedString rootedStr(cx, tmp.toString()); + printStack = strstr(JS_EncodeStringToUTF8(cx, rootedStr).get(), "JS Stack Trace") == NULL; + } + } + } + JS_ClearPendingException(cx); // `PyErr_SetString` uses `PyErr_SetObject` with `PyUnicode_FromString` under the hood // see https://github.com/python/cpython/blob/3.9/Python/errors.c#L234-L236 - PyObject *errStr = getExceptionString(cx, exceptionStack); - PyErr_SetObject(SpiderMonkeyError, errStr); + PyObject *errStr = getExceptionString(cx, exceptionStack, printStack); + PyObject *errObj = PyObject_CallFunction(SpiderMonkeyError, "O", errStr); // errObj = SpiderMonkeyError(errStr) Py_XDECREF(errStr); + // Preserve the original JS value as the `jsError` attribute for lossless back conversion + PyObject *originalJsErrCapsule = DictType::getPyObject(cx, exn); + PyObject_SetAttrString(errObj, "jsError", originalJsErrCapsule); + Py_XDECREF(originalJsErrCapsule); + // `PyErr_SetObject` can accept either an already created Exception instance or the containing exception value as the second argument + // see https://github.com/python/cpython/blob/v3.9.16/Python/errors.c#L134-L150 + PyErr_SetObject(SpiderMonkeyError, errObj); + Py_XDECREF(errObj); } diff --git a/tests/js/collide.simple b/tests/js/collide.simple new file mode 100644 index 00000000..7902a26b --- /dev/null +++ b/tests/js/collide.simple @@ -0,0 +1,20 @@ +/** + * @file collide.simple + * Test to ensure require module identitifier collision that resolves both py and js + * modules correctly prefers py modules. + * @author Wes Garland + * @date Mar 2024 + */ + +const { which } = require('./modules/collide'); +const whichjs = require('./modules/collide.js').which; +const whichpy = require('./modules/collide.py').which; + +if (which !== 'python') + throw new Error(`python module was not preferred, got ${which} instead`); + +if (whichpy !== 'python') + throw new Error(`python module was not explicitly loaded, got ${whichpy} instead`); + +if (whichjs !== 'javascript') + throw new Error(`javascript module was not explicitly loaded, got ${whichjs} instead`); diff --git a/tests/js/commonjs-modules.bash b/tests/js/commonjs-modules.bash index abd6c411..0bc36f86 100755 --- a/tests/js/commonjs-modules.bash +++ b/tests/js/commonjs-modules.bash @@ -5,6 +5,8 @@ # suite causes this test to fail. # @author Wes Garland, wes@distributive.network # @date June 2023 +# +# timeout: 40 panic() { diff --git a/tests/js/console-methods.simple b/tests/js/console-methods.simple new file mode 100644 index 00000000..6c9a4f14 --- /dev/null +++ b/tests/js/console-methods.simple @@ -0,0 +1,116 @@ +/** + * @file console-methods.simple + * + * Test methods in the console API other than simple logging functions. + * + * @author Tom Tang (xmader@distributive.network) + * @date February 2024 + */ +/* eslint-disable no-control-regex */ + +const { Console } = require('console'); + +let _temp = []; + +/** + * @param {'stdout' | 'stderr'} type + * @param {string | RegExp} expected + * @param {(console: Console) => void} callback + * @param {boolean} expectMatch + * @param {Console} _console reuse previous console object + */ +function expectOutput (type, expected, callback, expectMatch = true, _console = undefined) +{ + _temp = [type, expected, expectMatch]; // update the match string even if a previous console object is reused + if (!_console) + { + const opts = {}; + ['stdout', 'stderr'].forEach(selfType => ( + opts[selfType] = { + write(str) + { + [type, expected, expectMatch] = _temp; + if (type !== selfType) + throw new Error(`should not print to ${selfType}`); + + const match = str.match(expected); + if (expectMatch && !match) + { + console.log([str]); + throw new Error(`Expected "${expected}" but got "${str}"`); + } + else if (!expectMatch && match) + { + console.log([str]); + throw new Error(`The output should not be "${str}"`); + } + } + } + )); + _console = new Console(opts); + } + + callback(_console); +} + +expectOutput('stdout', '123\n', (console) => console.log('123')); +expectOutput('stdout', '1234\n', (console) => console.log('123'), false); + +expectOutput('stdout', /^\033c$/, (console) => console.clear()); + +// console.assert() +// +// Nothing should be printed if the condition evals to true +expectOutput('stdout', '', (console) => console.assert(true, 'abc')); +expectOutput('stderr', '', (console) => console.assert(1 > 0, 'abc')); +// Print 'Assertion failed' if false +expectOutput('stderr', /^Assertion failed$/m, (console) => console.assert(false)); +expectOutput('stderr', /^Assertion failed: abc$/m, (console) => console.assert(false, 'abc')); +expectOutput('stderr', /^Assertion failed: \[ \x1b\[33m1\x1b\[39m, \x1b\[33m2\x1b\[39m \]$/m, (console) => console.assert(false, [1, 2])); +expectOutput('stderr', /^Assertion failed: abc undefined$/m, (console) => console.assert(false, 'abc', undefined)); + +// console.trace() +expectOutput('stdout', /^Trace\n/, (console) => console.trace()); +expectOutput('stdout', /^Trace: \x1b\[90mundefined\x1b\[39m\n/, (console) => console.trace(undefined)); +expectOutput('stdout', /^Trace: \x1b\[33m123\x1b\[39m\n/, (console) => console.trace(123)); +expectOutput('stdout', /^((?!console\.js)[\s\S])*$/, (console) => console.trace()); // implementation details should not show up in the trace + +// console.count() +let keptConsole; +expectOutput('stdout', '', (console) => (keptConsole = console)); +expectOutput('stderr', 'Counter for \'default\' does not exist.', (console) => console.countReset(), true, keptConsole); +expectOutput('stderr', 'Counter for \'abc\' does not exist.', (console) => console.countReset('abc'), true, keptConsole); + +expectOutput('stdout', 'default: 1', (console) => console.count(), true, keptConsole); +expectOutput('stdout', 'default: 2', (console) => console.count(), true, keptConsole); +expectOutput('stdout', 'default: 3', (console) => console.count(), true, keptConsole); +expectOutput('stderr', '', (console) => console.countReset(), false, keptConsole); // counter resets to 1 +expectOutput('stdout', 'default: 1', (console) => console.count(), true, keptConsole); + +expectOutput('stdout', 'abc: 1', (console) => console.count('abc'), true, keptConsole); +expectOutput('stdout', 'NaN: 1', (console) => console.count(NaN), true, keptConsole); + +// console.group() +expectOutput('stdout', '', (console) => (keptConsole = console)); +expectOutput('stdout', /^abc$/m, (console) => console.group('abc'), true, keptConsole); +expectOutput('stdout', /^│ {3}d$/m, (console) => console.log('d'), true, keptConsole); // grouping level 1 +expectOutput('stdout', /^│ {3}e$/m, (console) => console.group('e'), true, keptConsole); +expectOutput('stdout', /^│ {3}│ {3}f$/m, (console) => console.log('f'), true, keptConsole); // level 2 +expectOutput('stdout', /^$/, (console) => console.groupEnd(), true, keptConsole); +expectOutput('stdout', /^│ {3}g$/m, (console) => console.log('g'), true, keptConsole); // back to level 1 + +// console.time() +expectOutput('stdout', '', (console) => (keptConsole = console)); +expectOutput('stderr', /^No such label 'default' for console\.timeLog/m, (console) => console.timeLog(), true, keptConsole); +expectOutput('stderr', /^No such label 'default' for console\.timeEnd/m, (console) => console.timeEnd(), true, keptConsole); +expectOutput('stdout', '', (console) => console.time(), true, keptConsole); +expectOutput('stderr', /^Label 'default' already exists for console\.time/m, (console) => console.time(), true, keptConsole); +expectOutput('stdout', /^default: \d+ms$/m, (console) => console.timeLog(), true, keptConsole); +expectOutput('stdout', /^default: \d+ms$/m, (console) => console.timeEnd(), true, keptConsole); +expectOutput('stderr', /^No such label 'default' for console\.timeLog/m, (console) => console.timeLog(), true, keptConsole); // timer cleared +expectOutput('stderr', /^No such label 'default' for console\.timeEnd/m, (console) => console.timeEnd(), true, keptConsole); + +expectOutput('stderr', /^No such label 'abc' for console\.timeLog/m, (console) => console.timeLog('abc'), true, keptConsole); +expectOutput('stdout', '', (console) => console.time('abc'), true, keptConsole); +expectOutput('stdout', /^abc: \d+ms$/m, (console) => console.timeLog('abc'), true, keptConsole); +expectOutput('stdout', /^abc: \d+ms$/m, (console) => console.timeEnd('abc'), true, keptConsole); diff --git a/tests/js/eval-test.simple b/tests/js/eval-test.simple new file mode 100644 index 00000000..e511a8a7 --- /dev/null +++ b/tests/js/eval-test.simple @@ -0,0 +1,18 @@ +/** + * @file eval-test.simple + * A test that makes sure we can correctly evaluate a JS file + * @copyright Copyright (c) 2024, Distributve Corp + * @author Wes Garland, wes@distributive.network + * @date March 2024 + */ +'use strict'; + +python.exec(` +import os + +globalThis = pm.eval('globalThis') +globalThis.result = pm.eval(open(os.path.join(os.getcwd(), "tests", "js", "resources", "eval-test.js"), "rb")) +`); + +if (globalThis.result !== 18436572) + throw new Error('incorrect result from eval-test.js'); diff --git a/tests/js/js2py/array-change-index.simple b/tests/js/js2py/array-change-index.simple index fbd5b0a9..20a04830 100644 --- a/tests/js/js2py/array-change-index.simple +++ b/tests/js/js2py/array-change-index.simple @@ -14,7 +14,7 @@ def setArrayAtIndex(array, index, new): array[1] = new # can't index "array" based on "index"... probably because index is a float. So just pass "1" return array `); -const setArrayAtIndex = python.eval("setArrayAtIndex") +const setArrayAtIndex = python.eval('setArrayAtIndex'); const numbersBack = setArrayAtIndex(numbers, 1, 999); // check that the array data was modified by reference in python diff --git a/tests/js/js2py/error.simple b/tests/js/js2py/error.simple index fde06acf..fd631eba 100644 --- a/tests/js/js2py/error.simple +++ b/tests/js/js2py/error.simple @@ -11,30 +11,35 @@ const throughJS = x => x; let errorFlag = false; -function tester(exceptionpy, exceptionjs) { +function tester(exceptionpy, exceptionjs) +{ if (!exceptionpy.toString() === exceptionjs.toString()) { console.error('Expected\n', exceptionpy.toString(), '\nbut got\n', exceptionjs.toString()); errorFlag = true; - } else { + } + else + { console.log('pass -', exceptionpy.toString()); } } -function inbuiltError() { - const exceptionpy = python.eval(`Exception('I know Python!')`); +function inbuiltError() +{ + const exceptionpy = python.eval('Exception(\'I know Python!\')'); const exceptionjs = throughJS(exceptionpy); tester(exceptionpy, exceptionjs); } -function customError() { +function customError() +{ python.exec( `class IAmAnError(Exception): def __init__(self, message): super().__init__(message) ` ); - const exceptionpy = python.eval(`IAmAnError('I know Python!')`); + const exceptionpy = python.eval('IAmAnError(\'I know Python!\')'); const exceptionjs = throughJS(exceptionpy); tester(exceptionpy, exceptionjs); } @@ -42,7 +47,8 @@ function customError() { inbuiltError(); customError(); -if (errorFlag) { +if (errorFlag) +{ throw new Error('test failed'); } diff --git a/tests/js/js2py/function-curry.simple b/tests/js/js2py/function-curry.simple index bf226cc4..1f37b10d 100644 --- a/tests/js/js2py/function-curry.simple +++ b/tests/js/js2py/function-curry.simple @@ -5,18 +5,24 @@ * @date July 2023 */ -'use strict' -const curry = (fn) => { - return function curried(...args) { - if (args.length >= fn.length) { - return fn.apply(this, args); - } else { - return function(...args2) { - return curried.apply(this, args.concat(args2)); - } +'use strict'; +const curry = (fn) => +{ + return function curried(...args) + { + if (args.length >= fn.length) + { + return fn.apply(null, args); + } + else + { + return function(...args2) + { + return curried.apply(null, args.concat(args2)); + }; } }; -} +}; const takeMiddle = (_x, y, _z) => y; diff --git a/tests/js/js2py/object-mutation.simple b/tests/js/js2py/object-mutation.simple index 2952efdb..623edbd4 100644 --- a/tests/js/js2py/object-mutation.simple +++ b/tests/js/js2py/object-mutation.simple @@ -17,10 +17,10 @@ def change_and_return(obj): python.exec(pcode); -const fun = python.eval("change_and_return"); +const fun = python.eval('change_and_return'); const obj2 = fun(obj); -if (obj.a !== 5 || obj2["a"] !== 5) +if (obj.a !== 5 || obj2['a'] !== 5) { console.error('Object isn\'t sharing memory.'); throw new Error('Test failed'); @@ -28,7 +28,7 @@ if (obj.a !== 5 || obj2["a"] !== 5) obj.a = 1000; -if (obj.a !== 1000 || obj2["a"] !== 1000) +if (obj.a !== 1000 || obj2['a'] !== 1000) { console.error('Object isn\'t sharing memory.'); throw new Error('Test failed'); diff --git a/tests/js/js2py/promise-await-in-python.simple b/tests/js/js2py/promise-await-in-python.simple index 07f021d8..8e9ea6e3 100644 --- a/tests/js/js2py/promise-await-in-python.simple +++ b/tests/js/js2py/promise-await-in-python.simple @@ -6,9 +6,12 @@ */ 'use strict'; -var resolve, reject; +var resolve; var valToResolve = 1; -const examplePromise = new Promise((res, rej) => {resolve = res, reject = rej}); +const examplePromise = new Promise((res, rej) => +{ + resolve = res; +}); const pythonCode = ` import asyncio @@ -19,15 +22,15 @@ async def awaitPromise(promise): `; python.exec(pythonCode); -const pythonFunction = python.eval("awaitPromise") +const pythonFunction = python.eval('awaitPromise'); async function test() { const backValue = await pythonFunction(examplePromise); if (backValue !== 1) - throw new Error(`Received value ${backValue} instead of ${valToResolve} from awaiting a JS promise in python`) + throw new Error(`Received value ${backValue} instead of ${valToResolve} from awaiting a JS promise in python`); } -test() +test(); resolve(valToResolve); diff --git a/tests/js/js2py/promise.simple b/tests/js/js2py/promise.simple index 503b38d9..af26cc85 100644 --- a/tests/js/js2py/promise.simple +++ b/tests/js/js2py/promise.simple @@ -8,16 +8,20 @@ 'use strict'; -var resolve, reject; -const examplePromise = new Promise((res, rej) => {resolve = res, reject = rej}); +var resolve; +const examplePromise = new Promise((res, rej) => +{ + resolve = res; +}); -const pythonCode = `lambda x: x`; +const pythonCode = 'lambda x: x'; const pythonLambda = python.eval(pythonCode); const outPromise = pythonLambda(examplePromise); -outPromise.then(() => { - console.log("able to resolve promise after going through python"); +outPromise.then(() => +{ + console.log('able to resolve promise after going through python'); python.exit(); }); diff --git a/tests/js/modules/collide.js b/tests/js/modules/collide.js new file mode 100644 index 00000000..832f009a --- /dev/null +++ b/tests/js/modules/collide.js @@ -0,0 +1 @@ +exports.which = 'javascript'; diff --git a/tests/js/modules/collide.py b/tests/js/modules/collide.py new file mode 100644 index 00000000..c6e221cd --- /dev/null +++ b/tests/js/modules/collide.py @@ -0,0 +1 @@ +exports['which'] = "python" diff --git a/tests/js/modules/print-load.js b/tests/js/modules/print-load.js index d0ea56df..2be4fe02 100644 --- a/tests/js/modules/print-load.js +++ b/tests/js/modules/print-load.js @@ -1,2 +1 @@ -//python.print('LOADED', __file) -console.log('LOADED', __filename) +console.log('LOADED', __filename); diff --git a/tests/js/modules/python-cjs-module.py b/tests/js/modules/python-cjs-module.py index 71c892de..72de3161 100644 --- a/tests/js/modules/python-cjs-module.py +++ b/tests/js/modules/python-cjs-module.py @@ -6,4 +6,5 @@ def helloWorld(): print('hello, world!') + exports['helloWorld'] = helloWorld diff --git a/tests/js/program-exit.bash b/tests/js/program-exit.bash new file mode 100755 index 00000000..70eb4987 --- /dev/null +++ b/tests/js/program-exit.bash @@ -0,0 +1,35 @@ +#! /bin/bash +# +# @file program-exit.bash +# A peter-jr test which shows that python.exit can cause a program to exit without +# reporting errors, even if there are pending timers. +# +# @author Wes Garland, wes@distributive.network +# @date July 2023 +# +# timeout: 5 + +set -u + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +"${PMJS:-../../pmjs}" ./program-exit.js 2>&1 \ +| while read line + do + panic "Unexpected output '$line'" + done +exitCode="$?" +[ "$exitCode" = 0 ] || exit "$exitCode" + +"${PMJS:-../../pmjs}" ./program-exit.js +exitCode="$?" + +[ "$exitCode" = "99" ] || panic "exit code should have been 99 but was $exitCode" + +exit 0 diff --git a/tests/js/program-exit.js b/tests/js/program-exit.js new file mode 100644 index 00000000..b88b0ed2 --- /dev/null +++ b/tests/js/program-exit.js @@ -0,0 +1,8 @@ +/** + * @file program-exit.js + * Support code program-exit.bash + * @author Wes Garland, wes@distributive.network + * @date July 2023 + */ +setTimeout(()=>console.log('hey'), 10400) +python.exit(99) diff --git a/tests/js/program-module.simple b/tests/js/program-module.simple index 71726067..48c0e910 100644 --- a/tests/js/program-module.simple +++ b/tests/js/program-module.simple @@ -19,14 +19,14 @@ function check(message, testResult) check('require.main is an object', typeof require.main === 'object'); check('require.main is the current module', require.main === module); -check('require is the global require', require === globalThis.require) +check('require is the global require', require === globalThis.require); check('exports is the global exports', exports === globalThis.exports); check('module is the global module', module === globalThis.module); // eslint-disable-next-line no-global-assign module = {}; check('free module symbol is global symbol', module === globalThis.module); -check('arguments.length is ' + arguments.length, ...arguments) +check('arguments.length is ' + arguments.length, ...arguments); if (failures) console.log(`${failures} sub-tests failing`); diff --git a/tests/js/program-throw.bash b/tests/js/program-throw.bash new file mode 100755 index 00000000..2f4d848c --- /dev/null +++ b/tests/js/program-throw.bash @@ -0,0 +1,28 @@ +#! /bin/bash +# +# @file program-throw.bash +# A peter-jr test which shows that uncaught exceptions in the program throw, get shown +# on stderr, cause a non-zero exit code, and aren't delayed because of pending events. +# +# @author Wes Garland, wes@distributive.network +# @date July 2023 +# +# timeout: 5 + +set -u + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +"${PMJS:-../../pmjs}" ./program-throw.js 2>&1 1>/dev/null \ +| egrep 'hello|goodbye' \ +| while read line + do + [[ "$line" =~ goodbye ]] && panic "found goodbye - timer fired when it shouldn't have!" + [[ "$line" =~ hello ]] && echo "found expected '$line'" && exit 0 + done diff --git a/tests/js/program-throw.js b/tests/js/program-throw.js new file mode 100644 index 00000000..dd842f3e --- /dev/null +++ b/tests/js/program-throw.js @@ -0,0 +1,9 @@ +/** + * @file program-throw.js + * Support code program-throw.bash + * @author Wes Garland, wes@distributive.network + * @date July 2023 + */ + +setTimeout(() => console.error('goodbye'), 6000) +throw new Error('hello') diff --git a/tests/js/py2js/datetime.simple b/tests/js/py2js/datetime.simple index f9ec5cdc..61a82484 100644 --- a/tests/js/py2js/datetime.simple +++ b/tests/js/py2js/datetime.simple @@ -6,7 +6,7 @@ * @author Elijah Deluzio, elijah@distributive.network * @date July 2023 */ -'use strict' +'use strict'; const throughJS = x => x; var expectedJsTimestamp; @@ -16,8 +16,8 @@ var pyDate; // Test 1: Date from timestamp of 0 (1970 - 01 - 01), timestamp = 0 expectedJsTimestamp = 0; -python.exec(`from datetime import timezone`) -python.exec(`import datetime`); +python.exec('from datetime import timezone'); +python.exec('import datetime'); pyDate = python.eval(`datetime.datetime.fromtimestamp(${expectedJsTimestamp}, tz=timezone.utc)`); jsDate = throughJS(pyDate); @@ -31,8 +31,8 @@ console.log('pass -', jsDate); // Test 2: Date from 21st century (2222 - 02 - 03), timestamp = 7955193600 expectedJsTimestamp = 7955193600; -python.exec(`from datetime import timezone`) -python.exec(`import datetime`); +python.exec('from datetime import timezone'); +python.exec('import datetime'); pyDate = python.eval(`datetime.datetime.fromtimestamp(${expectedJsTimestamp}, tz=timezone.utc)`); jsDate = throughJS(pyDate); diff --git a/tests/js/py2js/error.simple b/tests/js/py2js/error.simple index 90dadfc6..92212709 100644 --- a/tests/js/py2js/error.simple +++ b/tests/js/py2js/error.simple @@ -8,17 +8,20 @@ */ 'use strict'; -class RandomError extends Error { - constructor(message) { +class RandomError extends Error +{ + constructor(message) + { super(message); } } -const exceptionjs = new RandomError("I was created!"); +const exceptionjs = new RandomError('I was created!'); const throughPython = python.eval('(lambda x: x)'); const exceptionpy = throughPython(exceptionjs); -if (!exceptionpy.toString().includes(exceptionjs.toString())) { +if (!exceptionpy.toString().includes(exceptionjs.toString())) +{ console.error('Expected\n', exceptionjs.toString(), '\nbut got\n', exceptionpy.toString()); throw new Error('test failed'); } diff --git a/tests/js/py2js/integer.simple b/tests/js/py2js/integer.simple index ef3043fd..74d2a233 100644 --- a/tests/js/py2js/integer.simple +++ b/tests/js/py2js/integer.simple @@ -15,7 +15,7 @@ if (typeof number !== 'number') } if (number !== 777) { - console.error(`expected ${777} but got ${bigint}`); + console.error(`expected ${777} but got ${number}`); throw new Error('test failed'); } diff --git a/tests/js/quint.js b/tests/js/quint.js new file mode 100644 index 00000000..0d3e1c99 --- /dev/null +++ b/tests/js/quint.js @@ -0,0 +1,72 @@ +/** + * @file quint.js + * A minimum testing framework with QUnit-like (https://qunitjs.com/) APIs + * @author Tom Tang + * @date Aug 2023 + */ + +const QUnitAssert = { + arity(fn, length) + { + if (fn.length !== length) throw new Error(`'${fn}' does not have arity of ${length}`); + }, + isFunction(x) + { + if (typeof x !== 'function') throw new Error(`'${x}' is not a function`); + }, + name(x, name) + { + if (x.name !== name) throw new Error(`'${x}' does not have a name of ${name}`); + }, + true(x) + { + if (x !== true) throw new Error(`'${x}' is not true`); + }, + false(x) + { + if (x !== false) throw new Error(`'${x}' is not false`); + }, + same(a, b) + { + if (a !== b) throw new Error(`'${a}' does not equal to '${b}'`); + }, + arrayEqual(a, b) + { + if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`'${a}' does not equal to '${b}'`); + }, + throws(fn, error) + { + try + { + fn(); + } + catch (err) + { + if (!err.toString().includes(error)) throw new Error(`'${fn}' throws '${err}' but expects '${error}'`); + return; + } + throw new Error(`'${fn}' does not throw`); + }, + looksNative(fn) + { + if (!fn.toString().includes('[native code]')) throw new Error(`'${fn}' does not look native`); + }, + enumerable(obj, propertyName) + { + const descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); + if (!descriptor.enumerable) throw new Error(`'${obj[Symbol.toStringTag]}.${propertyName}' is not enumerable`); + }, +}; + +const QUnit = { + test(name, callback) + { + callback(QUnitAssert); + }, + skip(name, callback) + { + // no op + } +}; + +module.exports = QUnit; diff --git a/tests/js/resources/eval-test.js b/tests/js/resources/eval-test.js new file mode 100644 index 00000000..56bf058e --- /dev/null +++ b/tests/js/resources/eval-test.js @@ -0,0 +1,2 @@ +console.log('eval-test.js loaded from', __filename); +18436572; diff --git a/tests/js/set-interval.bash b/tests/js/set-interval.bash new file mode 100644 index 00000000..8d3a7c2e --- /dev/null +++ b/tests/js/set-interval.bash @@ -0,0 +1,43 @@ +#! /bin/bash +# +# @file set-interval.bash +# A peter-jr test which ensures that the `setInterval` global function works properly. +# +# @author Tom Tang (xmader@distributive.network) +# @date April 2024 + +set -u +set -o pipefail + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +code=' +let n = 0; +const timer = setInterval(()=> +{ + console.log("callback called"); + + n++; + if (n >= 5) clearInterval(timer); // clearInterval should work inside the callback + + throw new Error("testing from the callback"); // timer should continue running regardless of whether the job function succeeds or not +}, 50); +' + +"${PMJS:-pmjs}" \ +-e "$code" \ +< /dev/null 2> /dev/null \ +| tr -d '\r' \ +| grep -c '^callback called$' \ +| while read qty + do + echo "callback called: $qty" + [ "$qty" != "5" ] && panic qty should not be $qty + break + done || exit $? diff --git a/tests/js/set-timeout.simple b/tests/js/set-timeout.simple new file mode 100644 index 00000000..322b5ff6 --- /dev/null +++ b/tests/js/set-timeout.simple @@ -0,0 +1,32 @@ +/** + * @file set-timeout.simple + * Simple test which ensures that setTimeout() fires, and that it fires at about the right + * time. + * @author Wes Garland, wes@distributive.network + * @date March 2024 + */ + +const time = python.eval('__import__("time").time'); +const start = time(); + +/** + * - must fire later than 100ms + * - must fire before 10s + * - 10s is well before 100s but very CI-load-tolerant; the idea is not to check for accurancy, but + * rather ensure we haven't mixed up seconds and milliseconds somewhere. + */ +function check100ms() +{ + let end = time(); + + if (end - start < 0.1) + throw new Error('timer fired too soon'); + if (end - start > 10) + throw new Error('timer fired too late'); + + console.log('done - timer fired after ', (end-start) * 1000, 'ms'); + python.exit.code = 0; +} + +python.exit.code = 2; +setTimeout(check100ms, 100); diff --git a/tests/js/test-atob-btoa.simple b/tests/js/test-atob-btoa.simple index 29d82768..75db5622 100644 --- a/tests/js/test-atob-btoa.simple +++ b/tests/js/test-atob-btoa.simple @@ -5,15 +5,17 @@ * @date July 2023 */ -function expect(a) { +function expect(a) +{ return { - toBe(b) { + toBe(b) + { if (a !== b) throw new Error(`'${a}' does not equal to '${b}'`); } - } + }; } -/*! +/* ! * Modified from https://github.com/oven-sh/bun/blob/bun-v0.6.12/test/js/web/util/atob.test.js * Bun * MIT License @@ -22,37 +24,37 @@ function expect(a) { // // atob // -expect(atob("YQ==")).toBe("a"); -expect(atob("YWI=")).toBe("ab"); -expect(atob("YWJj")).toBe("abc"); -expect(atob("YWJjZA==")).toBe("abcd"); -expect(atob("YWJjZGU=")).toBe("abcde"); -expect(atob("YWJjZGVm")).toBe("abcdef"); -expect(atob("zzzz")).toBe("Ï<ó"); -expect(atob("")).toBe(""); -expect(atob("null")).toBe("žée"); -expect(atob("6ek=")).toBe("éé"); +expect(atob('YQ==')).toBe('a'); +expect(atob('YWI=')).toBe('ab'); +expect(atob('YWJj')).toBe('abc'); +expect(atob('YWJjZA==')).toBe('abcd'); +expect(atob('YWJjZGU=')).toBe('abcde'); +expect(atob('YWJjZGVm')).toBe('abcdef'); +expect(atob('zzzz')).toBe('Ï<ó'); +expect(atob('')).toBe(''); +expect(atob('null')).toBe('žée'); +expect(atob('6ek=')).toBe('éé'); // expect(atob("6ek")).toBe("éé"); // FIXME (Tom Tang): binascii.Error: Incorrect padding -expect(atob("gIE=")).toBe("€"); +expect(atob('gIE=')).toBe('€'); // expect(atob("zz")).toBe("Ï"); // FIXME (Tom Tang): same here // expect(atob("zzz")).toBe("Ï<"); -expect(atob("zzz=")).toBe("Ï<"); -expect(atob(" YQ==")).toBe("a"); -expect(atob("YQ==\u000a")).toBe("a"); +expect(atob('zzz=')).toBe('Ï<'); +expect(atob(' YQ==')).toBe('a'); +expect(atob('YQ==\u000a')).toBe('a'); // // btoa // -expect(btoa("a")).toBe("YQ=="); -expect(btoa("ab")).toBe("YWI="); -expect(btoa("abc")).toBe("YWJj"); -expect(btoa("abcd")).toBe("YWJjZA=="); -expect(btoa("abcde")).toBe("YWJjZGU="); -expect(btoa("abcdef")).toBe("YWJjZGVm"); -expect(typeof btoa).toBe("function"); -expect(btoa("")).toBe(""); -expect(btoa("null")).toBe("bnVsbA=="); -expect(btoa("undefined")).toBe("dW5kZWZpbmVk"); -expect(btoa("[object Window]")).toBe("W29iamVjdCBXaW5kb3dd"); -expect(btoa("éé")).toBe("6ek="); -expect(btoa("\u0080\u0081")).toBe("gIE="); +expect(btoa('a')).toBe('YQ=='); +expect(btoa('ab')).toBe('YWI='); +expect(btoa('abc')).toBe('YWJj'); +expect(btoa('abcd')).toBe('YWJjZA=='); +expect(btoa('abcde')).toBe('YWJjZGU='); +expect(btoa('abcdef')).toBe('YWJjZGVm'); +expect(typeof btoa).toBe('function'); +expect(btoa('')).toBe(''); +expect(btoa('null')).toBe('bnVsbA=='); +expect(btoa('undefined')).toBe('dW5kZWZpbmVk'); +expect(btoa('[object Window]')).toBe('W29iamVjdCBXaW5kb3dd'); +expect(btoa('éé')).toBe('6ek='); +expect(btoa('\u0080\u0081')).toBe('gIE='); diff --git a/tests/js/throw-filename.js b/tests/js/throw-filename.js index ecaa4d53..d29cf743 100644 --- a/tests/js/throw-filename.js +++ b/tests/js/throw-filename.js @@ -6,9 +6,9 @@ */ try { - throw new Error("lp0 on fire"); + throw new Error('lp0 on fire'); } catch(error) { - console.log(error.stack) + console.log(error.stack); } diff --git a/tests/js/timer-reject.bash b/tests/js/timer-reject.bash new file mode 100755 index 00000000..6cc3c89e --- /dev/null +++ b/tests/js/timer-reject.bash @@ -0,0 +1,47 @@ +#! /bin/bash +# +# @file timer-reject.bash +# A peter-jr test which shows that unhandled rejections in timers get shown on stderr, +# exit with status 1, and aren't delayed because of pending events. +# +# @author Wes Garland, wes@distributive.network +# @date July 2023 +# +# timeout: 10 + +set -u +set -o pipefail + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +"${PMJS:-../../pmjs}" ./timer-reject.js 2>&1 1>/dev/null \ +| egrep 'hello|goodbye|fire' \ +| ( + read line + if [[ "$line" =~ hello ]]; then + echo "found expected '$line'" + else + panic "expected hello, found '${line}'" + fi + + read line + if [[ "$line" =~ Error:.goodbye ]]; then + echo "found expected '$line'" + else + panic "expected Error: goodbye, found '${line}'" + fi + ) +exitCode="$?" + +if [ "${exitCode}" = "1" ]; then + echo pass + exit 0 +fi + +[ "$exitCode" = 2 ] || panic "Exit code was $exitCode" diff --git a/tests/js/timer-reject.js b/tests/js/timer-reject.js new file mode 100644 index 00000000..b756cd34 --- /dev/null +++ b/tests/js/timer-reject.js @@ -0,0 +1,10 @@ +/** + * @file timer-reject.js + * Support code timer-reject.bash + * @author Wes Garland, wes@distributive.network + * @date July 2023 + */ + +setTimeout(async () => { throw new Error('goodbye') }, 600); +setTimeout(async () => { console.warn('this should not fire') }, 2000); +console.error('hello'); diff --git a/tests/js/timer-throw.bash b/tests/js/timer-throw.bash new file mode 100755 index 00000000..a3d87804 --- /dev/null +++ b/tests/js/timer-throw.bash @@ -0,0 +1,38 @@ +#! /bin/bash +# +# @file program-throw.bash +# A peter-jr test which shows that uncaught exceptions in the program throw, get shown +# on stderr, cause a non-zero exit code, and aren't delayed because of pending events. +# +# @author Wes Garland, wes@distributive.network +# @date July 2023 +# +# timeout: 5 + +set -u + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +"${PMJS:-../../pmjs}" ./timer-throw.js 2>&1 1>/dev/null \ +| egrep 'hello|goodbye' \ +| ( + read line + if [[ "$line" =~ hello ]]; then + echo "found expected '$line'" + else + panic "expected hello, found '${line}'" + fi + + read line + if [[ "$line" =~ Error:.goodbye ]]; then + echo "found expected '$line'" + else + panic "expected Error: goodbye, found '${line}'" + fi + ) diff --git a/tests/js/timer-throw.js b/tests/js/timer-throw.js new file mode 100644 index 00000000..c9901890 --- /dev/null +++ b/tests/js/timer-throw.js @@ -0,0 +1,9 @@ +/** + * @file timer-throw.js + * Support code timer-throw.bash + * @author Wes Garland, wes@distributive.network + * @date July 2023 + */ + +setTimeout(() => { throw new Error('goodbye') }, 600); +console.error('hello'); diff --git a/tests/js/timers-force-exit.simple b/tests/js/timers-force-exit.simple new file mode 100644 index 00000000..a18f63b5 --- /dev/null +++ b/tests/js/timers-force-exit.simple @@ -0,0 +1,13 @@ +/** + * @file timers-force-exit.simple + * ensure we can use python.exit() even though there are timers pending + * @author Wes Garland, wes@distributive.network + * @date July 2023 + * + * timeout: 4 + */ + +setTimeout(()=>console.log('fired timer'), 500000); +setTimeout(()=>console.error('should not have fired timer!'), 0); +console.log('about to exit even though timers are pending'); +python.exit(0); diff --git a/tests/js/timers-natural-exit.bash b/tests/js/timers-natural-exit.bash new file mode 100755 index 00000000..00c5532b --- /dev/null +++ b/tests/js/timers-natural-exit.bash @@ -0,0 +1,27 @@ +#! /bin/bash +# +# @file timers-natural-exit.bash +# A peter-jr test which show that programs exit when the event loop becomes empty. +# +# @author Wes Garland, wes@distributive.network +# @date July 2023 + +set -u +set -o pipefail + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +"${PMJS:-../../pmjs}" ./timers-natural-exit.js \ +| egrep 'end of program|fired timer' \ +| ( + read line + [ "$line" = "end of program" ] || panic "first line read was '$line', not 'end of program'" + read line + [ "$line" = "fired timer" ] || panic "second line read was '$line', not 'fired timer'" + ) diff --git a/tests/js/timers-natural-exit.js b/tests/js/timers-natural-exit.js new file mode 100644 index 00000000..178e3cae --- /dev/null +++ b/tests/js/timers-natural-exit.js @@ -0,0 +1,9 @@ +/** + * @file timers-natural-exit.js + * Support code timers-natural-exit.bash + * @author Wes Garland, wes@distributive.network + * @date July 2023 + */ + +setTimeout(()=>console.log('fired timer'), 500); +console.log('end of program') diff --git a/tests/js/timers-segfault.simple b/tests/js/timers-segfault.simple new file mode 100644 index 00000000..afe42df3 --- /dev/null +++ b/tests/js/timers-segfault.simple @@ -0,0 +1,13 @@ +/** + * @file timers-segfault.simple + * Test using the builtin_modules/timers.js module. + * @author Wes Garland, wes@distributive.network + * @date July 2023 + * + * timeout: 10 + */ +/* eslint-disable brace-style */ + +setTimeout(() => { console.log(0); }); +const interval = setInterval(() => { console.log(1); }, 500); +setTimeout(() => { clearInterval(interval); console.log(2); }, 1000); diff --git a/tests/js/typeofs.simple b/tests/js/typeofs.simple index e9e56859..80a60467 100755 --- a/tests/js/typeofs.simple +++ b/tests/js/typeofs.simple @@ -17,7 +17,7 @@ function check(jsval, expected) switch (typeof expected) { case 'function': - disp = expected.name || '(anonymous function)' + disp = expected.name || '(anonymous function)'; break; case 'object': disp = JSON.stringify(expected); @@ -42,16 +42,16 @@ function check(jsval, expected) } } -check(throughBoth(123) , 'number') -check(throughBoth(123n), 'bigint') -check(throughBoth('sn'), 'string') -check(throughBoth({}), 'object') -check(throughBoth(true), 'boolean') -check(throughBoth(false), 'boolean') -check(throughBoth(undefined), 'undefined') -check(throughBoth(null), 'object') -check(throughBoth([1,1]), 'object') -check(throughBoth(()=>1), 'function') -check(throughBoth([2,2]), Array) -check(throughBoth(new Array(1)), Array) +check(throughBoth(123) , 'number'); +check(throughBoth(123n), 'bigint'); +check(throughBoth('sn'), 'string'); +check(throughBoth({}), 'object'); +check(throughBoth(true), 'boolean'); +check(throughBoth(false), 'boolean'); +check(throughBoth(undefined), 'undefined'); +check(throughBoth(null), 'object'); +check(throughBoth([1,1]), 'object'); +check(throughBoth(()=>1), 'function'); +check(throughBoth([2,2]), Array); +check(throughBoth(new Array(1)), Array); /** see also typeofs-segfaults.simple.failing */ diff --git a/tests/js/uncaught-rejection-handler.bash b/tests/js/uncaught-rejection-handler.bash new file mode 100644 index 00000000..837d286c --- /dev/null +++ b/tests/js/uncaught-rejection-handler.bash @@ -0,0 +1,53 @@ +#! /bin/bash +# +# @file uncaught-rejection-handler.bash +# For testing if the actual JS error gets printed out for uncaught Promise rejections, +# instead of printing out a Python `Future exception was never retrieved` error message when not in pmjs +# +# @author Tom Tang (xmader@distributive.network) +# @date June 2024 + +set -u +set -o pipefail + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +code=' +import asyncio +import pythonmonkey as pm + +async def pythonmonkey_main(): + pm.eval("""void Promise.reject(new TypeError("abc"));""") + await pm.wait() + +asyncio.run(pythonmonkey_main()) +' + +OUTPUT=$(python -c "$code" \ + < /dev/null 2>&1 +) + +echo "$OUTPUT" \ +| tr -d '\r' \ +| (grep -c 'Future exception was never retrieved' || true) \ +| while read qty + do + echo "$OUTPUT" + [ "$qty" != "0" ] && panic "There shouldn't be a 'Future exception was never retrieved' error massage" + break + done || exit $? + +echo "$OUTPUT" \ +| tr -d '\r' \ +| grep -c 'Uncaught TypeError: abc' \ +| while read qty + do + [ "$qty" != "1" ] && panic "It should print out 'Uncaught TypeError' directly" + break + done || exit $? diff --git a/tests/js/url-search-params.simple b/tests/js/url-search-params.simple new file mode 100644 index 00000000..c51ded5f --- /dev/null +++ b/tests/js/url-search-params.simple @@ -0,0 +1,979 @@ +/** + * @file url-search-params.simple + * Simple test for URLSearchParams + * @author Tom Tang + * @date Aug 2023 + */ + +const QUnit = require('./quint'); + +/* ! + * Modified from https://github.com/zloirock/core-js/blob/d99baeff/tests/unit-global/web.url-search-params.js + * core-js + * MIT License + */ + +const DESCRIPTORS = true; +const NODE = false; + +const { getPrototypeOf, getOwnPropertyDescriptor } = Object; + +QUnit.test('URLSearchParams', assert => +{ + assert.isFunction(URLSearchParams); + assert.arity(URLSearchParams, 0); + assert.name(URLSearchParams, 'URLSearchParams'); + if (!NODE) assert.looksNative(URLSearchParams); + + assert.same(String(new URLSearchParams()), ''); + assert.same(String(new URLSearchParams('')), ''); + assert.same(String(new URLSearchParams('a=b')), 'a=b'); + assert.same(String(new URLSearchParams(new URLSearchParams('a=b'))), 'a=b'); + assert.same(String(new URLSearchParams([])), ''); + assert.same(String(new URLSearchParams([[1, 2], ['a', 'b']])), '1=2&a=b'); + // assert.same(String(new URLSearchParams(createIterable([createIterable(['a', 'b']), createIterable(['c', 'd'])]))), 'a=b&c=d'); + assert.same(String(new URLSearchParams({})), ''); + assert.same(String(new URLSearchParams({ 1: 2, a: 'b' })), '1=2&a=b'); + + assert.same(String(new URLSearchParams('?a=b')), 'a=b', 'leading ? should be ignored'); + assert.same(String(new URLSearchParams('??a=b')), '%3Fa=b'); + assert.same(String(new URLSearchParams('?')), ''); + assert.same(String(new URLSearchParams('??')), '%3F='); + + assert.same(String(new URLSearchParams('a=b c')), 'a=b+c'); + assert.same(String(new URLSearchParams('a=b&b=c&a=d')), 'a=b&b=c&a=d'); + + assert.same(String(new URLSearchParams('a==')), 'a=%3D'); + assert.same(String(new URLSearchParams('a=b=')), 'a=b%3D'); + assert.same(String(new URLSearchParams('a=b=c')), 'a=b%3Dc'); + assert.same(String(new URLSearchParams('a==b')), 'a=%3Db'); + + let params = new URLSearchParams('a=b'); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.false(params.has('b'), 'search params object has not got name "b"'); + + params = new URLSearchParams('a=b&c'); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.true(params.has('c'), 'search params object has name "c"'); + + params = new URLSearchParams('&a&&& &&&&&a+b=& c&m%c3%b8%c3%b8'); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.true(params.has('a b'), 'search params object has name "a b"'); + assert.true(params.has(' '), 'search params object has name " "'); + assert.false(params.has('c'), 'search params object did not have the name "c"'); + assert.true(params.has(' c'), 'search params object has name " c"'); + assert.true(params.has('møø'), 'search params object has name "møø"'); + + params = new URLSearchParams('a=b+c'); + assert.same(params.get('a'), 'b c', 'parse +'); + params = new URLSearchParams('a+b=c'); + assert.same(params.get('a b'), 'c', 'parse +'); + + params = new URLSearchParams('a=b c'); + assert.same(params.get('a'), 'b c', 'parse " "'); + params = new URLSearchParams('a b=c'); + assert.same(params.get('a b'), 'c', 'parse " "'); + + params = new URLSearchParams('a=b%20c'); + assert.same(params.get('a'), 'b c', 'parse %20'); + params = new URLSearchParams('a%20b=c'); + assert.same(params.get('a b'), 'c', 'parse %20'); + + params = new URLSearchParams('a=b\0c'); + assert.same(params.get('a'), 'b\0c', 'parse \\0'); + params = new URLSearchParams('a\0b=c'); + assert.same(params.get('a\0b'), 'c', 'parse \\0'); + + params = new URLSearchParams('a=b%00c'); + assert.same(params.get('a'), 'b\0c', 'parse %00'); + params = new URLSearchParams('a%00b=c'); + assert.same(params.get('a\0b'), 'c', 'parse %00'); + + params = new URLSearchParams('a=b\u2384'); + assert.same(params.get('a'), 'b\u2384', 'parse \u2384'); + params = new URLSearchParams('a\u2384b=c'); + assert.same(params.get('a\u2384b'), 'c', 'parse \u2384'); + + params = new URLSearchParams('a=b%e2%8e%84'); + assert.same(params.get('a'), 'b\u2384', 'parse %e2%8e%84'); + params = new URLSearchParams('a%e2%8e%84b=c'); + assert.same(params.get('a\u2384b'), 'c', 'parse %e2%8e%84'); + + params = new URLSearchParams('a=b\uD83D\uDCA9c'); + assert.same(params.get('a'), 'b\uD83D\uDCA9c', 'parse \uD83D\uDCA9'); + params = new URLSearchParams('a\uD83D\uDCA9b=c'); + assert.same(params.get('a\uD83D\uDCA9b'), 'c', 'parse \uD83D\uDCA9'); + + params = new URLSearchParams('a=b%f0%9f%92%a9c'); + assert.same(params.get('a'), 'b\uD83D\uDCA9c', 'parse %f0%9f%92%a9'); + params = new URLSearchParams('a%f0%9f%92%a9b=c'); + assert.same(params.get('a\uD83D\uDCA9b'), 'c', 'parse %f0%9f%92%a9'); + + params = new URLSearchParams(); + params.set('query', '+15555555555'); + assert.same(params.toString(), 'query=%2B15555555555'); + assert.same(params.get('query'), '+15555555555', 'parse encoded +'); + params = new URLSearchParams(params.toString()); + assert.same(params.get('query'), '+15555555555', 'parse encoded +'); + + const testData = [ + { input: '?a=%', output: [['a', '%']], name: 'handling %' }, + { input: { '+': '%C2' }, output: [['+', '%C2']], name: 'object with +' }, + { input: { c: 'x', a: '?' }, output: [['c', 'x'], ['a', '?']], name: 'object with two keys' }, + { input: [['c', 'x'], ['a', '?']], output: [['c', 'x'], ['a', '?']], name: 'array with two keys' }, + // eslint-disable-next-line max-len -- ignore + // !!! { input: { 'a\0b': '42', 'c\uD83D': '23', dሴ: 'foo' }, output: [['a\0b', '42'], ['c\uFFFD', '23'], ['d\u1234', 'foo']], name: 'object with NULL, non-ASCII, and surrogate keys' }, + ]; + + for (const { input, output, name } of testData) + { + params = new URLSearchParams(input); + let i = 0; + params.forEach((value, key) => + { + const [reqKey, reqValue] = output[i++]; + assert.same(key, reqKey, `construct with ${ name }`); + assert.same(value, reqValue, `construct with ${ name }`); + }); + } + + assert.throws(() => + { + // eslint-disable-next-line new-cap + URLSearchParams(''); + }, 'TypeError: Incorrect invocation'); // throws w/o `new` + + assert.throws(() => + { + new URLSearchParams([[1, 2, 3]]); + }, 'TypeError: Expected sequence with length 2'); + + // assert.throws(() => { + // new URLSearchParams([createIterable([createIterable([1, 2, 3])])]); + // }, 'sequence elements must be pairs #2'); + + assert.throws(() => + { + new URLSearchParams([[1]]); + }, 'TypeError: Expected sequence with length 2'); + + // assert.throws(() => { + // new URLSearchParams([createIterable([createIterable([1])])]); + // }, 'sequence elements must be pairs #4'); +}); + +QUnit.test('URLSearchParams#append', assert => +{ + const { append } = URLSearchParams.prototype; + assert.isFunction(append); + assert.arity(append, 2); + assert.name(append, 'append'); + assert.enumerable(URLSearchParams.prototype, 'append'); + if (!NODE) assert.looksNative(append); + + assert.same(new URLSearchParams().append('a', 'b'), undefined, 'void'); + + let params = new URLSearchParams(); + params.append('a', 'b'); + assert.same(String(params), 'a=b'); + params.append('a', 'b'); + assert.same(String(params), 'a=b&a=b'); + params.append('a', 'c'); + assert.same(String(params), 'a=b&a=b&a=c'); + + params = new URLSearchParams(); + params.append('', ''); + assert.same(String(params), '='); + params.append('', ''); + assert.same(String(params), '=&='); + + params = new URLSearchParams(); + params.append(undefined, undefined); + assert.same(String(params), 'undefined=undefined'); + params.append(undefined, undefined); + assert.same(String(params), 'undefined=undefined&undefined=undefined'); + + params = new URLSearchParams(); + params.append(null, null); + assert.same(String(params), 'null=null'); + params.append(null, null); + assert.same(String(params), 'null=null&null=null'); + + params = new URLSearchParams(); + params.append('first', 1); + params.append('second', 2); + params.append('third', ''); + params.append('first', 10); + assert.true(params.has('first'), 'search params object has name "first"'); + assert.same(params.get('first'), '1', 'search params object has name "first" with value "1"'); + assert.same(params.get('second'), '2', 'search params object has name "second" with value "2"'); + assert.same(params.get('third'), '', 'search params object has name "third" with value ""'); + params.append('first', 10); + assert.same(params.get('first'), '1', 'search params object has name "first" with value "1"'); + + assert.throws(() => + { + return new URLSearchParams('').append(); + }, 'TypeError: Not enough arguments'); // throws w/o arguments +}); + +QUnit.test('URLSearchParams#delete', assert => +{ + const $delete = URLSearchParams.prototype.delete; + assert.isFunction($delete); + assert.arity($delete, 1); + assert.enumerable(URLSearchParams.prototype, 'delete'); + if (!NODE) assert.looksNative($delete); + + let params = new URLSearchParams('a=b&c=d'); + params.delete('a'); + assert.same(String(params), 'c=d'); + + params = new URLSearchParams('a=a&b=b&a=a&c=c'); + params.delete('a'); + assert.same(String(params), 'b=b&c=c'); + + params = new URLSearchParams('a=a&=&b=b&c=c'); + params.delete(''); + assert.same(String(params), 'a=a&b=b&c=c'); + + params = new URLSearchParams('a=a&null=null&b=b'); + params.delete(null); + assert.same(String(params), 'a=a&b=b'); + + params = new URLSearchParams('a=a&undefined=undefined&b=b'); + params.delete(undefined); + assert.same(String(params), 'a=a&b=b'); + + params = new URLSearchParams(); + params.append('first', 1); + assert.true(params.has('first'), 'search params object has name "first"'); + assert.same(params.get('first'), '1', 'search params object has name "first" with value "1"'); + params.delete('first'); + assert.false(params.has('first'), 'search params object has no "first" name'); + params.append('first', 1); + params.append('first', 10); + params.delete('first'); + assert.false(params.has('first'), 'search params object has no "first" name'); + + params = new URLSearchParams('a=1&a=2&a=null&a=3&b=4'); + params.delete('a', 2); + assert.same(String(params), 'a=1&a=null&a=3&b=4'); + + params = new URLSearchParams('a=1&a=2&a=null&a=3&b=4'); + params.delete('a', null); + assert.same(String(params), 'a=1&a=2&a=3&b=4'); + + params = new URLSearchParams('a=1&a=2&a=null&a=3&b=4'); + params.delete('a', undefined); + assert.same(String(params), 'b=4'); + + if (DESCRIPTORS) + { + let url = new URL('http://example.com/?param1¶m2'); + url.searchParams.delete('param1'); + url.searchParams.delete('param2'); + assert.same(String(url), 'http://example.com/', 'url.href does not have ?'); + assert.same(url.search, '', 'url.search does not have ?'); + + url = new URL('http://example.com/?'); + url.searchParams.delete('param1'); + // assert.same(String(url), 'http://example.com/', 'url.href does not have ?'); // Safari bug + assert.same(url.search, '', 'url.search does not have ?'); + } + + assert.throws(() => + { + return new URLSearchParams('').delete(); + }, 'TypeError: Not enough arguments'); +}); + +QUnit.test('URLSearchParams#get', assert => +{ + const { get } = URLSearchParams.prototype; + assert.isFunction(get); + assert.arity(get, 1); + assert.name(get, 'get'); + assert.enumerable(URLSearchParams.prototype, 'get'); + if (!NODE) assert.looksNative(get); + + let params = new URLSearchParams('a=b&c=d'); + assert.same(params.get('a'), 'b'); + assert.same(params.get('c'), 'd'); + assert.same(params.get('e'), null); + + params = new URLSearchParams('a=b&c=d&a=e'); + assert.same(params.get('a'), 'b'); + + params = new URLSearchParams('=b&c=d'); + assert.same(params.get(''), 'b'); + + params = new URLSearchParams('a=&c=d&a=e'); + assert.same(params.get('a'), ''); + + params = new URLSearchParams('first=second&third&&'); + assert.true(params.has('first'), 'Search params object has name "first"'); + assert.same(params.get('first'), 'second', 'Search params object has name "first" with value "second"'); + assert.same(params.get('third'), '', 'Search params object has name "third" with the empty value.'); + assert.same(params.get('fourth'), null, 'Search params object has no "fourth" name and value.'); + + assert.same(new URLSearchParams('a=b c').get('a'), 'b c'); + assert.same(new URLSearchParams('a b=c').get('a b'), 'c'); + + assert.same(new URLSearchParams('a=b%20c').get('a'), 'b c', 'parse %20'); + assert.same(new URLSearchParams('a%20b=c').get('a b'), 'c', 'parse %20'); + + assert.same(new URLSearchParams('a=b\0c').get('a'), 'b\0c', 'parse \\0'); + assert.same(new URLSearchParams('a\0b=c').get('a\0b'), 'c', 'parse \\0'); + + assert.same(new URLSearchParams('a=b%2Bc').get('a'), 'b+c', 'parse %2B'); + assert.same(new URLSearchParams('a%2Bb=c').get('a+b'), 'c', 'parse %2B'); + + assert.same(new URLSearchParams('a=b%00c').get('a'), 'b\0c', 'parse %00'); + assert.same(new URLSearchParams('a%00b=c').get('a\0b'), 'c', 'parse %00'); + + assert.same(new URLSearchParams('a==').get('a'), '=', 'parse ='); + assert.same(new URLSearchParams('a=b=').get('a'), 'b=', 'parse ='); + assert.same(new URLSearchParams('a=b=c').get('a'), 'b=c', 'parse ='); + assert.same(new URLSearchParams('a==b').get('a'), '=b', 'parse ='); + + assert.same(new URLSearchParams('a=b\u2384').get('a'), 'b\u2384', 'parse \\u2384'); + assert.same(new URLSearchParams('a\u2384b=c').get('a\u2384b'), 'c', 'parse \\u2384'); + + assert.same(new URLSearchParams('a=b%e2%8e%84').get('a'), 'b\u2384', 'parse %e2%8e%84'); + assert.same(new URLSearchParams('a%e2%8e%84b=c').get('a\u2384b'), 'c', 'parse %e2%8e%84'); + + assert.same(new URLSearchParams('a=b\uD83D\uDCA9c').get('a'), 'b\uD83D\uDCA9c', 'parse \\uD83D\\uDCA9'); + assert.same(new URLSearchParams('a\uD83D\uDCA9b=c').get('a\uD83D\uDCA9b'), 'c', 'parse \\uD83D\\uDCA9'); + + assert.same(new URLSearchParams('a=b%f0%9f%92%a9c').get('a'), 'b\uD83D\uDCA9c', 'parse %f0%9f%92%a9'); + assert.same(new URLSearchParams('a%f0%9f%92%a9b=c').get('a\uD83D\uDCA9b'), 'c', 'parse %f0%9f%92%a9'); + + assert.same(new URLSearchParams('=').get(''), '', 'parse ='); + + assert.throws(() => + { + return new URLSearchParams('').get(); + }, 'TypeError: Not enough arguments'); +}); + +QUnit.test('URLSearchParams#getAll', assert => +{ + const { getAll } = URLSearchParams.prototype; + assert.isFunction(getAll); + assert.arity(getAll, 1); + assert.name(getAll, 'getAll'); + assert.enumerable(URLSearchParams.prototype, 'getAll'); + if (!NODE) assert.looksNative(getAll); + + let params = new URLSearchParams('a=b&c=d'); + assert.arrayEqual(params.getAll('a'), ['b']); + assert.arrayEqual(params.getAll('c'), ['d']); + assert.arrayEqual(params.getAll('e'), []); + + params = new URLSearchParams('a=b&c=d&a=e'); + assert.arrayEqual(params.getAll('a'), ['b', 'e']); + + params = new URLSearchParams('=b&c=d'); + assert.arrayEqual(params.getAll(''), ['b']); + + params = new URLSearchParams('a=&c=d&a=e'); + assert.arrayEqual(params.getAll('a'), ['', 'e']); + + params = new URLSearchParams('a=1&a=2&a=3&a'); + assert.arrayEqual(params.getAll('a'), ['1', '2', '3', ''], 'search params object has expected name "a" values'); + params.set('a', 'one'); + assert.arrayEqual(params.getAll('a'), ['one'], 'search params object has expected name "a" values'); + + assert.throws(() => + { + return new URLSearchParams('').getAll(); + }, 'TypeError: Not enough arguments'); +}); + +QUnit.test('URLSearchParams#has', assert => +{ + const { has } = URLSearchParams.prototype; + assert.isFunction(has); + assert.arity(has, 1); + assert.name(has, 'has'); + assert.enumerable(URLSearchParams.prototype, 'has'); + if (!NODE) assert.looksNative(has); + + let params = new URLSearchParams('a=b&c=d'); + assert.true(params.has('a')); + assert.true(params.has('c')); + assert.false(params.has('e')); + + params = new URLSearchParams('a=b&c=d&a=e'); + assert.true(params.has('a')); + + params = new URLSearchParams('=b&c=d'); + assert.true(params.has('')); + + params = new URLSearchParams('null=a'); + assert.true(params.has(null)); + + params = new URLSearchParams('a=b&c=d&&'); + params.append('first', 1); + params.append('first', 2); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.true(params.has('c'), 'search params object has name "c"'); + assert.true(params.has('first'), 'search params object has name "first"'); + assert.false(params.has('d'), 'search params object has no name "d"'); + params.delete('first'); + assert.false(params.has('first'), 'search params object has no name "first"'); + + params = new URLSearchParams('a=1&a=2&a=null&a=3&b=4'); + assert.true(params.has('a', 2)); + assert.true(params.has('a', null)); + assert.false(params.has('a', 4)); + assert.true(params.has('b', 4)); + assert.false(params.has('b', null)); + assert.true(params.has('b', undefined)); + assert.false(params.has('c', undefined)); + + assert.throws(() => + { + return new URLSearchParams('').has(); + }, 'TypeError: Not enough arguments'); +}); + +QUnit.test('URLSearchParams#set', assert => +{ + const { set } = URLSearchParams.prototype; + assert.isFunction(set); + assert.arity(set, 2); + assert.name(set, 'set'); + assert.enumerable(URLSearchParams.prototype, 'set'); + if (!NODE) assert.looksNative(set); + + let params = new URLSearchParams('a=b&c=d'); + params.set('a', 'B'); + assert.same(String(params), 'a=B&c=d'); + + params = new URLSearchParams('a=b&c=d&a=e'); + params.set('a', 'B'); + assert.same(String(params), 'a=B&c=d'); + params.set('e', 'f'); + assert.same(String(params), 'a=B&c=d&e=f'); + + params = new URLSearchParams('a=1&a=2&a=3'); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.same(params.get('a'), '1', 'search params object has name "a" with value "1"'); + params.set('first', 4); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.same(params.get('a'), '1', 'search params object has name "a" with value "1"'); + assert.same(String(params), 'a=1&a=2&a=3&first=4'); + params.set('a', 4); + assert.true(params.has('a'), 'search params object has name "a"'); + assert.same(params.get('a'), '4', 'search params object has name "a" with value "4"'); + assert.same(String(params), 'a=4&first=4'); + + assert.throws(() => + { + return new URLSearchParams('').set(); + }, 'TypeError: Not enough arguments'); +}); + +QUnit.test('URLSearchParams#sort', assert => +{ + const { sort } = URLSearchParams.prototype; + assert.isFunction(sort); + assert.arity(sort, 0); + assert.name(sort, 'sort'); + assert.enumerable(URLSearchParams.prototype, 'sort'); + if (!NODE) assert.looksNative(sort); + + let params = new URLSearchParams('a=1&b=4&a=3&b=2'); + params.sort(); + assert.same(String(params), 'a=1&a=3&b=4&b=2'); + params.delete('a'); + params.append('a', '0'); + params.append('b', '0'); + params.sort(); + assert.same(String(params), 'a=0&b=4&b=2&b=0'); + + const testData = [ + { + input: 'z=b&a=b&z=a&a=a', + output: [['a', 'b'], ['a', 'a'], ['z', 'b'], ['z', 'a']], + }, + { + input: '\uFFFD=x&\uFFFC&\uFFFD=a', + output: [['\uFFFC', ''], ['\uFFFD', 'x'], ['\uFFFD', 'a']], + }, + { + input: 'ffi&🌈', // 🌈 > code point, but < code unit because two code units + output: [['🌈', ''], ['ffi', '']], + }, + { + input: 'é&e\uFFFD&e\u0301', + output: [['e\u0301', ''], ['e\uFFFD', ''], ['é', '']], + }, + { + input: 'z=z&a=a&z=y&a=b&z=x&a=c&z=w&a=d&z=v&a=e&z=u&a=f&z=t&a=g', + output: [ + ['a', 'a'], + ['a', 'b'], + ['a', 'c'], + ['a', 'd'], + ['a', 'e'], + ['a', 'f'], + ['a', 'g'], + ['z', 'z'], + ['z', 'y'], + ['z', 'x'], + ['z', 'w'], + ['z', 'v'], + ['z', 'u'], + ['z', 't'], + ], + }, + { + input: 'bbb&bb&aaa&aa=x&aa=y', + output: [['aa', 'x'], ['aa', 'y'], ['aaa', ''], ['bb', ''], ['bbb', '']], + }, + { + input: 'z=z&=f&=t&=x', + output: [['', 'f'], ['', 't'], ['', 'x'], ['z', 'z']], + }, + { + input: 'a🌈&a💩', + output: [['a🌈', ''], ['a💩', '']], + }, + ]; + + for (const { input, output } of testData) + { + let i = 0; + params = new URLSearchParams(input); + params.sort(); + params.forEach((value, key) => + { + const [reqKey, reqValue] = output[i++]; + assert.same(key, reqKey); + assert.same(value, reqValue); + }); + + i = 0; + const url = new URL(`?${ input }`, 'https://example/'); + params = url.searchParams; + params.sort(); + params.forEach((value, key) => + { + const [reqKey, reqValue] = output[i++]; + assert.same(key, reqKey); + assert.same(value, reqValue); + }); + } + + if (DESCRIPTORS) + { + const url = new URL('http://example.com/?'); + url.searchParams.sort(); + assert.same(url.href, 'http://example.com/', 'Sorting non-existent params removes ? from URL'); + assert.same(url.search, '', 'Sorting non-existent params removes ? from URL'); + } +}); + +QUnit.test('URLSearchParams#toString', assert => +{ + const { toString } = URLSearchParams.prototype; + assert.isFunction(toString); + assert.arity(toString, 0); + assert.name(toString, 'toString'); + if (!NODE) assert.looksNative(toString); + + let params = new URLSearchParams(); + params.append('a', 'b c'); + assert.same(String(params), 'a=b+c'); + params.delete('a'); + params.append('a b', 'c'); + assert.same(String(params), 'a+b=c'); + + params = new URLSearchParams(); + params.append('a', ''); + assert.same(String(params), 'a='); + params.append('a', ''); + assert.same(String(params), 'a=&a='); + params.append('', 'b'); + assert.same(String(params), 'a=&a=&=b'); + params.append('', ''); + assert.same(String(params), 'a=&a=&=b&='); + params.append('', ''); + assert.same(String(params), 'a=&a=&=b&=&='); + + params = new URLSearchParams(); + params.append('', 'b'); + assert.same(String(params), '=b'); + params.append('', 'b'); + assert.same(String(params), '=b&=b'); + + params = new URLSearchParams(); + params.append('', ''); + assert.same(String(params), '='); + params.append('', ''); + assert.same(String(params), '=&='); + + params = new URLSearchParams(); + params.append('a', 'b+c'); + assert.same(String(params), 'a=b%2Bc'); + params.delete('a'); + params.append('a+b', 'c'); + assert.same(String(params), 'a%2Bb=c'); + + params = new URLSearchParams(); + params.append('=', 'a'); + assert.same(String(params), '%3D=a'); + params.append('b', '='); + assert.same(String(params), '%3D=a&b=%3D'); + + params = new URLSearchParams(); + params.append('&', 'a'); + assert.same(String(params), '%26=a'); + params.append('b', '&'); + assert.same(String(params), '%26=a&b=%26'); + + params = new URLSearchParams(); + params.append('a', '\r'); + assert.same(String(params), 'a=%0D'); + + params = new URLSearchParams(); + params.append('a', '\n'); + assert.same(String(params), 'a=%0A'); + + params = new URLSearchParams(); + params.append('a', '\r\n'); + assert.same(String(params), 'a=%0D%0A'); + + params = new URLSearchParams(); + params.append('a', 'b%c'); + assert.same(String(params), 'a=b%25c'); + params.delete('a'); + params.append('a%b', 'c'); + assert.same(String(params), 'a%25b=c'); + + params = new URLSearchParams(); + params.append('a', 'b\0c'); + assert.same(String(params), 'a=b%00c'); + params.delete('a'); + params.append('a\0b', 'c'); + assert.same(String(params), 'a%00b=c'); + + params = new URLSearchParams(); + params.append('a', 'b\uD83D\uDCA9c'); + assert.same(String(params), 'a=b%F0%9F%92%A9c'); + params.delete('a'); + params.append('a\uD83D\uDCA9b', 'c'); + assert.same(String(params), 'a%F0%9F%92%A9b=c'); + + params = new URLSearchParams('a=b&c=d&&e&&'); + assert.same(String(params), 'a=b&c=d&e='); + params = new URLSearchParams('a = b &a=b&c=d%20'); + assert.same(String(params), 'a+=+b+&a=b&c=d+'); + params = new URLSearchParams('a=&a=b'); + assert.same(String(params), 'a=&a=b'); +}); + +QUnit.test('URLSearchParams#forEach', assert => +{ + const { forEach } = URLSearchParams.prototype; + assert.isFunction(forEach); + assert.arity(forEach, 1); + assert.name(forEach, 'forEach'); + assert.enumerable(URLSearchParams.prototype, 'forEach'); + if (!NODE) assert.looksNative(forEach); + + const expectedValues = { a: '1', b: '2', c: '3' }; + let params = new URLSearchParams('a=1&b=2&c=3'); + let result = ''; + params.forEach((value, key, that) => + { + assert.same(params.get(key), expectedValues[key]); + assert.same(value, expectedValues[key]); + assert.same(that, params); + result += key; + }); + assert.same(result, 'abc'); + + new URL('http://a.b/c').searchParams.forEach(() => + { + assert.avoid(); + }); + + // fails in Chrome 66- + if (DESCRIPTORS) + { + const url = new URL('http://a.b/c?a=1&b=2&c=3&d=4'); + params = url.searchParams; + result = ''; + params.forEach((val, key) => + { + url.search = 'x=1&y=2&z=3'; + result += key + val; + }); + assert.same(result, 'a1y2z3'); + } + + // fails in Chrome 66- + params = new URLSearchParams('a=1&b=2&c=3'); + result = ''; + params.forEach((value, key) => + { + params.delete('b'); + result += key + value; + }); + assert.same(result, 'a1c3'); +}); + +QUnit.test('URLSearchParams#entries', assert => +{ + const { entries } = URLSearchParams.prototype; + assert.isFunction(entries); + assert.arity(entries, 0); + assert.name(entries, 'entries'); + assert.enumerable(URLSearchParams.prototype, 'entries'); + if (!NODE) assert.looksNative(entries); + + const expectedValues = { a: '1', b: '2', c: '3' }; + let params = new URLSearchParams('a=1&b=2&c=3'); + let iterator = params.entries(); + let result = ''; + let entry; + while (!(entry = iterator.next()).done) + { + const [key, value] = entry.value; + assert.same(params.get(key), expectedValues[key]); + assert.same(value, expectedValues[key]); + result += key; + } + assert.same(result, 'abc'); + + assert.true(new URL('http://a.b/c').searchParams.entries().next().done, 'should be finished'); + + // fails in Chrome 66- + if (DESCRIPTORS) + { + const url = new URL('http://a.b/c?a=1&b=2&c=3&d=4'); + iterator = url.searchParams.entries(); + result = ''; + while (!(entry = iterator.next()).done) + { + const [key, value] = entry.value; + url.search = 'x=1&y=2&z=3'; + result += key + value; + } + assert.same(result, 'a1y2z3'); + } + + // fails in Chrome 66- + params = new URLSearchParams('a=1&b=2&c=3'); + iterator = params.entries(); + result = ''; + while (!(entry = iterator.next()).done) + { + params.delete('b'); + const [key, value] = entry.value; + result += key + value; + } + assert.same(result, 'a1c3'); + + if (DESCRIPTORS) assert.true(getOwnPropertyDescriptor(getPrototypeOf(new URLSearchParams().entries()), 'next').enumerable, 'enumerable .next'); +}); + +QUnit.test('URLSearchParams#keys', assert => +{ + const { keys } = URLSearchParams.prototype; + assert.isFunction(keys); + assert.arity(keys, 0); + assert.name(keys, 'keys'); + assert.enumerable(URLSearchParams.prototype, 'keys'); + if (!NODE) assert.looksNative(keys); + + let iterator = new URLSearchParams('a=1&b=2&c=3').keys(); + let result = ''; + let entry; + while (!(entry = iterator.next()).done) + { + result += entry.value; + } + assert.same(result, 'abc'); + + assert.true(new URL('http://a.b/c').searchParams.keys().next().done, 'should be finished'); + + // fails in Chrome 66- + if (DESCRIPTORS) + { + const url = new URL('http://a.b/c?a=1&b=2&c=3&d=4'); + iterator = url.searchParams.keys(); + result = ''; + while (!(entry = iterator.next()).done) + { + const key = entry.value; + url.search = 'x=1&y=2&z=3'; + result += key; + } + assert.same(result, 'ayz'); + } + + // fails in Chrome 66- + const params = new URLSearchParams('a=1&b=2&c=3'); + iterator = params.keys(); + result = ''; + while (!(entry = iterator.next()).done) + { + params.delete('b'); + const key = entry.value; + result += key; + } + assert.same(result, 'ac'); + + if (DESCRIPTORS) assert.true(getOwnPropertyDescriptor(getPrototypeOf(new URLSearchParams().keys()), 'next').enumerable, 'enumerable .next'); +}); + +QUnit.test('URLSearchParams#values', assert => +{ + const { values } = URLSearchParams.prototype; + assert.isFunction(values); + assert.arity(values, 0); + assert.name(values, 'values'); + assert.enumerable(URLSearchParams.prototype, 'values'); + if (!NODE) assert.looksNative(values); + + let iterator = new URLSearchParams('a=1&b=2&c=3').values(); + let result = ''; + let entry; + while (!(entry = iterator.next()).done) + { + result += entry.value; + } + assert.same(result, '123'); + + assert.true(new URL('http://a.b/c').searchParams.values().next().done, 'should be finished'); + + // fails in Chrome 66- + if (DESCRIPTORS) + { + const url = new URL('http://a.b/c?a=a&b=b&c=c&d=d'); + iterator = url.searchParams.keys(); + result = ''; + while (!(entry = iterator.next()).done) + { + const { value } = entry; + url.search = 'x=x&y=y&z=z'; + result += value; + } + assert.same(result, 'ayz'); + } + + // fails in Chrome 66- + const params = new URLSearchParams('a=1&b=2&c=3'); + iterator = params.values(); + result = ''; + while (!(entry = iterator.next()).done) + { + params.delete('b'); + const key = entry.value; + result += key; + } + assert.same(result, '13'); + + if (DESCRIPTORS) assert.true(getOwnPropertyDescriptor(getPrototypeOf(new URLSearchParams().values()), 'next').enumerable, 'enumerable .next'); +}); + +QUnit.test('URLSearchParams#@@iterator', assert => +{ + const entries = URLSearchParams.prototype[Symbol.iterator]; + assert.isFunction(entries); + assert.arity(entries, 0); + assert.name(entries, 'entries'); + if (!NODE) assert.looksNative(entries); + + assert.same(entries, URLSearchParams.prototype.entries); + + const expectedValues = { a: '1', b: '2', c: '3' }; + let params = new URLSearchParams('a=1&b=2&c=3'); + let iterator = params[Symbol.iterator](); + let result = ''; + let entry; + while (!(entry = iterator.next()).done) + { + const [key, value] = entry.value; + assert.same(params.get(key), expectedValues[key]); + assert.same(value, expectedValues[key]); + result += key; + } + assert.same(result, 'abc'); + + assert.true(new URL('http://a.b/c').searchParams[Symbol.iterator]().next().done, 'should be finished'); + + // fails in Chrome 66- + if (DESCRIPTORS) + { + const url = new URL('http://a.b/c?a=1&b=2&c=3&d=4'); + iterator = url.searchParams[Symbol.iterator](); + result = ''; + while (!(entry = iterator.next()).done) + { + const [key, value] = entry.value; + url.search = 'x=1&y=2&z=3'; + result += key + value; + } + assert.same(result, 'a1y2z3'); + } + + // fails in Chrome 66- + params = new URLSearchParams('a=1&b=2&c=3'); + iterator = params[Symbol.iterator](); + result = ''; + while (!(entry = iterator.next()).done) + { + params.delete('b'); + const [key, value] = entry.value; + result += key + value; + } + assert.same(result, 'a1c3'); + + if (DESCRIPTORS) assert.true(getOwnPropertyDescriptor(getPrototypeOf(new URLSearchParams()[Symbol.iterator]()), 'next').enumerable, 'enumerable .next'); +}); + +QUnit.test('URLSearchParams#size', assert => +{ + const params = new URLSearchParams('a=1&b=2&b=3'); + assert.true('size' in params); + assert.same(params.size, 3); + + if (DESCRIPTORS) + { + assert.true('size' in URLSearchParams.prototype); + + const { enumerable, configurable, get } = getOwnPropertyDescriptor(URLSearchParams.prototype, 'size'); + + assert.true(enumerable, 'enumerable'); + assert.true(configurable, 'configurable'); + + if (!NODE) assert.looksNative(get); + + assert.throws(() => get.call([]), 'TypeError: Incompatible receiver, URLSearchParams required'); + } +}); + +QUnit.test('URLSearchParams#@@toStringTag', assert => +{ + const params = new URLSearchParams('a=b'); + assert.same(({}).toString.call(params), '[object URLSearchParams]'); +}); + +if (typeof Request === 'function') +{ + QUnit.test('URLSearchParams with Request', assert => + { + const async = assert.async(); + new Request('http://zloirock.ru', { body: new URLSearchParams({ foo: 'baz' }), method: 'POST' }).text().then(text => + { + assert.same(text, 'foo=baz'); + async(); + }); + }); +} diff --git a/tests/js/url.simple b/tests/js/url.simple new file mode 100644 index 00000000..68a3083d --- /dev/null +++ b/tests/js/url.simple @@ -0,0 +1,700 @@ +/** + * @file url.simple + * Simple test for the URL interface + * @author Tom Tang + * @date Aug 2023 + */ + +const QUnit = require('./quint'); + +/* ! + * Modified from https://github.com/zloirock/core-js/blob/d99baeff/tests/unit-global/web.url.js + * core-js + * MIT License + */ + +const DESCRIPTORS = true; +const NODE = false; + +const { hasOwnProperty } = Object.prototype; + +QUnit.test('URL constructor', assert => +{ + assert.isFunction(URL); + if (!NODE) assert.arity(URL, 1); + assert.name(URL, 'URL'); + if (!NODE) assert.looksNative(URL); + + assert.same(String(new URL('http://www.domain.com/a/b')), 'http://www.domain.com/a/b'); + assert.same(String(new URL('/c/d', 'http://www.domain.com/a/b')), 'http://www.domain.com/c/d'); + assert.same(String(new URL('b/c', 'http://www.domain.com/a/b')), 'http://www.domain.com/a/b/c'); + assert.same(String(new URL('b/c', new URL('http://www.domain.com/a/b'))), 'http://www.domain.com/a/b/c'); + assert.same(String(new URL({ toString: () => 'https://example.org/' })), 'https://example.org/'); + + assert.same(String(new URL('nonspecial://example.com/')), 'nonspecial://example.com/'); + + assert.same(String(new URL('https://測試')), 'https://xn--g6w251d/', 'unicode parsing'); + assert.same(String(new URL('https://xxпривет.тест')), 'https://xn--xx-flcmn5bht.xn--e1aybc/', 'unicode parsing'); + assert.same(String(new URL('https://xxПРИВЕТ.тест')), 'https://xn--xx-flcmn5bht.xn--e1aybc/', 'unicode parsing'); + assert.same(String(new URL('http://Example.com/', 'https://example.org/')), 'http://example.com/'); + assert.same(String(new URL('https://Example.com/', 'https://example.org/')), 'https://example.com/'); + assert.same(String(new URL('nonspecial://Example.com/', 'https://example.org/')), 'nonspecial://Example.com/'); + assert.same(String(new URL('http:Example.com/', 'https://example.org/')), 'http://example.com/'); + assert.same(String(new URL('https:Example.com/', 'https://example.org/')), 'https://example.org/Example.com/'); + assert.same(String(new URL('nonspecial:Example.com/', 'https://example.org/')), 'nonspecial:Example.com/'); + + assert.same(String(new URL('http://0300.168.0xF0')), 'http://192.168.0.240/'); + assert.same(String(new URL('http://[20:0:0:1:0:0:0:ff]')), 'http://[20:0:0:1::ff]/'); + // assert.same(String(new URL('http://257.168.0xF0')), 'http://257.168.0xf0/', 'incorrect IPv4 parsed as host'); // TypeError in Chrome and Safari + assert.same(String(new URL('http://0300.168.0xG0')), 'http://0300.168.0xg0/', 'incorrect IPv4 parsed as host'); + + assert.same(String(new URL('file:///var/log/system.log')), 'file:///var/log/system.log', 'file scheme'); + // assert.same(String(new URL('file://nnsc.nsf.net/bar/baz')), 'file://nnsc.nsf.net/bar/baz', 'file scheme'); // 'file:///bar/baz' in FF + // assert.same(String(new URL('file://localhost/bar/baz')), 'file:///bar/baz', 'file scheme'); // 'file://localhost/bar/baz' in Chrome + + assert.throws(() => new URL(), 'TypeError: Not enough arguments'); + assert.throws(() => new URL(''), 'TypeError: Invalid scheme'); + // Node 19.7 + // https://github.com/nodejs/node/issues/46755 + // assert.throws(() => new URL('', 'about:blank'), 'TypeError: Failed to construct URL: Invalid URL'); + assert.throws(() => new URL('abc'), 'TypeError: Invalid scheme'); + assert.throws(() => new URL('//abc'), 'TypeError: Invalid scheme'); + assert.throws(() => new URL('http:///www.domain.com/', 'abc'), 'TypeError: Invalid scheme'); + assert.throws(() => new URL('http:///www.domain.com/', null), 'TypeError: Invalid scheme'); + assert.throws(() => new URL('//abc', null), 'TypeError: Invalid scheme'); + assert.throws(() => new URL('http://[20:0:0:1:0:0:0:ff'), 'TypeError: Invalid host'); + assert.throws(() => new URL('http://[20:0:0:1:0:0:0:fg]'), 'TypeError: Invalid host'); + // assert.throws(() => new URL('http://a%b'), 'forbidden host code point'); // no error in FF + assert.throws(() => new URL('1http://zloirock.ru'), 'TypeError: Invalid scheme'); +}); + +QUnit.test('URL#href', assert => +{ + let url = new URL('http://zloirock.ru/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'href')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'href'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.href, 'http://zloirock.ru/'); + + if (DESCRIPTORS) + { + url.searchParams.append('foo', 'bar'); + assert.same(url.href, 'http://zloirock.ru/?foo=bar'); + + url = new URL('http://zloirock.ru/foo'); + url.href = 'https://測試'; + assert.same(url.href, 'https://xn--g6w251d/', 'unicode parsing'); + assert.same(String(url), 'https://xn--g6w251d/', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.href = 'https://xxпривет.тест'; + assert.same(url.href, 'https://xn--xx-flcmn5bht.xn--e1aybc/', 'unicode parsing'); + assert.same(String(url), 'https://xn--xx-flcmn5bht.xn--e1aybc/', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.href = 'https://xxПРИВЕТ.тест'; + assert.same(url.href, 'https://xn--xx-flcmn5bht.xn--e1aybc/', 'unicode parsing'); + assert.same(String(url), 'https://xn--xx-flcmn5bht.xn--e1aybc/', 'unicode parsing'); + + url = new URL('http://zloirock.ru/'); + url.href = 'http://0300.168.0xF0'; + assert.same(url.href, 'http://192.168.0.240/'); + assert.same(String(url), 'http://192.168.0.240/'); + + url = new URL('http://zloirock.ru/'); + url.href = 'http://[20:0:0:1:0:0:0:ff]'; + assert.same(url.href, 'http://[20:0:0:1::ff]/'); + assert.same(String(url), 'http://[20:0:0:1::ff]/'); + + // url = new URL('http://zloirock.ru/'); + // url.href = 'http://257.168.0xF0'; // TypeError and Safari + // assert.same(url.href, 'http://257.168.0xf0/', 'incorrect IPv4 parsed as host'); // `F` instead of `f` in Chrome + // assert.same(String(url), 'http://257.168.0xf0/', 'incorrect IPv4 parsed as host'); // `F` instead of `f` in Chrome + + url = new URL('http://zloirock.ru/'); + url.href = 'http://0300.168.0xG0'; + assert.same(url.href, 'http://0300.168.0xg0/', 'incorrect IPv4 parsed as host'); + assert.same(String(url), 'http://0300.168.0xg0/', 'incorrect IPv4 parsed as host'); + + url = new URL('http://192.168.0.240/'); + url.href = 'file:///var/log/system.log'; + assert.same(url.href, 'file:///var/log/system.log', 'file -> ip'); + assert.same(String(url), 'file:///var/log/system.log', 'file -> ip'); + + url = new URL('file:///var/log/system.log'); + url.href = 'http://0300.168.0xF0'; + // Node 19.7 + // https://github.com/nodejs/node/issues/46755 + // assert.same(url.href, 'http://192.168.0.240/', 'file -> http'); + // assert.same(String(url), 'http://192.168.0.240/', 'file -> http'); + + // assert.throws(() => new URL('http://zloirock.ru/').href = undefined, 'incorrect URL'); // no error in Chrome + // assert.throws(() => new URL('http://zloirock.ru/').href = '', 'incorrect URL'); // no error in Chrome + // assert.throws(() => new URL('http://zloirock.ru/').href = 'abc', 'incorrect URL'); // no error in Chrome + // assert.throws(() => new URL('http://zloirock.ru/').href = '//abc', 'incorrect URL'); // no error in Chrome + // assert.throws(() => new URL('http://zloirock.ru/').href = 'http://[20:0:0:1:0:0:0:ff', 'incorrect IPv6'); // no error in Chrome + // assert.throws(() => new URL('http://zloirock.ru/').href = 'http://[20:0:0:1:0:0:0:fg]', 'incorrect IPv6'); // no error in Chrome + // assert.throws(() => new URL('http://zloirock.ru/').href = 'http://a%b', 'forbidden host code point'); // no error in Chrome and FF + // assert.throws(() => new URL('http://zloirock.ru/').href = '1http://zloirock.ru', 'incorrect scheme'); // no error in Chrome + } +}); + +QUnit.test('URL#origin', assert => +{ + const url = new URL('http://es6.zloirock.ru/tests.html'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'origin')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'origin'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + } + + assert.same(url.origin, 'http://es6.zloirock.ru'); + + assert.same(new URL('https://測試/tests').origin, 'https://xn--g6w251d'); +}); + +QUnit.test('URL#protocol', assert => +{ + let url = new URL('http://zloirock.ru/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'protocol')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'protocol'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.protocol, 'http:'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/'); + url.protocol = 'https'; + assert.same(url.protocol, 'https:'); + assert.same(String(url), 'https://zloirock.ru/'); + + // https://nodejs.org/api/url.html#url_special_schemes + // url = new URL('http://zloirock.ru/'); + // url.protocol = 'fish'; + // assert.same(url.protocol, 'http:'); + // assert.same(url.href, 'http://zloirock.ru/'); + // assert.same(String(url), 'http://zloirock.ru/'); + + url = new URL('http://zloirock.ru/'); + url.protocol = '1http'; + assert.same(url.protocol, 'http:'); + assert.same(url.href, 'http://zloirock.ru/', 'incorrect scheme'); + assert.same(String(url), 'http://zloirock.ru/', 'incorrect scheme'); + } +}); + +QUnit.test('URL#username', assert => +{ + let url = new URL('http://zloirock.ru/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'username')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'username'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.username, ''); + + url = new URL('http://username@zloirock.ru/'); + assert.same(url.username, 'username'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/'); + url.username = 'username'; + assert.same(url.username, 'username'); + assert.same(String(url), 'http://username@zloirock.ru/'); + } +}); + +QUnit.test('URL#password', assert => +{ + let url = new URL('http://zloirock.ru/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'password')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'password'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.password, ''); + + url = new URL('http://username:password@zloirock.ru/'); + assert.same(url.password, 'password'); + + // url = new URL('http://:password@zloirock.ru/'); // TypeError in FF + // assert.same(url.password, 'password'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/'); + url.username = 'username'; + url.password = 'password'; + assert.same(url.password, 'password'); + assert.same(String(url), 'http://username:password@zloirock.ru/'); + + // url = new URL('http://zloirock.ru/'); + // url.password = 'password'; + // assert.same(url.password, 'password'); // '' in FF + // assert.same(String(url), 'http://:password@zloirock.ru/'); // 'http://zloirock.ru/' in FF + } +}); + +QUnit.test('URL#host', assert => +{ + let url = new URL('http://zloirock.ru:81/path'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'host')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'host'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.host, 'zloirock.ru:81'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru:81/path'); + url.host = 'example.com:82'; + assert.same(url.host, 'example.com:82'); + assert.same(String(url), 'http://example.com:82/path'); + + // url = new URL('http://zloirock.ru:81/path'); + // url.host = 'other?domain.com'; + // assert.same(String(url), 'http://other:81/path'); // 'http://other/?domain.com/path' in Safari + + url = new URL('https://www.mydomain.com:8080/path/'); + url.host = 'www.otherdomain.com:80'; + assert.same(url.href, 'https://www.otherdomain.com:80/path/', 'set default port for another protocol'); + + // url = new URL('https://www.mydomain.com:8080/path/'); + // url.host = 'www.otherdomain.com:443'; + // assert.same(url.href, 'https://www.otherdomain.com/path/', 'set default port'); + + url = new URL('http://zloirock.ru/foo'); + url.host = '測試'; + assert.same(url.host, 'xn--g6w251d', 'unicode parsing'); + assert.same(String(url), 'http://xn--g6w251d/foo', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.host = 'xxпривет.тест'; + assert.same(url.host, 'xn--xx-flcmn5bht.xn--e1aybc', 'unicode parsing'); + assert.same(String(url), 'http://xn--xx-flcmn5bht.xn--e1aybc/foo', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.host = 'xxПРИВЕТ.тест'; + assert.same(url.host, 'xn--xx-flcmn5bht.xn--e1aybc', 'unicode parsing'); + assert.same(String(url), 'http://xn--xx-flcmn5bht.xn--e1aybc/foo', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.host = '0300.168.0xF0'; + assert.same(url.host, '192.168.0.240'); + assert.same(String(url), 'http://192.168.0.240/foo'); + + // url = new URL('http://zloirock.ru/foo'); + // url.host = '[20:0:0:1:0:0:0:ff]'; + // assert.same(url.host, '[20:0:0:1::ff]'); // ':0' in Chrome, 'zloirock.ru' in Safari + // assert.same(String(url), 'http://[20:0:0:1::ff]/foo'); // 'http://[20:0/foo' in Chrome, 'http://zloirock.ru/foo' in Safari + + // url = new URL('file:///var/log/system.log'); + // url.host = 'nnsc.nsf.net'; // does not work in FF + // assert.same(url.hostname, 'nnsc.nsf.net', 'file'); + // assert.same(String(url), 'file://nnsc.nsf.net/var/log/system.log', 'file'); + + // url = new URL('http://zloirock.ru/'); + // url.host = '[20:0:0:1:0:0:0:ff'; + // assert.same(url.host, 'zloirock.ru', 'incorrect IPv6'); // ':0' in Chrome + // assert.same(String(url), 'http://zloirock.ru/', 'incorrect IPv6'); // 'http://[20:0/' in Chrome + + // url = new URL('http://zloirock.ru/'); + // url.host = '[20:0:0:1:0:0:0:fg]'; + // assert.same(url.host, 'zloirock.ru', 'incorrect IPv6'); // ':0' in Chrome + // assert.same(String(url), 'http://zloirock.ru/', 'incorrect IPv6'); // 'http://[20:0/' in Chrome + + // url = new URL('http://zloirock.ru/'); + // url.host = 'a%b'; + // assert.same(url.host, 'zloirock.ru', 'forbidden host code point'); // '' in Chrome, 'a%b' in FF + // assert.same(String(url), 'http://zloirock.ru/', 'forbidden host code point'); // 'http://a%25b/' in Chrome, 'http://a%b/' in FF + } +}); + +QUnit.test('URL#hostname', assert => +{ + let url = new URL('http://zloirock.ru:81/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'hostname')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'hostname'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.hostname, 'zloirock.ru'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru:81/'); + url.hostname = 'example.com'; + assert.same(url.hostname, 'example.com'); + assert.same(String(url), 'http://example.com:81/'); + + // url = new URL('http://zloirock.ru:81/'); + // url.hostname = 'example.com:82'; + // assert.same(url.hostname, 'example.com'); // '' in Chrome + // assert.same(String(url), 'http://example.com:81/'); // 'http://example.com:82:81/' in Chrome + + url = new URL('http://zloirock.ru/foo'); + url.hostname = '測試'; + assert.same(url.hostname, 'xn--g6w251d', 'unicode parsing'); + assert.same(String(url), 'http://xn--g6w251d/foo', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.hostname = 'xxпривет.тест'; + assert.same(url.hostname, 'xn--xx-flcmn5bht.xn--e1aybc', 'unicode parsing'); + assert.same(String(url), 'http://xn--xx-flcmn5bht.xn--e1aybc/foo', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.hostname = 'xxПРИВЕТ.тест'; + assert.same(url.hostname, 'xn--xx-flcmn5bht.xn--e1aybc', 'unicode parsing'); + assert.same(String(url), 'http://xn--xx-flcmn5bht.xn--e1aybc/foo', 'unicode parsing'); + + url = new URL('http://zloirock.ru/foo'); + url.hostname = '0300.168.0xF0'; + assert.same(url.hostname, '192.168.0.240'); + assert.same(String(url), 'http://192.168.0.240/foo'); + + // url = new URL('http://zloirock.ru/foo'); + // url.hostname = '[20:0:0:1:0:0:0:ff]'; + // assert.same(url.hostname, '[20:0:0:1::ff]'); // 'zloirock.ru' in Safari + // assert.same(String(url), 'http://[20:0:0:1::ff]/foo'); // 'http://zloirock.ru/foo' in Safari + + // url = new URL('file:///var/log/system.log'); + // url.hostname = 'nnsc.nsf.net'; // does not work in FF + // assert.same(url.hostname, 'nnsc.nsf.net', 'file'); + // assert.same(String(url), 'file://nnsc.nsf.net/var/log/system.log', 'file'); + + // url = new URL('http://zloirock.ru/'); + // url.hostname = '[20:0:0:1:0:0:0:ff'; + // assert.same(url.hostname, 'zloirock.ru', 'incorrect IPv6'); // '' in Chrome + // assert.same(String(url), 'http://zloirock.ru/', 'incorrect IPv6'); // 'http://[20:0:0:1:0:0:0:ff' in Chrome + + // url = new URL('http://zloirock.ru/'); + // url.hostname = '[20:0:0:1:0:0:0:fg]'; + // assert.same(url.hostname, 'zloirock.ru', 'incorrect IPv6'); // '' in Chrome + // assert.same(String(url), 'http://zloirock.ru/', 'incorrect IPv6'); // 'http://[20:0:0:1:0:0:0:ff/' in Chrome + + // url = new URL('http://zloirock.ru/'); + // url.hostname = 'a%b'; + // assert.same(url.hostname, 'zloirock.ru', 'forbidden host code point'); // '' in Chrome, 'a%b' in FF + // assert.same(String(url), 'http://zloirock.ru/', 'forbidden host code point'); // 'http://a%25b/' in Chrome, 'http://a%b/' in FF + } +}); + +QUnit.test('URL#port', assert => +{ + let url = new URL('http://zloirock.ru:1337/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'port')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'port'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.port, '1337'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/'); + url.port = 80; + assert.same(url.port, ''); + assert.same(String(url), 'http://zloirock.ru/'); + url.port = 1337; + assert.same(url.port, '1337'); + assert.same(String(url), 'http://zloirock.ru:1337/'); + // url.port = 'abcd'; + // assert.same(url.port, '1337'); // '0' in Chrome + // assert.same(String(url), 'http://zloirock.ru:1337/'); // 'http://zloirock.ru:0/' in Chrome + // url.port = '5678abcd'; + // assert.same(url.port, '5678'); // '1337' in FF + // assert.same(String(url), 'http://zloirock.ru:5678/'); // 'http://zloirock.ru:1337/"' in FF + url.port = 1234.5678; + assert.same(url.port, '1234'); + assert.same(String(url), 'http://zloirock.ru:1234/'); + // url.port = 1e10; + // assert.same(url.port, '1234'); // '0' in Chrome + // assert.same(String(url), 'http://zloirock.ru:1234/'); // 'http://zloirock.ru:0/' in Chrome + } +}); + +QUnit.test('URL#pathname', assert => +{ + let url = new URL('http://zloirock.ru/foo/bar'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'pathname')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'pathname'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.pathname, '/foo/bar'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/'); + url.pathname = 'bar/baz'; + assert.same(url.pathname, '/bar/baz'); + assert.same(String(url), 'http://zloirock.ru/bar/baz'); + } +}); + +QUnit.test('URL#search', assert => +{ + let url = new URL('http://zloirock.ru/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'search')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'search'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.search, ''); + + url = new URL('http://zloirock.ru/?foo=bar'); + assert.same(url.search, '?foo=bar'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/?'); + assert.same(url.search, ''); + assert.same(String(url), 'http://zloirock.ru/?'); + url.search = 'foo=bar'; + assert.same(url.search, '?foo=bar'); + assert.same(String(url), 'http://zloirock.ru/?foo=bar'); + url.search = '?bar=baz'; + assert.same(url.search, '?bar=baz'); + assert.same(String(url), 'http://zloirock.ru/?bar=baz'); + url.search = ''; + assert.same(url.search, ''); + assert.same(String(url), 'http://zloirock.ru/'); + } +}); + +QUnit.test('URL#searchParams', assert => +{ + let url = new URL('http://zloirock.ru/?foo=bar&bar=baz'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'searchParams')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'searchParams'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + } + + assert.true(url.searchParams instanceof URLSearchParams); + assert.same(url.searchParams.get('foo'), 'bar'); + assert.same(url.searchParams.get('bar'), 'baz'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/'); + url.searchParams.append('foo', 'bar'); + assert.same(String(url), 'http://zloirock.ru/?foo=bar'); + + url = new URL('http://zloirock.ru/'); + url.search = 'foo=bar'; + assert.same(url.searchParams.get('foo'), 'bar'); + + url = new URL('http://zloirock.ru/?foo=bar&bar=baz'); + url.search = ''; + assert.false(url.searchParams.has('foo')); + } +}); + +QUnit.test('URL#hash', assert => +{ + let url = new URL('http://zloirock.ru/'); + + if (DESCRIPTORS) + { + assert.false(hasOwnProperty.call(url, 'hash')); + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'hash'); + assert.true(descriptor.enumerable); + assert.true(descriptor.configurable); + assert.same(typeof descriptor.get, 'function'); + assert.same(typeof descriptor.set, 'function'); + } + + assert.same(url.hash, ''); + + url = new URL('http://zloirock.ru/#foo'); + assert.same(url.hash, '#foo'); + + url = new URL('http://zloirock.ru/#'); + assert.same(url.hash, ''); + assert.same(String(url), 'http://zloirock.ru/#'); + + if (DESCRIPTORS) + { + url = new URL('http://zloirock.ru/#'); + url.hash = 'foo'; + assert.same(url.hash, '#foo'); + assert.same(String(url), 'http://zloirock.ru/#foo'); + url.hash = ''; + assert.same(url.hash, ''); + assert.same(String(url), 'http://zloirock.ru/'); + // url.hash = '#'; + // assert.same(url.hash, ''); + // assert.same(String(url), 'http://zloirock.ru/'); // 'http://zloirock.ru/#' in FF + url.hash = '#foo'; + assert.same(url.hash, '#foo'); + assert.same(String(url), 'http://zloirock.ru/#foo'); + url.hash = '#foo#bar'; + assert.same(url.hash, '#foo#bar'); + assert.same(String(url), 'http://zloirock.ru/#foo#bar'); + + url = new URL('http://zloirock.ru/'); + url.hash = 'абa'; + assert.same(url.hash, '#%D0%B0%D0%B1a'); + + // url = new URL('http://zloirock.ru/'); + // url.hash = '\udc01\ud802a'; + // assert.same(url.hash, '#%EF%BF%BD%EF%BF%BDa', 'unmatched surrogates'); + } +}); + +QUnit.test('URL#toJSON', assert => +{ + const { toJSON } = URL.prototype; + assert.isFunction(toJSON); + assert.arity(toJSON, 0); + assert.name(toJSON, 'toJSON'); + assert.enumerable(URL.prototype, 'toJSON'); + if (!NODE) assert.looksNative(toJSON); + + const url = new URL('http://zloirock.ru/'); + assert.same(url.toJSON(), 'http://zloirock.ru/'); + + if (DESCRIPTORS) + { + url.searchParams.append('foo', 'bar'); + assert.same(url.toJSON(), 'http://zloirock.ru/?foo=bar'); + } +}); + +QUnit.test('URL#toString', assert => +{ + const { toString } = URL.prototype; + assert.isFunction(toString); + assert.arity(toString, 0); + assert.name(toString, 'toString'); + assert.enumerable(URL.prototype, 'toString'); + if (!NODE) assert.looksNative(toString); + + const url = new URL('http://zloirock.ru/'); + assert.same(url.toString(), 'http://zloirock.ru/'); + + if (DESCRIPTORS) + { + url.searchParams.append('foo', 'bar'); + assert.same(url.toString(), 'http://zloirock.ru/?foo=bar'); + } +}); + +QUnit.test('URL#@@toStringTag', assert => +{ + const url = new URL('http://zloirock.ru/'); + assert.same(({}).toString.call(url), '[object URL]'); +}); + +QUnit.test('URL.sham', assert => +{ + assert.same(URL.sham, DESCRIPTORS ? undefined : true); +}); + +/* ! + * Modified from https://github.com/zloirock/core-js/blob/d99baeff/tests/unit-global/web.url.can-parse.js + * core-js + * MIT License + */ + +QUnit.test('URL.canParse', assert => +{ + const { canParse } = URL; + + assert.isFunction(canParse); + assert.arity(canParse, 1); + assert.name(canParse, 'canParse'); + if (!NODE) assert.looksNative(canParse); + + assert.false(canParse(undefined), 'undefined'); + assert.false(canParse(undefined, undefined), 'undefined, undefined'); + assert.true(canParse('q:w'), 'q:w'); + assert.true(canParse('q:w', undefined), 'q:w, undefined'); + // assert.false(canParse(undefined, 'q:w'), 'undefined, q:w'); // fails in Chromium on Windows + assert.true(canParse('q:/w'), 'q:/w'); + assert.true(canParse('q:/w', undefined), 'q:/w, undefined'); + assert.true(canParse(undefined, 'q:/w'), 'undefined, q:/w'); + assert.false(canParse('https://login:password@examp:le.com:8080/?a=1&b=2&a=3&c=4#fragment'), 'https://login:password@examp:le.com:8080/?a=1&b=2&a=3&c=4#fragment'); + assert.true(canParse('https://login:password@example.com:8080/?a=1&b=2&a=3&c=4#fragment'), 'https://login:password@example.com:8080/?a=1&b=2&a=3&c=4#fragment'); + assert.true(canParse('https://login:password@example.com:8080/?a=1&b=2&a=3&c=4#fragment', undefined), 'https://login:password@example.com:8080/?a=1&b=2&a=3&c=4#fragment, undefined'); + assert.true(canParse('x', 'https://login:password@example.com:8080/?a=1&b=2&a=3&c=4#fragment'), 'x, https://login:password@example.com:8080/?a=1&b=2&a=3&c=4#fragment'); + + assert.throws(() => canParse(), 'TypeError: Not enough arguments'); + assert.throws(() => canParse({ toString() + { + throw Error('conversion thrower #1'); + } }), 'conversion thrower #1'); + assert.throws(() => canParse('q:w', { toString() + { + throw Error('conversion thrower #2'); + } }), 'conversion thrower #2'); +}); diff --git a/tests/js/util-module.simple b/tests/js/util-module.simple new file mode 100644 index 00000000..db37d5af --- /dev/null +++ b/tests/js/util-module.simple @@ -0,0 +1,20 @@ +/** + * @file util-module.simple + * Simple test for the builtin util module + * @author Tom Tang + * @date March 2024 + */ + +const util = require('util'); + +// https://github.com/Distributive-Network/PythonMonkey/pull/300 +const err = new TypeError(); +if (err.propertyIsEnumerable('stack')) + throw new Error('The stack property should not be enumerable.'); +err.anything = 123; +err.stack = 'abc'; +if (!err.propertyIsEnumerable('stack')) + throw new Error('In SpiderMonkey, the stack property should be enumerable after changing it.'); +const output = util.format(err); +if (output.match(/abc/g).length !== 1) // should only be printed once + throw new Error('The stack property should not be printed along with other enumerable properties'); diff --git a/tests/js/xhr-http-keep-alive.bash b/tests/js/xhr-http-keep-alive.bash new file mode 100644 index 00000000..3ac6a421 --- /dev/null +++ b/tests/js/xhr-http-keep-alive.bash @@ -0,0 +1,72 @@ +#! /bin/bash +# +# @file xhr-http-keep-alive.bash +# For testing HTTP-Keep-Alive automatically in the CI. +# +# We use `strace` to track system calls within the process that open a TCP connection. +# If HTTP-Keep-Alive is working, the total number of TCP connections opened should be 1 for a single remote host. +# +# @author Tom Tang (xmader@distributive.network) +# @date May 2024 + +set -u +set -o pipefail + +panic() +{ + echo "FAIL: $*" >&2 + exit 2 +} + +cd `dirname "$0"` || panic "could not change to test directory" + +if [[ "$OSTYPE" != "linux-gnu"* ]]; then + exit 0 + # Skip non-Linux for this test + # TODO: add tests on Windows and macOS. What's the equivalence of `strace`? +fi + +code=' +function newRequest(url) { + return new Promise(function (resolve, reject) + { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = function () + { + if (this.status >= 200 && this.status < 300) resolve(this.response); + else reject(new Error(this.status)); + }; + xhr.onerror = (ev) => reject(ev.error); + xhr.send(); + }); +} + +async function main() { + await newRequest("http://www.example.org/"); + await newRequest("http://www.example.org/"); + await newRequest("http://http.badssl.com/"); +} + +main(); +' + +# Trace the `socket` system call https://man.archlinux.org/man/socket.2 +# AF_INET: IPv4, IPPROTO_TCP: TCP connection +TRACE=$(strace -f -e socket \ + "${PMJS:-pmjs}" -e "$code" \ + < /dev/null 2>&1 +) + +# We have 3 HTTP requests, but we should only have 2 TCP connections open, +# as HTTP-Keep-Alive reuses the socket for a single remote host. +echo "$TRACE" \ +| tr -d '\r' \ +| grep -c -E 'socket\(AF_INET, \w*(\|\w*)+, IPPROTO_TCP\)' \ +| while read qty + do + echo "$TRACE" + echo "TCP connections opened: $qty" + [ "$qty" != "2" ] && panic qty should not be $qty + break + done || exit $? diff --git a/tests/python/conftest.py b/tests/python/conftest.py index 56552c0a..4fceb789 100644 --- a/tests/python/conftest.py +++ b/tests/python/conftest.py @@ -3,11 +3,13 @@ import gc # This is run at the end of each test function + + @pytest.fixture(scope="function", autouse=True) def teardown_function(): - """ - Forcing garbage collection (twice) whenever a test function finishes, - to locate GC-related errors - """ - gc.collect(), pm.collect() - gc.collect(), pm.collect() + """ + Forcing garbage collection (twice) whenever a test function finishes, + to locate GC-related errors + """ + gc.collect(), pm.collect() + gc.collect(), pm.collect() diff --git a/tests/python/test_arrays.py b/tests/python/test_arrays.py new file mode 100644 index 00000000..c91b95e5 --- /dev/null +++ b/tests/python/test_arrays.py @@ -0,0 +1,2652 @@ +import pythonmonkey as pm +from datetime import datetime + + +def test_assign(): + items = [1, 2, 3] + pm.eval("(arr) => {arr[0] = 42}")(items) + assert items[0] == 42 + + +def test_get(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr[1]}")(result, items) + assert result[0] == 2 + + +def test_get_length(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.length}")(result, items) + assert result[0] == 3 + + +def test_missing_func(): + items = [1, 2, 3] + try: + pm.eval("(arr) => {arr.after()}")(items) + assert False + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__('TypeError: arr.after is not a function') + +# reverse + + +def test_reverse(): + items = [1, 2, 3] + pm.eval("(arr) => {arr.reverse()}")(items) + assert items == [3, 2, 1] + + +def test_reverse_size_one(): + items = [1] + pm.eval("(arr) => {arr.reverse()}")(items) + assert items == [1] + + +def test_reverse_size_zero(): + items = [] + pm.eval("(arr) => {arr.reverse()}")(items) + assert items == [] + + +def test_reverse_returns_reference(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.reverse(); result[0][0] = 4}")(result, items) + assert result[0] == [4, 2, 1] + assert items == [4, 2, 1] + + +def test_reverse_ignores_extra_args(): + items = [1, 2, 3] + pm.eval("(arr) => {arr.reverse(9)}")(items) + assert items == [3, 2, 1] + +# pop + + +def test_pop(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.pop()}")(result, items) + assert items == [1, 2] + assert result[0] == 3 + + +def test_pop_empty(): + items = [] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.pop()}")(result, items) + assert items == [] + assert result[0] is None + + +def test_pop_ignore_extra_args(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.pop(1)}")(result, items) + assert items == [1, 2] + assert result[0] == 3 + +# join + + +def test_join_no_arg(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, items) + assert result[0] == '1,2,3' + + +def test_join_empty_array(): + items = [] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, items) + assert result[0] == '' + + +def test_join_no_arg_diff_types(): + items = [1, False, "END"] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, items) + assert result[0] == '1,false,END' + + +def test_join_no_arg_with_embedded_list_type(): + items = [1, [2, 3], "END"] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, items) + assert result[0] == '1,2,3,END' + + +def test_join_with_sep(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join('-')}")(result, items) + assert result[0] == '1-2-3' + + +def test_join_none(): + items = [None, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, items) + assert result[0] == ',2,3' + + +def test_join_null(): + items = [pm.null, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, items) + assert result[0] == ',2,3' + + +def test_join_utf8(): + prices = ["¥7", 500, 8123, 12] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.join()}")(result, prices) + assert result[0] == '¥7,500,8123,12' + +# toString + + +def test_toString(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.toString()}")(result, items) + assert result[0] == '1,2,3' + +# push + + +def test_push(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.push(4)}")(result, items) + assert items == [1, 2, 3, 4] + assert result[0] == 4 + + +def test_push_no_arg(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.push()}")(result, items) + assert items == [1, 2, 3,] + assert result[0] == 3 + + +def test_push_two_args(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.push(4,false)}")(result, items) + assert items == [1, 2, 3, 4, False] + assert result[0] == 5 + + +def test_push_list(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.push([4,5])}")(result, items) + assert items == [1, 2, 3, [4, 5]] + assert result[0] == 4 + +# shift + + +def test_shift(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.shift()}")(result, items) + assert items == [2, 3] + assert result[0] == 1 + + +def test_shift_empty(): + items = [] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.shift()}")(result, items) + assert items == [] + assert result[0] is None + +# unshift + + +def test_unshift_zero_arg(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.unshift()}")(result, items) + assert items == [1, 2, 3] + assert result[0] == 3 + + +def test_unshift_one_arg(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.unshift(6)}")(result, items) + assert items == [6, 1, 2, 3] + assert result[0] == 4 + + +def test_unshift_two_args(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.unshift(6,7)}")(result, items) + assert items == [6, 7, 1, 2, 3] + assert result[0] == 5 + +# concat + + +def test_concat_primitive(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.concat(4)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2, 3, 4] + + +def test_concat_array(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.concat([4,5])}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2, 3, 4, 5] + + +def test_concat_empty_arg(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.concat()}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2, 3] + assert items is not result[0] + + +def test_concat_two_arrays(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.concat([7,8], [0,1])}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2, 3, 7, 8, 0, 1] + + +def test_concat_mix(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.concat([7,8], true, [0,1])}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2, 3, 7, 8, True, 0, 1] + + +def test_concat_object_element(): + d = {"a": 1} + items = [1, 2, d] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.concat()}")(result, items) + assert items == [1, 2, d] + assert result[0] == [1, 2, d] + assert items is not result[0] + assert d is items[2] + assert d is result[0][2] + +# slice + + +def test_slice(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.slice(1,2)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [2] + + +def test_slice_copy(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.slice(0,3)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2, 3] + assert items is not result[0] + + +def test_slice_start_zero(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.slice(0,2)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [1, 2] + + +def test_slice_stop_length(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.slice(1,3)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [2, 3] + + +def test_slice_stop_beyond_length(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.slice(1,4)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [2, 3] + + +def test_slice_start_negative(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.slice(-3,-1)}")(result, items) + assert result[0] == [1, 2] + +# indexOf + + +def test_indexOf(): + items = [1, 2, 3, 1] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(1)}")(result, items) + assert result[0] == 0 + + +def test_indexOf_with_start(): + items = [1, 2, 3, 4, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(3, 3)}")(result, items) + assert result[0] == 4 + + +def test_indexOf_with_negative_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(3, -2)}")(result, items) + assert result[0] == 2 + + +def test_indexOf_zero_size(): + items = [] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(1)}")(result, items) + assert result[0] == -1 + + +def test_indexOf_start_beyond_length(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(1, 10)}")(result, items) + assert result[0] == -1 + + +def test_indexOf_not_found(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(10)}")(result, items) + assert result[0] == -1 + + +def test_indexOf_small_start(): + items = [1, 2, 3, 1] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.indexOf(1, -10)}")(result, items) + assert result[0] == 0 + +# lastIndexOf + + +def test_lastIndexOf(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(1)}")(result, items) + assert result[0] == 0 + + +def test_lastIndexOf_dup(): + items = [1, 2, 3, 1] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(1)}")(result, items) + assert result[0] == 3 + + +def test_lastIndexOf_with_from_index(): + items = [1, 2, 3, 1] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(1, 2)}")(result, items) + assert result[0] == 0 + + +def test_lastIndexOf_with_from_index_greater_than_size(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(1, 10)}")(result, items) + assert result[0] == 0 + + +def test_lastIndexOf_with_negative_from_index(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(1, -2)}")(result, items) + assert result[0] == 0 + + +def test_lastIndexOf_not_found(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(3, 0)}")(result, items) + assert result[0] == -1 + + +def test_lastIndexOf_small_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.lastIndexOf(1, -10)}")(result, items) + assert result[0] == -1 + +# splice + + +def test_splice_no_args(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice()}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [] + + +def test_splice_one_arg(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1)}")(result, items) + assert items == [1] + assert result[0] == [2, 3] + + +def test_splice_one_arg_negative(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(-2)}")(result, items) + assert items == [1] + assert result[0] == [2, 3] + + +def test_splice_two_args_negative_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1, -1)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [] + + +def test_splice_two_args_zero_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1, 0)}")(result, items) + assert items == [1, 2, 3] + assert result[0] == [] + + +def test_splice_two_args_one_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1, 1)}")(result, items) + assert items == [1, 3] + assert result[0] == [2] + + +def test_splice_two_args_two_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1, 2)}")(result, items) + assert items == [1] + assert result[0] == [2, 3] + + +def test_splice_three_args_zero_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1,0,5)}")(result, items) + assert items == [1, 5, 2, 3] + assert result[0] == [] + + +def test_splice_three_args_one_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1,1,5)}")(result, items) + assert items == [1, 5, 3] + assert result[0] == [2] + + +def test_splice_three_args_two_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1,2,5)}")(result, items) + assert items == [1, 5] + assert result[0] == [2, 3] + + +def test_splice_four_args_zero_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1,0,5,6)}")(result, items) + assert items == [1, 5, 6, 2, 3] + assert result[0] == [] + + +def test_splice_four_args_one_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1,1,5,6)}")(result, items) + assert items == [1, 5, 6, 3] + assert result[0] == [2] + + +def test_splice_four_args_two_count(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.splice(1,2,5,6)}")(result, items) + assert items == [1, 5, 6] + assert result[0] == [2, 3] + +# fill + + +def test_fill_returns_ref_to_self(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8)}")(result, items) + assert items == [8, 8, 8] + assert items is result[0] + + +def test_fill_other_type(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(false)}")(result, items) + assert items == [False, False, False] + + +def test_fill_with_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,1)}")(result, items) + assert items == [1, 8, 8] + + +def test_fill_with_start_negative(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,-2)}")(result, items) + assert items == [1, 8, 8] + + +def test_fill_with_start_too_high(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,7)}")(result, items) + assert items == [1, 2, 3] + + +def test_fill_with_stop_smaller_than_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,7,2)}")(result, items) + assert items == [1, 2, 3] + + +def test_fill_with_stop(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,1,2)}")(result, items) + assert items == [1, 8, 3] + + +def test_fill_with_negative_stop(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,1,-1)}")(result, items) + assert items == [1, 8, 3] + + +def test_fill_with_negative_stop_too_low(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,1,-10)}")(result, items) + assert items == [1, 2, 3] + + +def test_fill_with_stop_too_high(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.fill(8,1,10)}")(result, items) + assert items == [1, 8, 8] + + +def test_fill_object(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {let a = {a:1}; result[0] = arr.fill(a)}")(result, items) + assert items == [{"a": 1}, {"a": 1}, {"a": 1}] + assert items is result[0] + assert items[0] is items[1] is items[2] + +# copyWithin + + +def test_copyWithin(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(0,1)}")(result, items) + assert items == [2, 3, 3] + assert items is result[0] + + +def test_copyWithin_no_args(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin()}")(result, items) + assert items == [1, 2, 3] + + +def test_copyWithin_target_only_overwrite_all(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(0)}")(result, items) + assert items == [1, 2, 3] + + +def test_copyWithin_target_only(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1)}")(result, items) + assert items == [1, 1, 2] + + +def test_copyWithin_negative_target_only(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(-1)}")(result, items) + assert items == [1, 2, 1] + + +def test_copyWithin_target_too_large(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(10)}")(result, items) + assert items == [1, 2, 3] + + +def test_copyWithin_target_and_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1, 2)}")(result, items) + assert items == [1, 3, 3] + + +def test_copyWithin_target_and_start_too_large(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1, 10)}")(result, items) + assert items == [1, 2, 3] + + +def test_copyWithin_target_and_negative_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1, -1)}")(result, items) + assert items == [1, 3, 3] + + +def test_copyWithin_target_and_start_and_end(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1,2,3)}")(result, items) + assert items == [1, 3, 3, 4, 5] + + +def test_copyWithin_target_and_start_and_negative_end(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1,2,-2)}")(result, items) + assert items == [1, 3, 3, 4, 5] + + +def test_copyWithin_target_too_small_and_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(-10,2)}")(result, items) + assert items == [3, 2, 3] + + +def test_copyWithin_target_greater_than_start(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => { result[0] = arr.copyWithin(2,1)}")(result, items) + assert items == [1, 2, 2] + + +def test_copyWithin_target_and_start_too_small(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1, -10)}")(result, items) + assert items == [1, 1, 2] + + +def test_copyWithin_target_and_start_and_end_too_small(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(1,2,-10)}")(result, items) + assert items == [1, 2, 3, 4, 5] + + +def test_copyWithin_target_and_start_and_end_too_large(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(2,1,10)}")(result, items) + assert items == [1, 2, 2, 3, 4] + + +def test_copyWithin_target_and_start_greater_than_end(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.copyWithin(2,3,2)}")(result, items) + assert items == [1, 2, 3, 4, 5] + +# includes + + +def test_includes(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes(1)}")(result, items) + assert result[0] + + +def test_includes_start_index(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes(1, 1)}")(result, items) + assert not result[0] + + +def test_includes_start_index_negative(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes(1, -1)}")(result, items) + assert not result[0] + + +def test_includes_start_index_negative_large(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes(1, -10)}")(result, items) + assert result[0] + + +def test_includes_start_index_large(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes(1, 10)}")(result, items) + assert not result[0] + + +def test_includes_other_type(): + items = [1, 2, 'Hi'] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes('Hi')}")(result, items) + assert result[0] + + +def test_includes_not(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes(5)}")(result, items) + assert not result[0] + + +def test_includes_not_other_type(): + items = [1, 2, 'Hi'] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.includes('Joe')}")(result, items) + assert not result[0] + + +def test_includes_too_few_args(): + items = [4, 2, 6, 7] + try: + pm.eval("(arr) => {arr.includes()}")(items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: includes: At least 1 argument required, but only 0 passed") + +# sort + + +def test_sort_empty(): + items = [] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.sort()}")(result, items) + assert result[0] is items + assert items == [] + + +def test_sort_one(): + items = [5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.sort()}")(result, items) + assert result[0] is items + assert items == [5] + + +def test_sort_numbers(): + items = [4, 2, 6, 7] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.sort()}")(result, items) + assert result[0] is items + assert items == [2, 4, 6, 7] + + +def test_sort_strings(): + items = ['Four', 'Three', 'One'] + pm.eval("(arr) => {arr.sort()}")(items) + assert items == ['Four', 'One', 'Three'] + + +def test_sort_with_two_args_keyfunc(): + items = ['Four', 'Three', 'One'] + + def myFunc(e, f): + return len(e) - len(f) + pm.eval("(arr, compareFun) => {arr.sort(compareFun)}")(items, myFunc) + assert items == ['One', 'Four', 'Three'] + + +def test_sort_with_two_args_keyfunc_wrong_return_type(): + items = ['Four', 'Three', 'One'] + + def myFunc(e, f): + return e + f + try: + pm.eval("(arr, compareFun) => {arr.sort(compareFun)}")(items, myFunc) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "incorrect compare function return type" + + +def test_sort_with_two_args_keyfunc_wrong_data_type(): + items = [4, 2, 6, 7] + + def myFunc(e, f): + return len(e) - len(f) + try: + pm.eval("(arr, compareFun) => {arr.sort(compareFun)}")(items, myFunc) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert "object of type 'float' has no len()" in str(e) + assert "test_arrays.py" in str(e) + assert "line 883" in str(e) + assert "in myFunc" in str(e) + assert "JS Stack Trace" in str(e) + assert "@evaluate:1:27" in str(e) + + +def test_sort_with_js_func(): + items = ['Four', 'Three', 'One'] + result = [None] + myFunc = pm.eval("((a, b) => a.toLocaleUpperCase() < b.toLocaleUpperCase() ? -1 : 1)") + pm.eval("(result, arr, compareFun) => {result[0] = arr.sort(compareFun)}")(result, items, myFunc) + assert result[0] is items + assert items == ['Four', 'One', 'Three'] + + +def test_sort_numbers_tricky(): + items = [1, 30, 4, 21, 100000] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.sort()}")(result, items) + assert result[0] is items + assert items == [1, 100000, 21, 30, 4] + + +def test_sort_with_js_func_wrong_data_type(): + items = [4, 2, 6, 7] + myFunc = pm.eval("((a, b) => a.toLocaleUpperCase() < b.toLocaleUpperCase() ? -1 : 1)") + try: + pm.eval("(arr, compareFun) => {arr.sort(compareFun)}")(items, myFunc) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: a.toLocaleUpperCase is not a function") + +# forEach + + +def test_forEach(): + items = ['Four', 'Three', 'One'] + result = [''] + returnResult = [0] + pm.eval(""" + (returnResult, result, arr) => { + returnResult[0] = arr.forEach((element) => result[0] += element); + } + """)(returnResult, result, items) + assert items == ['Four', 'Three', 'One'] + assert result == ['FourThreeOne'] + assert returnResult == [None] + + +def test_forEach_check_index(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.forEach((element, index) => result[0] += index)}")(result, items) + assert result == ['012'] + + +def test_forEach_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.forEach((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_forEach_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.forEach(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_forEach_check_this_arg_wrong_type(): + items = ['Four', 'Three', 'One'] + result = [None] + a = 9 + try: + pm.eval(""" + (result, arr, a) => { + class Counter { + constructor() { + this.count = 0; + } + add(array) { + array.forEach(function countEntry(entry) { ++this.count; }, a); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """)(result, items, a) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: 'this' argument is not an object or null") + +# TODO python function support + + +def test_forEach_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.forEach(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [None] + + +def test_forEach_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.forEach(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_forEach_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.forEach(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_forEach_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++ + } + let jsObj = {count: 0} + arr.forEach(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# TODO should not pass + + +def test_forEach_check_this_arg_null(): + items = ['Four', 'Three', 'One'] + result = [None] + try: + pm.eval(""" + (result, arr) => { + class Counter { + constructor() { + this.count = 0; + } + add(array) { + array.forEach(function countEntry(entry) {++this.count; }, null); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """)(result, items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert "TypeError:" in str(e) + assert "this is null" in str(e) + + +def test_forEach_too_few_args(): + items = [4, 2, 6, 7] + try: + pm.eval("(arr) => {arr.forEach()}")(items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: forEach: At least 1 argument required, but only 0 passed") + +# map + + +def test_map(): + items = [4, 2, 6, 7] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.map((x) => x * x)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == [16, 4, 36, 49] + + +def test_map_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.map((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0123' + + +def test_map_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.map((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_map_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.map(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_map_too_few_args(): + items = [4, 2, 6, 7] + try: + pm.eval("(arr) => {arr.map()}")(items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: map: At least 1 argument required, but only 0 passed") + + +def test_map_arg_wrong_type(): + items = [4, 2, 6, 7] + try: + pm.eval("(arr) => {arr.map(8)}")(items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: map: callback is not a function") + + +def test_map_check_array_mutation(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.map((element, index, array) => {array[0] = 'Ten'; result[0] = array})}")(result, items) + assert result[0] == ['Ten', 'Three', 'One'] + assert items == ['Ten', 'Three', 'One'] + + +def test_map_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + return 42 + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.map(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [[42.0, 42.0, 42.0]] + + +def test_map_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.map(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_map_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.map(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_map_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++ + } + let jsObj = {count: 0} + arr.map(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# filter + + +def test_filter(): + words = ['spray', 'elite', 'exuberant', 'destruction', 'present'] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.filter((word) => word.length > 6)}")(result, words) + assert words == ['spray', 'elite', 'exuberant', 'destruction', 'present'] + assert result[0] == ['exuberant', 'destruction', 'present'] + + +def test_filter_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.filter((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0123' + + +def test_filter_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.filter((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_filter_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.filter(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_filter_too_few_args(): + items = [4, 2, 6, 7] + try: + pm.eval("(arr) => {arr.filter()}")(items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: filter: At least 1 argument required, but only 0 passed") + + +def test_filter_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.filter(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [[]] + + +def test_filter_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.filter(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_filter_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.filter(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_filter_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++ + } + let jsObj = {count: 0} + arr.filter(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# reduce + + +def test_reduce(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + } + """)(result, items) + assert items == [1, 2, 3, 4, 5] + assert result[0] == 15 + + +def test_reduce_empty_array_no_accumulator(): + items = [] + try: + pm.eval("(arr) => {arr.reduce((accumulator, currentValue) => accumulator + currentValue)}")(items) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("TypeError: reduce of empty array with no initial value") + + +def test_reduce_float(): + items = [1.9, 4.6, 9.3, 16.5] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + } + """)(result, items) + assert result[0] == 32.3 + + +def test_reduce_string(): + items = ['Hi', 'There'] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue, ""); + } + """)(result, items) + assert result[0] == 'HiThere' + + +def test_reduce_initial_value_not_zero(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 5); + } + """)(result, items) + assert items == [1, 2, 3, 4, 5] + assert result[0] == 20 + + +def test_reduce_no_initial_value(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue); + } + """)(result, items) + assert items == [1, 2, 3, 4, 5] + assert result[0] == 15 + + +def test_reduce_length_one_with_initial_value(): + items = [1] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 2); + } + """)(result, items) + assert result[0] == 3 + + +def test_reduce_length_one_no_initial_value(): + items = [1] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduce((accumulator, currentValue) => accumulator + currentValue); + } + """)(result, items) + assert result[0] == 1 + + +def test_reduce_list_meaningless(): + items = [['Hi', 'There']] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.reduce((x) => x * 2)}")(result, items) + assert result[0] == ['Hi', 'There'] + +# reduceRight + + +def test_reduceRight_list_concat(): + items = [[0, 1], [2, 3], [4, 5]] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduceRight((accumulator, currentValue) => accumulator.concat(currentValue)); + } + """)(result, items) + assert result[0] == [4, 5, 2, 3, 0, 1] + + +def test_reduceRight_list_concat_with_initial_value(): + items = [[0, 1], [2, 3], [4, 5]] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduceRight((accumulator, currentValue) => accumulator.concat(currentValue), [7,8]); + } + """)(result, items) + assert result[0] == [7, 8, 4, 5, 2, 3, 0, 1] + + +def test_reduceRight(): + items = [0, 1, 2, 3, 4] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduceRight((accumulator, currentValue, index, array) => accumulator + currentValue); + } + """)(result, items) + assert result[0] == 10 + + +def test_reduceRight_with_initial_value(): + items = [0, 1, 2, 3, 4] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduceRight((accumulator, currentValue, index, array) => accumulator + currentValue, 5); + } + """)(result, items) + assert result[0] == 15 + + +def test_reduceRight_float(): + items = [1.9, 4.6, 9.3, 16.5] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.reduceRight((accumulator, currentValue, index, array) => accumulator + currentValue); + } + """)(result, items) + assert result[0] == 32.3 + + +def test_reduceRight_check_index(): + items = [1.9, 4.6, 9.3, 16.5] + result = [''] + pm.eval(""" + (result, arr) => { + arr.reduceRight((accumulator, currentValue, index, array) => { + accumulator + currentValue; + result[0] += index + }); + } + """)(result, items) + assert result[0] == '210' + + +def test_reduceRight_check_array(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval(""" + (result, arr) => { + arr.reduceRight((accumulator, currentValue, index, array) => { + accumulator + currentValue; + result[0] = array; + }); + } + """)(result, items) + assert result == [items] + assert result[0] is items + +# some + + +def test_some_true(): + items = [1, 2, 3, 4, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.some((element) => element % 2 === 0)}")(result, items) + assert items == [1, 2, 3, 4, 5] + assert result[0] + + +def test_some_false(): + items = [1, 3, 5] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.some((element) => element % 2 === 0)}")(result, items) + assert not result[0] + + +def test_some_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.some((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0123' + + +def test_some_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.some((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_some_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.some(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_some_truthy_conversion(): + result = [None] + pm.eval( + """ + (result) => { + const TRUTHY_VALUES = [true, "true", 1, {}]; + function getBoolean(value) { + if (typeof value === "string") { + value = value.toLowerCase().trim(); + } + return TRUTHY_VALUES.some((t) => t === value); + } + result[0] = getBoolean(1); + } + """)(result) + assert result[0] + + +def test_some_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + return False + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.some(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [False] + + +def test_some_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + return False + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.some(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_some_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + return False + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.some(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_some_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++; + return false; + } + let jsObj = {count: 0} + arr.some(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# every + + +def test_every_true(): + items = [2, 4, 6] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.every((element) => element % 2 === 0)}")(result, items) + assert items == [2, 4, 6] + assert result[0] + + +def test_every_false(): + items = [1, 2, 4, 6] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.every((element) => element % 2 === 0)}")(result, items) + assert not result[0] + + +def test_every_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.every((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0' + + +def test_every_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.every((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_every_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.every(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [1] + + +def test_every_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + return True + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.every(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [True] + + +def test_every_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + return True + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.every(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_every_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.every(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_every_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++ + return true; + } + let jsObj = {count: 0} + arr.every(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# find + + +def test_find_found_once(): + items = [5, 12, 8, 130, 44] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.find((element) => element > 100)}")(result, items) + assert items == [5, 12, 8, 130, 44] + assert result[0] == 130 + + +def test_find_found_twice(): + items = [5, 12, 8, 130, 4] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.find((element) => element > 10)}")(result, items) + assert result[0] == 12 + + +def test_find_not_found(): + items = [5, 12, 8, 130, 44] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.find((element) => element > 1000)}")(result, items) + assert result[0] is None + + +def test_find_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.find((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0123' + + +def test_find_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.find((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_find_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.find(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_find_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + return False + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.find(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [None] + + +def test_find_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + return False + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.find(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_find_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + return False + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.find(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_find_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++; + return false; + } + let jsObj = {count: 0} + arr.find(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# findIndex + + +def test_findIndex_found_once(): + items = [5, 12, 8, 130, 44] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.findIndex((element) => element > 100)}")(result, items) + assert items == [5, 12, 8, 130, 44] + assert result[0] == 3 + + +def test_findIndex_found_twice(): + items = [5, 12, 8, 130, 4] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.findIndex((element) => element > 10)}")(result, items) + assert result[0] == 1 + + +def test_findIndex_not_found(): + items = [5, 12, 8, 130, 4] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.findIndex((element) => element > 1000)}")(result, items) + assert result[0] == -1 + + +def test_findIndex_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.findIndex((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0123' + + +def test_findIndex_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.findIndex((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_findIndex_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.findIndex(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_findIndex_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.findIndex(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [-1] + + +def test_findIndex_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.findIndex(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_findIndex_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.findIndex(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_findIndex_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++ + } + let jsObj = {count: 0} + arr.findIndex(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# flat + + +def test_flat(): + items = [0, 1, 2, [3, 4]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flat()}")(result, items) + assert items == [0, 1, 2, [3, 4]] + assert result[0] == [0, 1, 2, 3, 4] + + +def test_flat_with_js_array(): + items = [0, 1, 2, [3, 4]] + result = [0] + pm.eval("(result, arr) => {arr[1] = [10,11]; result[0] = arr.flat()}")(result, items) + assert items == [0, [10, 11], 2, [3, 4]] + assert result[0] == [0, 10, 11, 2, 3, 4] + + +def test_flat_depth_zero(): + items = [0, 1, [2, [3, [4, 5]]]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flat(0)}")(result, items) + assert result[0] == [0, 1, [2, [3, [4, 5]]]] + + +def test_flat_depth_one(): + items = [0, 1, [2, [3, [4, 5]]]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flat(1)}")(result, items) + assert items == [0, 1, [2, [3, [4, 5]]]] + assert result[0] == [0, 1, 2, [3, [4, 5]]] + + +def test_flat_depth_two(): + items = [0, 1, [2, [3, [4, 5]]]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flat(2)}")(result, items) + assert items == [0, 1, [2, [3, [4, 5]]]] + assert result[0] == [0, 1, 2, 3, [4, 5]] + + +def test_flat_depth_large(): + items = [0, 1, [2, [3, [4, 5]]]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flat(10)}")(result, items) + assert result[0] == [0, 1, 2, 3, 4, 5] + +# flatMap + + +def test_flatMap(): + items = [1, 2, 1] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flatMap((num) => (num === 2 ? [2, 2] : 1))}")(result, items) + assert items == [1, 2, 1] + assert result[0] == [1, 2, 2, 1] + + +def test_flatMap_with_js_array(): + items = [1, 2, 2, 1] + result = [0] + pm.eval("(result, arr) => {arr[1] = [10,11]; result[0] = arr.flatMap((num) => (num === 2 ? [2, 2] : 1))}")( + result, items) + assert items == [1, [10, 11], 2, 1] + assert result[0] == [1, 1, 2, 2, 1] + + +def test_flatMap_no_replace(): + items = [1, 2, [4, 5]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flatMap((num) => (num === 2 ? [2, 2] : 1))}")(result, items) + assert items == [1, 2, [4, 5]] + assert result[0] == [1, 2, 2, 1] + + +def test_flatMap_no_replace_depth_one(): + items = [1, 2, [4, 5]] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.flatMap((num) => (num === 2 ? [2, [2, 2]] : 1))}")(result, items) + assert items == [1, 2, [4, 5]] + assert result[0] == [1, 2, [2, 2], 1] + + +def test_flatMap_equivalence(): + items = [1, 2, 1] + result = [0] + result2 = [0] + pm.eval("(result, arr) => {result[0] = arr.flatMap((num) => (num === 2 ? [2, 2] : 1))}")(result, items) + pm.eval("(result, arr) => {result[0] = arr.map((num) => (num === 2 ? [2, 2] : 1)).flat()}")(result2, items) + assert result[0] == result2[0] + + +def test_flatMap_check_index(): + items = [4, 2, 6, 7] + result = [''] + pm.eval("(result, arr) => {arr.flatMap((x, index) => result[0] += index)}")(result, items) + assert items == [4, 2, 6, 7] + assert result[0] == '0123' + + +def test_flatMap_check_array(): + items = ['Four', 'Three', 'One'] + result = [''] + pm.eval("(result, arr) => {arr.flatMap((element, index, array) => result[0] = array)}")(result, items) + assert result == [items] + assert result[0] is items + + +def test_flatMap_check_this_arg(): + items = ['Four', 'Three', 'One'] + result = [None] + pm.eval( + """ + (result, arr) => { + class Counter { + constructor() + { + this.count = 0; + } + add(array) { + array.flatMap(function countEntry(entry) { ++this.count; }, this); + } + } + const obj = new Counter(); + obj.add(arr); + result[0] = obj.count; + } + """ + )(result, items) + assert result == [3] + + +def test_flatMap_with_python_function(): + def func(element, index, array): + array[int(index)] = "to each his own" + return 42 + items = ['Four', 'Three', 'One'] + returnResult = [0] + pm.eval("(returnResult, arr, func) => {returnResult[0] = arr.flatMap(func)}")(returnResult, items, func) + assert items == ['to each his own', 'to each his own', 'to each his own'] + assert returnResult == [[42.0, 42.0, 42.0]] + + +def test_flatMap_self_pymethod(): + items = ['Four', 'Three', 'One'] + + class Counter: + def __init__(self): + self.count = 0 + + def increment(self, element, index, array): + self.count += 1 + + obj = Counter() + assert obj.count == 0 + result = pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.flatMap(increment, jsObj); + return jsObj.count; + } + """)(items, obj.increment) + assert obj.count == 0 + assert result == 3 + + +def test_flatMap_self_pyfunction(): + items = ['Four', 'Three', 'One'] + + def increment(self, element, index, array): + self.count += 1 + + try: + pm.eval(""" + (arr, increment, result) => { + let jsObj = {count: 0} + arr.flatMap(increment, jsObj); + return jsObj.count; + } + """)(items, increment) + assert False + except Exception as e: + assert type(e) is TypeError + assert str(e).__contains__("unbound python functions do not have a 'self' to bind") + + +def test_flatMap_self_jsfunction(): + items = ['Four', 'Three', 'One'] + + result = pm.eval(""" + (arr) => { + function increment(element, index, array) { + this.count++ + } + let jsObj = {count: 0} + arr.flatMap(increment, jsObj); + return jsObj.count; + } + """)(items) + assert result == 3 + +# valueOf + + +def test_valueOf(): + items = [1, 2, 1] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.valueOf()}")(result, items) + assert items == [1, 2, 1] + assert result[0] is items + +# toLocaleString + + +def test_toLocaleString(): + prices = ["¥7", 500, 8123, 12] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY' }); + } + """)(result, prices) + assert result[0] == '¥7,¥500,¥8,123,¥12' + + +def test_toLocaleString_with_none(): + prices = ["¥7", 500, 8123, None] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY' }); + } + """)(result, prices) + assert result[0] == '¥7,¥500,¥8,123,' + + +def test_toLocaleString_with_null(): + prices = ["¥7", 500, 8123, pm.null] + result = [None] + pm.eval(""" + (result, arr) => { + result[0] = arr.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY' }); + } + """)(result, prices) + assert result[0] == '¥7,¥500,¥8,123,' + + +def test_toLocaleString_no_args(): + prices = ["¥7", 500, 8123, 12] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.toLocaleString()}")(result, prices) + assert result[0] == '¥7,500,8,123,12' + + +def test_toLocaleString_one_arg_(): + prices = ["¥7", 500, 8123, 12] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.toLocaleString('ja-JP')}")(result, prices) + assert result[0] == '¥7,500,8,123,12' + + +def test_toLocaleString_one_arg_invalid_locale(): + prices = ["¥7", 500, 8123, 12] + result = [None] + try: + pm.eval("(result, arr) => {result[0] = arr.toLocaleString('asfasfsafsdf')}")(result, prices) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("RangeError: invalid language tag:") + + +def test_toLocaleString_two_args_invalid_currency(): + prices = ["¥7", 500, 8123, 12] + result = [None] + try: + pm.eval(""" + (result, arr) => { + result[0] = arr.toLocaleString('ja-JP', { style: 'currency', currency: 'JPYsdagasfgas' }); + } + """)(result, prices) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__("RangeError: invalid currency code in NumberFormat():") + + +def test_toLocaleString_with_datetime(): + prices = [500, datetime(year=2020, month=1, day=31, hour=13, minute=14, second=31)] + result = [None] + pm.eval("(result, arr) => {result[0] = arr.toLocaleString('en-uk')}")(result, prices) + assert result[0] == '500,31/01/2020, 13:14:31' + +# entries + + +def test_entries_next(): + items = ['a', 'b', 'c'] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.entries(); result[0] = result[0].next().value}")(result, items) + assert items == ['a', 'b', 'c'] + assert result[0] == [0, 'a'] + + +def test_entries_next_next(): + items = ['a', 'b', 'c'] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.entries(); result[0].next(); result[0] = result[0].next().value}")( + result, items) + assert result[0] == [1, 'b'] + + +def test_entries_next_next_undefined(): + items = ['a'] + result = [0] + pm.eval("(result, arr) => {result[0] = arr.entries(); result[0].next(); result[0] = result[0].next().value}")( + result, items) + assert result[0] is None + +# keys + + +def test_keys_iterator(): + items = ['a', 'b', 'c'] + result = [7, 8, 9] + pm.eval(""" + (result, arr) => { + index = 0; + iterator = arr.keys(); + for (const key of iterator) { + result[index] = key; + index++; + } + } + """)(result, items) + assert result == [0, 1, 2] + +# values + + +def test_values_iterator(): + items = ['a', 'b', 'c'] + result = [7, 8, 9] + pm.eval(""" + (result, arr) => { + index = 0; + iterator = arr.values(); + for (const value of iterator) { + result[index] = value; + index++; + } + } + """)(result, items) + assert result == ['a', 'b', 'c'] + assert result is not items + +# constructor property + + +def test_constructor_creates_array(): + items = [1, 2] + result = [0] + pm.eval("(result, arr) => { result[0] = arr.constructor; result[0] = new result[0]; result[0][0] = 9}")(result, items) + assert result[0] == [9] + +# length property + + +def test_constructor_creates_array(): + items = [1, 2] + result = [0] + pm.eval("(result, arr) => { result[0] = arr.length}")(result, items) + assert result[0] == 2 + +# iterator symbol property + + +def test_iterator_type_function(): + items = [1, 2] + result = [0] + pm.eval("(result, arr) => { result[0] = typeof arr[Symbol.iterator]}")(result, items) + assert result[0] == 'function' + + +def test_iterator_first_next(): + items = [1, 2] + result = [0] + pm.eval("(result, arr) => { result[0] = arr[Symbol.iterator]().next()}")(result, items) + assert result[0].value == 1 + assert not result[0].done + + +def test_iterator_second_next(): + items = [1, 2] + result = [0] + pm.eval("(result, arr) => { let iterator = arr[Symbol.iterator](); iterator.next(); result[0] = iterator.next()}")( + result, items) + assert result[0].value == 2 + assert not result[0].done + + +def test_iterator_last_next(): + items = [1, 2] + result = [0] + pm.eval(""" + (result, arr) => { + let iterator = arr[Symbol.iterator](); + iterator.next(); + iterator.next(); + result[0] = iterator.next(); + } + """)(result, items) + assert result[0].value is None + assert result[0].done + + +def test_iterator_iterator(): + items = [1, 2, 3, 4] + result = [0] + pm.eval("(result, arr) => {let iter = arr[Symbol.iterator](); let head = iter.next().value; result[0] = [...iter] }")( + result, items) + assert result[0] == [2, 3, 4] + +# Array.from + + +def test_array_from(): + items = [1, 2] + result = [0] + pm.eval("(result, arr) => { result[0] = Array.from(arr)}")(result, items) + assert result[0] == [1, 2] + assert result[0] is not items + +# bad index size expansion + + +def test_assign_bad_index(): + result = [] + pm.eval("(result) => {result[0] = 4}")(result) + assert result[0] == 4 + + +def test_assign_bad_index_with_existing_next(): + result = [8] + pm.eval("(result) => {result[1] = 4}")(result) + assert result == [8, 4] + + +def test_assign_bad_index_with_gap(): + result = [] + pm.eval("(result) => {result[0] = 4; result[5] = 6}")(result) + assert result == [4, None, None, None, None, 6] + + +def test_array_subclass_behaves_as_array(): + my_JS_function = pm.eval(""" + () => { + class MyClass extends Array { + constructor(...args) + { + super(...args); + return this; + } + } + return new MyClass(1,2); + } + """) + + a = my_JS_function() + assert a == [1, 2] + result = [] + for i in a: + result.append(i) + assert result == [1, 2] + assert a is not result + + +def test_iter_operator_tuple(): + myit = iter((1, 2)) + result = [None, None, None] + pm.eval('(result, myit) => { result[0] = myit.next(); result[1] = myit.next(); result[2] = myit.next()}')( + result, myit) + assert result[0] == {'done': False, 'value': 1.0} + assert result[1] == {'done': False, 'value': 2.0} + assert result[2] == {'done': True} + + +def test_iter_operator_array(): + myit = iter([1, 2, 3]) + result = [None, None, None, None] + pm.eval(""" + (result, myit) => { + result[0] = myit.next(); + result[1] = myit.next(); + result[2] = myit.next(); + result[3] = myit.next(); + } + """)(result, myit) + assert result[0] == {'done': False, 'value': 1.0} + assert result[1] == {'done': False, 'value': 2.0} + assert result[2] == {'done': False, 'value': 3.0} + assert result[3] == {'done': True} + + +def test_iter_reentrance(): + myit = iter((1, 2)) + result = pm.eval("(iter) => iter")(myit) + assert myit is result + + +def test_iter_reentrace_next(): + myit = iter((1, 2)) + result = [None] + pm.eval("(result, arr) => {result[0] = arr}")(result, myit) + next(result[0]) == 1 + next(result[0]) == 2 + try: + third = next(result[0]) + assert (False) + except StopIteration as e: + assert (True) + + +def test_iter_for_of(): + myit = iter((1, 2)) + result = [None, None] + pm.eval("""(result, myit) => {let index = 0; for (const value of myit) {result[index++] = value}}""")(result, myit) + assert result[0] == 1.0 + assert result[1] == 2.0 diff --git a/tests/python/test_bigints.py b/tests/python/test_bigints.py index f5b97c16..69c8c31e 100644 --- a/tests/python/test_bigints.py +++ b/tests/python/test_bigints.py @@ -2,138 +2,151 @@ import pythonmonkey as pm import random + def test_eval_numbers_bigints(): - def test_bigint(py_number: int): - js_number = pm.eval(f'{repr(py_number)}n') - assert py_number == js_number - - test_bigint(0) - test_bigint(1) - test_bigint(-1) - - # CPython would reuse the objects for small ints in range [-5, 256] - # Making sure we don't do any changes on them - def test_cached_int_object(py_number): - # type is still int - assert type(py_number) == int - assert type(py_number) != pm.bigint - test_bigint(py_number) - assert type(py_number) == int - assert type(py_number) != pm.bigint - # the value doesn't change - # TODO (Tom Tang): Find a way to create a NEW int object with the same value, because int literals also reuse the cached int objects - for _ in range(2): - test_cached_int_object(0) # _PyLong_FromByteArray reuses the int 0 object, - # see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L862 - for i in range(10): - test_cached_int_object(random.randint(-5, 256)) - - test_bigint(18014398509481984) # 2**54 - test_bigint(-18014398509481984) # -2**54 - test_bigint(18446744073709551615) # 2**64-1 - test_bigint(18446744073709551616) # 2**64 - test_bigint(-18446744073709551617) # -2**64-1 - - limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 - # = 2**300 + def test_bigint(py_number: int): + js_number = pm.eval(f'{repr(py_number)}n') + assert py_number == js_number + + test_bigint(0) + test_bigint(1) + test_bigint(-1) + + # CPython would reuse the objects for small ints in range [-5, 256] + # Making sure we don't do any changes on them + def test_cached_int_object(py_number): + + # type is still int + assert type(py_number) is int + assert not isinstance(py_number, pm.bigint) + test_bigint(py_number) + assert type(py_number) is int + assert not isinstance(py_number, pm.bigint) + # the value doesn't change + # TODO (Tom Tang): Find a way to create a NEW int object with the same + # value, because int literals also reuse the cached int objects + for _ in range(2): + test_cached_int_object(0) # _PyLong_FromByteArray reuses the int 0 object, + # see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L862 for i in range(10): - py_number = random.randint(-limit, limit) - test_bigint(py_number) + test_cached_int_object(random.randint(-5, 256)) + + test_bigint(18014398509481984) # 2**54 + test_bigint(-18014398509481984) # -2**54 + test_bigint(18446744073709551615) # 2**64-1 + test_bigint(18446744073709551616) # 2**64 + test_bigint(-18446744073709551617) # -2**64-1 + + limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 + # = 2**300 + for i in range(10): + py_number = random.randint(-limit, limit) + test_bigint(py_number) + + # TODO (Tom Tang): test -0 (negative zero) + # There's no -0 in both Python int and JS BigInt, + # but this could be possible in JS BigInt's internal representation as it uses a sign bit flag. + # On the other hand, Python int uses `ob_size` 0 for 0, >0 for positive values, <0 for negative values - # TODO (Tom Tang): test -0 (negative zero) - # There's no -0 in both Python int and JS BigInt, - # but this could be possible in JS BigInt's internal representation as it uses a sign bit flag. - # On the other hand, Python int uses `ob_size` 0 for 0, >0 for positive values, <0 for negative values def test_eval_boxed_numbers_bigints(): - def test_boxed_bigint(py_number: int): - # `BigInt()` can only be called without `new` - # https://tc39.es/ecma262/#sec-bigint-constructor - js_number = pm.eval(f'new Object({repr(py_number)}n)') - assert py_number == js_number - - test_boxed_bigint(0) - test_boxed_bigint(1) - test_boxed_bigint(-1) - - limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 - # = 2**300 - for i in range(10): - py_number = random.randint(-limit, limit) - test_boxed_bigint(py_number) + def test_boxed_bigint(py_number: int): + # `BigInt()` can only be called without `new` + # https://tc39.es/ecma262/#sec-bigint-constructor + js_number = pm.eval(f'new Object({repr(py_number)}n)') + assert py_number == js_number + + test_boxed_bigint(0) + test_boxed_bigint(1) + test_boxed_bigint(-1) + + limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 + # = 2**300 + for i in range(10): + py_number = random.randint(-limit, limit) + test_boxed_bigint(py_number) + def test_eval_functions_bigints(): - ident = pm.eval("(a) => { return a }") - add = pm.eval("(a, b) => { return a + b }") - - int1 = random.randint(-1000000,1000000) - bigint1 = pm.bigint(int1) - assert int1 == bigint1 - - # should return pm.bigint - assert type(ident(bigint1)) == pm.bigint - assert ident(bigint1) is not bigint1 - # should return float (because JS number is float64) - assert type(ident(int1)) == float - assert ident(int1) == ident(bigint1) - - # should raise exception on ints > (2^53-1), or < -(2^53-1) - def not_raise(num): - ident(num) - def should_raise(num): - with pytest.raises(OverflowError, match="Use pythonmonkey.bigint instead"): - ident(num) - not_raise(9007199254740991) # 2**53-1, 0x433_FFFFFFFFFFFFF in float64 - should_raise(9007199254740992) # 2**53, 0x434_0000000000000 in float64 - should_raise(9007199254740993) # 2**53+1, NOT 0x434_0000000000001 (2**53+2) - not_raise(-9007199254740991) # -(2**53-1) - should_raise(-9007199254740992) # -(2**53) - should_raise(-9007199254740993) # -(2**53+1) - - # should also raise exception on large integers (>=2**53) that can be exactly represented by a float64 - # in our current implementation - should_raise(9007199254740994) # 2**53+2, 0x434_0000000000001 in float64 - should_raise(2**61+2**9) # 0x43C_0000000000001 in float64 - - # should raise "Use pythonmonkey.bigint" instead of `PyLong_AsLongLong`'s "OverflowError: int too big to convert" on ints larger than 64bits - should_raise(2**65) - should_raise(-2**65) - not_raise(pm.bigint(2**65)) - not_raise(pm.bigint(-2**65)) - - # should raise JS error when mixing a BigInt with a number in arithmetic operations - def should_js_error(a, b): - with pytest.raises(pm.SpiderMonkeyError, match="can't convert BigInt to number"): - add(a, b) - should_js_error(pm.bigint(0), 0) - should_js_error(pm.bigint(1), 2) - should_js_error(3, pm.bigint(4)) - should_js_error(-5, pm.bigint(6)) - - assert add(pm.bigint(0), pm.bigint(0)) == 0 - assert add(pm.bigint(1), pm.bigint(0)) == 1 - assert add(pm.bigint(1), pm.bigint(2)) == 3 - assert add(pm.bigint(-1), pm.bigint(1)) == 0 - assert add(pm.bigint(2**60), pm.bigint(0)) == 1152921504606846976 - assert add(pm.bigint(2**65), pm.bigint(-2**65-1)) == -1 - - # fuzztest - limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 # 2**300 - for i in range(10): - num1 = random.randint(-limit, limit) - num2 = random.randint(-limit, limit) - assert add(pm.bigint(num1), pm.bigint(num2)) == num1+num2 + ident = pm.eval("(a) => { return a }") + add = pm.eval("(a, b) => { return a + b }") + + int1 = random.randint(-1000000, 1000000) + bigint1 = pm.bigint(int1) + assert int1 == bigint1 + + # should return pm.bigint + assert type(ident(bigint1)) == pm.bigint + assert ident(bigint1) is not bigint1 + # should return float (because JS number is float64) + assert type(ident(int1)) == float + assert ident(int1) == ident(bigint1) + + # should raise exception on ints > (2^53-1), or < -(2^53-1) + def not_raise(num): + ident(num) + + def should_raise(num): + with pytest.raises(OverflowError, match="Use pythonmonkey.bigint instead"): + ident(num) + # autopep8: off + not_raise(9007199254740991) # +(2**53-1), 0x433_FFFFFFFFFFFFF in float64 + should_raise(9007199254740992) # +(2**53 ), 0x434_0000000000000 in float64 + should_raise(9007199254740993) # +(2**53+1), NOT 0x434_0000000000001 (2**53+2) + not_raise(-9007199254740991) # -(2**53-1) + should_raise(-9007199254740992) # -(2**53 ) + should_raise(-9007199254740993) # -(2**53+1) + # autopep8: on + + # should also raise exception on large integers (>=2**53) that can be exactly represented by a float64 + # in our current implementation + # autopep8: off + should_raise(9007199254740994) # 2**53+2, 0x434_0000000000001 in float64 + should_raise(2**61 + 2**9) # 0x43C_0000000000001 in float64 + # autopep8: on + + # should raise "Use pythonmonkey.bigint" instead of `PyLong_AsLongLong`'s + # "OverflowError: int too big to convert" on ints larger than 64bits + should_raise(2**65) + should_raise(-2**65) + not_raise(pm.bigint(2**65)) + not_raise(pm.bigint(-2**65)) + + # should raise JS error when mixing a BigInt with a number in arithmetic operations + def should_js_error(a, b): + with pytest.raises(pm.SpiderMonkeyError, match="can't convert BigInt to number"): + add(a, b) + should_js_error(pm.bigint(0), 0) + should_js_error(pm.bigint(1), 2) + should_js_error(3, pm.bigint(4)) + should_js_error(-5, pm.bigint(6)) + + assert add(pm.bigint(0), pm.bigint(0)) == 0 + assert add(pm.bigint(1), pm.bigint(0)) == 1 + assert add(pm.bigint(1), pm.bigint(2)) == 3 + assert add(pm.bigint(-1), pm.bigint(1)) == 0 + assert add(pm.bigint(2**60), pm.bigint(0)) == 1152921504606846976 + assert add(pm.bigint(2**65), pm.bigint(-2**65 - 1)) == -1 + + # fuzztest + limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 # 2**300 + for i in range(10): + num1 = random.randint(-limit, limit) + num2 = random.randint(-limit, limit) + assert add(pm.bigint(num1), pm.bigint(num2)) == num1 + num2 + def test_eval_functions_bigint_factorial(): - factorial = pm.eval("(num) => {let r = 1n; for(let i = 0n; i Number.MAX_SAFE_INTEGER - assert factorial(pm.bigint(21)) == 51090942171709440000 # > 64 bit int - assert factorial(pm.bigint(35)) == 10333147966386144929666651337523200000000 # > 128 bit + factorial = pm.eval("(num) => {let r = 1n; for(let i = 0n; i Number.MAX_SAFE_INTEGER + assert factorial(pm.bigint(21)) == 51090942171709440000 # > 64 bit int + assert factorial(pm.bigint(35)) == 10333147966386144929666651337523200000000 # > 128 bit + def test_eval_functions_bigint_crc32(): - crc_table_at = pm.eval(""" + crc_table_at = pm.eval(""" // translated from https://rosettacode.org/wiki/CRC-32#Python const crc_table = (function create_table() { const a = [] @@ -150,7 +163,7 @@ def test_eval_functions_bigint_crc32(): })(); (n) => crc_table[n] """) - assert type(crc_table_at(1)) == pm.bigint - assert crc_table_at(0) == 0 - assert crc_table_at(1) == 1996959894 - assert crc_table_at(255) == 755167117 # last item + assert type(crc_table_at(1)) == pm.bigint + assert crc_table_at(0) == 0 + assert crc_table_at(1) == 1996959894 + assert crc_table_at(255) == 755167117 # last item diff --git a/tests/python/test_buffer_typed_array.py b/tests/python/test_buffer_typed_array.py index 04171848..0eab66d5 100644 --- a/tests/python/test_buffer_typed_array.py +++ b/tests/python/test_buffer_typed_array.py @@ -1,198 +1,397 @@ import pytest import pythonmonkey as pm import gc -import numpy, array, struct +import numpy +import array +import struct +from io import StringIO +import sys + def test_py_buffer_to_js_typed_array(): - # JS TypedArray/ArrayBuffer should coerce to Python memoryview type - def assert_js_to_py_memoryview(buf: memoryview): - assert type(buf) is memoryview - assert None == buf.obj # https://docs.python.org/3.9/c-api/buffer.html#c.Py_buffer.obj - assert 2 * 4 == buf.nbytes # 2 elements * sizeof(int32_t) - assert "02000000ffffffff" == buf.hex() # native (little) endian - buf1 = pm.eval("new Int32Array([2,-1])") - buf2 = pm.eval("new Int32Array([2,-1]).buffer") - assert_js_to_py_memoryview(buf1) - assert_js_to_py_memoryview(buf2) - assert [2, -1] == buf1.tolist() - assert [2, 0, 0, 0, 255, 255, 255, 255] == buf2.tolist() - assert -1 == buf1[1] - assert 255 == buf2[7] - with pytest.raises(IndexError, match="index out of bounds on dimension 1"): - buf1[2] - with pytest.raises(IndexError, match="index out of bounds on dimension 1"): - buf2[8] - del buf1, buf2 - - # test element value ranges - buf3 = pm.eval("new Uint8Array(1)") - with pytest.raises(ValueError, match="memoryview: invalid value for format 'B'"): - buf3[0] = 256 - with pytest.raises(ValueError, match="memoryview: invalid value for format 'B'"): - buf3[0] = -1 - with pytest.raises(IndexError, match="index out of bounds on dimension 1"): # no automatic resize - buf3[1] = 0 - del buf3 - - # Python buffers should coerce to JS TypedArray - # and the typecode maps to TypedArray subtype (Uint8Array, Float64Array, ...) - assert True == pm.eval("(arr)=>arr instanceof Uint8Array")( bytearray([1,2,3]) ) - assert True == pm.eval("(arr)=>arr instanceof Uint8Array")( numpy.array([1], dtype=numpy.uint8) ) - assert True == pm.eval("(arr)=>arr instanceof Uint16Array")( numpy.array([1], dtype=numpy.uint16) ) - assert True == pm.eval("(arr)=>arr instanceof Uint32Array")( numpy.array([1], dtype=numpy.uint32) ) - assert True == pm.eval("(arr)=>arr instanceof BigUint64Array")( numpy.array([1], dtype=numpy.uint64) ) - assert True == pm.eval("(arr)=>arr instanceof Int8Array")( numpy.array([1], dtype=numpy.int8) ) - assert True == pm.eval("(arr)=>arr instanceof Int16Array")( numpy.array([1], dtype=numpy.int16) ) - assert True == pm.eval("(arr)=>arr instanceof Int32Array")( numpy.array([1], dtype=numpy.int32) ) - assert True == pm.eval("(arr)=>arr instanceof BigInt64Array")( numpy.array([1], dtype=numpy.int64) ) - assert True == pm.eval("(arr)=>arr instanceof Float32Array")( numpy.array([1], dtype=numpy.float32) ) - assert True == pm.eval("(arr)=>arr instanceof Float64Array")( numpy.array([1], dtype=numpy.float64) ) - assert pm.eval("new Uint8Array([1])").format == "B" - assert pm.eval("new Uint16Array([1])").format == "H" - assert pm.eval("new Uint32Array([1])").format == "I" # FIXME (Tom Tang): this is "L" on 32-bit systems - assert pm.eval("new BigUint64Array([1n])").format == "Q" - assert pm.eval("new Int8Array([1])").format == "b" - assert pm.eval("new Int16Array([1])").format == "h" - assert pm.eval("new Int32Array([1])").format == "i" - assert pm.eval("new BigInt64Array([1n])").format == "q" - assert pm.eval("new Float32Array([1])").format == "f" - assert pm.eval("new Float64Array([1])").format == "d" - - # not enough bytes to populate an element of the TypedArray - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: buffer length for BigInt64Array should be a multiple of 8"): - pm.eval("(arr) => new BigInt64Array(arr.buffer)")(array.array('i', [-11111111])) - - # TypedArray with `byteOffset` and `length` - arr1 = array.array('i', [-11111111, 22222222, -33333333, 44444444]) - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid or out-of-range index"): - pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ -4)")(arr1) - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: start offset of Int32Array should be a multiple of 4"): - pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 1)")(arr1) - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: size of buffer is too small for Int32Array with byteOffset"): - pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 20)")(arr1) - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid or out-of-range index"): - pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ -1)")(arr1) - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: attempting to construct out-of-bounds Int32Array on ArrayBuffer"): - pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ 4)")(arr1) - arr2 = pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ 2)")(arr1) - assert 2 * 4 == arr2.nbytes # 2 elements * sizeof(int32_t) - assert [22222222, -33333333] == arr2.tolist() - assert "8e155301ab5f03fe" == arr2.hex() # native (little) endian - assert 22222222 == arr2[0] # offset 1 int32 - with pytest.raises(IndexError, match="index out of bounds on dimension 1"): - arr2[2] - arr3 = pm.eval("(arr) => new Int32Array(arr.buffer, 16 /* byteOffset */)")(arr1) # empty Int32Array - assert 0 == arr3.nbytes - del arr3 - - # test GC - del arr1 - gc.collect(), pm.collect() - gc.collect(), pm.collect() - # TODO (Tom Tang): the 0th element in the underlying buffer is still accessible after GC, even is not referenced by the JS TypedArray with byteOffset - del arr2 - - # mutation - mut_arr_original = bytearray(4) - pm.eval(""" + # JS TypedArray/ArrayBuffer should coerce to Python memoryview type + def assert_js_to_py_memoryview(buf: memoryview): + assert type(buf) is memoryview + assert None is buf.obj # https://docs.python.org/3.9/c-api/buffer.html#c.Py_buffer.obj + assert 2 * 4 == buf.nbytes # 2 elements * sizeof(int32_t) + assert "02000000ffffffff" == buf.hex() # native (little) endian + buf1 = pm.eval("new Int32Array([2,-1])") + buf2 = pm.eval("new Int32Array([2,-1]).buffer") + assert_js_to_py_memoryview(buf1) + assert_js_to_py_memoryview(buf2) + assert [2, -1] == buf1.tolist() + assert [2, 0, 0, 0, 255, 255, 255, 255] == buf2.tolist() + assert -1 == buf1[1] + assert 255 == buf2[7] + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): + buf1[2] + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): + buf2[8] + del buf1, buf2 + + # test element value ranges + buf3 = pm.eval("new Uint8Array(1)") + with pytest.raises(ValueError, match="memoryview: invalid value for format 'B'"): + buf3[0] = 256 + with pytest.raises(ValueError, match="memoryview: invalid value for format 'B'"): + buf3[0] = -1 + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): # no automatic resize + buf3[1] = 0 + del buf3 + + # Python buffers should coerce to JS TypedArray + # and the typecode maps to TypedArray subtype (Uint8Array, Float64Array, ...) + assert pm.eval("(arr)=>arr instanceof Uint8Array")(bytearray([1, 2, 3])) + assert pm.eval("(arr)=>arr instanceof Uint8Array")(numpy.array([1], dtype=numpy.uint8)) + assert pm.eval("(arr)=>arr instanceof Uint16Array")(numpy.array([1], dtype=numpy.uint16)) + assert pm.eval("(arr)=>arr instanceof Uint32Array")(numpy.array([1], dtype=numpy.uint32)) + assert pm.eval("(arr)=>arr instanceof BigUint64Array")(numpy.array([1], dtype=numpy.uint64)) + assert pm.eval("(arr)=>arr instanceof Int8Array")(numpy.array([1], dtype=numpy.int8)) + assert pm.eval("(arr)=>arr instanceof Int16Array")(numpy.array([1], dtype=numpy.int16)) + assert pm.eval("(arr)=>arr instanceof Int32Array")(numpy.array([1], dtype=numpy.int32)) + assert pm.eval("(arr)=>arr instanceof BigInt64Array")(numpy.array([1], dtype=numpy.int64)) + assert pm.eval("(arr)=>arr instanceof Float16Array")(numpy.array([1], dtype=numpy.float16)) + assert pm.eval("(arr)=>arr instanceof Float32Array")(numpy.array([1], dtype=numpy.float32)) + assert pm.eval("(arr)=>arr instanceof Float64Array")(numpy.array([1], dtype=numpy.float64)) + assert pm.eval("new Uint8Array([1])").format == "B" + assert pm.eval("new Uint16Array([1])").format == "H" + assert pm.eval("new Uint32Array([1])").format == "I" # FIXME (Tom Tang): this is "L" on 32-bit systems + assert pm.eval("new BigUint64Array([1n])").format == "Q" + assert pm.eval("new Int8Array([1])").format == "b" + assert pm.eval("new Int16Array([1])").format == "h" + assert pm.eval("new Int32Array([1])").format == "i" + assert pm.eval("new BigInt64Array([1n])").format == "q" + assert pm.eval("new Float16Array([1])").format == "e" + assert pm.eval("new Float32Array([1])").format == "f" + assert pm.eval("new Float64Array([1])").format == "d" + + # not enough bytes to populate an element of the TypedArray + with pytest.raises(pm.SpiderMonkeyError, + match="RangeError: buffer length for BigInt64Array should be a multiple of 8"): + pm.eval("(arr) => new BigInt64Array(arr.buffer)")(array.array('i', [-11111111])) + + # TypedArray with `byteOffset` and `length` + arr1 = array.array('i', [-11111111, 22222222, -33333333, 44444444]) + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid or out-of-range index"): + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ -4)")(arr1) + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: start offset of Int32Array should be a multiple of 4"): + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 1)")(arr1) + with pytest.raises(pm.SpiderMonkeyError, + match="RangeError: size of buffer is too small for Int32Array with byteOffset"): + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 20)")(arr1) + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid or out-of-range index"): + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ -1)")(arr1) + with pytest.raises(pm.SpiderMonkeyError, + match="RangeError: attempting to construct out-of-bounds Int32Array on ArrayBuffer"): + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ 4)")(arr1) + arr2 = pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ 2)")(arr1) + assert 2 * 4 == arr2.nbytes # 2 elements * sizeof(int32_t) + assert [22222222, -33333333] == arr2.tolist() + assert "8e155301ab5f03fe" == arr2.hex() # native (little) endian + assert 22222222 == arr2[0] # offset 1 int32 + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): + arr2[2] + arr3 = pm.eval("(arr) => new Int32Array(arr.buffer, 16 /* byteOffset */)")(arr1) # empty Int32Array + assert 0 == arr3.nbytes + del arr3 + + # test GC + del arr1 + gc.collect(), pm.collect() + gc.collect(), pm.collect() + # TODO (Tom Tang): the 0th element in the underlying buffer is still + # accessible after GC, even is not referenced by the JS TypedArray with + # byteOffset + del arr2 + + # mutation + mut_arr_original = bytearray(4) + pm.eval(""" (/* @type Uint8Array */ arr) => { // 2.25 in float32 little endian arr[2] = 0x10 arr[3] = 0x40 } """)(mut_arr_original) - assert 0x10 == mut_arr_original[2] - assert 0x40 == mut_arr_original[3] - # mutation to a different TypedArray accessing the same underlying data block will also change the original buffer - def do_mutation(mut_arr_js): - assert 2.25 == mut_arr_js[0] - mut_arr_js[0] = 225.50048828125 # float32 little endian: 0x 20 80 61 43 - assert "20806143" == mut_arr_original.hex() - assert 225.50048828125 == array.array("f", mut_arr_original)[0] - mut_arr_new = pm.eval(""" + assert 0x10 == mut_arr_original[2] + assert 0x40 == mut_arr_original[3] + # mutation to a different TypedArray accessing the same underlying data block will also change the original buffer + + def do_mutation(mut_arr_js): + assert 2.25 == mut_arr_js[0] + mut_arr_js[0] = 225.50048828125 # float32 little endian: 0x 20 80 61 43 + assert "20806143" == mut_arr_original.hex() + assert 225.50048828125 == array.array("f", mut_arr_original)[0] + mut_arr_new = pm.eval(""" (/* @type Uint8Array */ arr, do_mutation) => { const mut_arr_js = new Float32Array(arr.buffer) do_mutation(mut_arr_js) return arr } """)(mut_arr_original, do_mutation) - assert [0x20, 0x80, 0x61, 0x43] == mut_arr_new.tolist() - - # simple 1-D numpy array should just work as well - numpy_int16_array = numpy.array([0, 1, 2, 3], dtype=numpy.int16) - assert "0,1,2,3" == pm.eval("(typedArray) => typedArray.toString()")(numpy_int16_array) - assert 3.0 == pm.eval("(typedArray) => typedArray[3]")(numpy_int16_array) - assert True == pm.eval("(typedArray) => typedArray instanceof Int16Array")(numpy_int16_array) - numpy_memoryview = pm.eval("(typedArray) => typedArray")(numpy_int16_array) - assert 2 == numpy_memoryview[2] - assert 4 * 2 == numpy_memoryview.nbytes # 4 elements * sizeof(int16_t) - assert "h" == numpy_memoryview.format # the type code for int16 is 'h', see https://docs.python.org/3.9/library/array.html - with pytest.raises(IndexError, match="index out of bounds on dimension 1"): - numpy_memoryview[4] - - # can work for empty Python buffer - def assert_empty_py_buffer(buf, type: str): - assert 0 == pm.eval("(typedArray) => typedArray.length")(buf) - assert None == pm.eval("(typedArray) => typedArray[0]")(buf) # `undefined` - assert True == pm.eval("(typedArray) => typedArray instanceof "+type)(buf) - assert_empty_py_buffer(bytearray(b''), "Uint8Array") - assert_empty_py_buffer(numpy.array([], dtype=numpy.uint64), "BigUint64Array") - assert_empty_py_buffer(array.array('d', []), "Float64Array") - - # can work for empty TypedArray - def assert_empty_typedarray(buf: memoryview, typecode: str): - assert typecode == buf.format - assert struct.calcsize(typecode) == buf.itemsize - assert 0 == buf.nbytes - assert "" == buf.hex() - assert b"" == buf.tobytes() - assert [] == buf.tolist() - buf.release() - assert_empty_typedarray(pm.eval("new BigInt64Array()"), "q") - assert_empty_typedarray(pm.eval("new Float32Array(new ArrayBuffer(4), 4 /*byteOffset*/)"), "f") - assert_empty_typedarray(pm.eval("(arr)=>arr")( bytearray([]) ), "B") - assert_empty_typedarray(pm.eval("(arr)=>arr")( numpy.array([], dtype=numpy.uint16) ),"H") - assert_empty_typedarray(pm.eval("(arr)=>arr")( array.array("d", []) ),"d") - - # can work for empty ArrayBuffer - def assert_empty_arraybuffer(buf): - assert "B" == buf.format - assert 1 == buf.itemsize - assert 0 == buf.nbytes - assert "" == buf.hex() - assert b"" == buf.tobytes() - assert [] == buf.tolist() - buf.release() - assert_empty_arraybuffer(pm.eval("new ArrayBuffer()")) - assert_empty_arraybuffer(pm.eval("new Uint8Array().buffer")) - assert_empty_arraybuffer(pm.eval("new Float64Array().buffer")) - assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( bytearray([]) )) - assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( pm.eval("(arr)=>arr.buffer")(bytearray()) )) - assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( numpy.array([], dtype=numpy.uint64) )) - assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( array.array("d", []) )) - - # TODO (Tom Tang): shared ArrayBuffer should be disallowed - # pm.eval("new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer") - - # TODO (Tom Tang): once a JS ArrayBuffer is transferred to a worker thread, it should be invalidated in Python-land as well - - # TODO (Tom Tang): error for detached ArrayBuffer, or should it be considered as empty? - - # should error on immutable Python buffers - # Note: Python `bytes` type must be converted to a (mutable) `bytearray` because there's no such a concept of read-only ArrayBuffer in JS - with pytest.raises(BufferError, match="Object is not writable."): - pm.eval("(typedArray) => {}")(b'') - immutable_numpy_array = numpy.arange(10) - immutable_numpy_array.setflags(write=False) - with pytest.raises(ValueError, match="buffer source array is read-only"): - pm.eval("(typedArray) => {}")(immutable_numpy_array) - - # buffer should be in C order (row major) - fortran_order_arr = numpy.array([[1, 2], [3, 4]], order="F") # 1-D array is always considered C-contiguous because it doesn't matter if it's row or column major in 1-D - with pytest.raises(ValueError, match="ndarray is not C-contiguous"): - pm.eval("(typedArray) => {}")(fortran_order_arr) - - # disallow multidimensional array - numpy_2d_array = numpy.array([[1, 2], [3, 4]], order="C") - with pytest.raises(BufferError, match="multidimensional arrays are not allowed"): - pm.eval("(typedArray) => {}")(numpy_2d_array) + assert [0x20, 0x80, 0x61, 0x43] == mut_arr_new.tolist() + + # simple 1-D numpy array should just work as well + numpy_int16_array = numpy.array([0, 1, 2, 3], dtype=numpy.int16) + assert "0,1,2,3" == pm.eval("(typedArray) => typedArray.toString()")(numpy_int16_array) + assert 3.0 == pm.eval("(typedArray) => typedArray[3]")(numpy_int16_array) + assert pm.eval("(typedArray) => typedArray instanceof Int16Array")(numpy_int16_array) + numpy_memoryview = pm.eval("(typedArray) => typedArray")(numpy_int16_array) + assert 2 == numpy_memoryview[2] + assert 4 * 2 == numpy_memoryview.nbytes # 4 elements * sizeof(int16_t) + # the type code for int16 is 'h', see https://docs.python.org/3.9/library/array.html + assert "h" == numpy_memoryview.format + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): + numpy_memoryview[4] + + # can work for empty Python buffer + def assert_empty_py_buffer(buf, type: str): + assert 0 == pm.eval("(typedArray) => typedArray.length")(buf) + assert None is pm.eval("(typedArray) => typedArray[0]")(buf) # `undefined` + assert pm.eval("(typedArray) => typedArray instanceof " + type)(buf) + assert_empty_py_buffer(bytearray(b''), "Uint8Array") + assert_empty_py_buffer(numpy.array([], dtype=numpy.uint64), "BigUint64Array") + assert_empty_py_buffer(array.array('d', []), "Float64Array") + + # can work for empty TypedArray + def assert_empty_typedarray(buf: memoryview, typecode: str): + assert typecode == buf.format + assert struct.calcsize(typecode) == buf.itemsize + assert 0 == buf.nbytes + assert "" == buf.hex() + assert b"" == buf.tobytes() + assert [] == buf.tolist() + buf.release() + assert_empty_typedarray(pm.eval("new BigInt64Array()"), "q") + assert_empty_typedarray(pm.eval("new Float32Array(new ArrayBuffer(4), 4 /*byteOffset*/)"), "f") + assert_empty_typedarray(pm.eval("(arr)=>arr")(bytearray([])), "B") + assert_empty_typedarray(pm.eval("(arr)=>arr")(numpy.array([], dtype=numpy.uint16)), "H") + assert_empty_typedarray(pm.eval("(arr)=>arr")(array.array("d", [])), "d") + + # can work for empty ArrayBuffer + def assert_empty_arraybuffer(buf): + assert "B" == buf.format + assert 1 == buf.itemsize + assert 0 == buf.nbytes + assert "" == buf.hex() + assert b"" == buf.tobytes() + assert [] == buf.tolist() + buf.release() + assert_empty_arraybuffer(pm.eval("new ArrayBuffer()")) + assert_empty_arraybuffer(pm.eval("new Uint8Array().buffer")) + assert_empty_arraybuffer(pm.eval("new Float64Array().buffer")) + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")(bytearray([]))) + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")(pm.eval("(arr)=>arr.buffer")(bytearray()))) + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")(numpy.array([], dtype=numpy.uint64))) + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")(array.array("d", []))) + + # TODO (Tom Tang): shared ArrayBuffer should be disallowed + # pm.eval("new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer") + + # TODO (Tom Tang): once a JS ArrayBuffer is transferred to a worker + # thread, it should be invalidated in Python-land as well + + # TODO (Tom Tang): error for detached ArrayBuffer, or should it be considered as empty? + + # buffer should be in C order (row major) + # 1-D array is always considered C-contiguous because it doesn't matter if it's row or column major in 1-D + fortran_order_arr = numpy.array([[1, 2], [3, 4]], order="F") + with pytest.raises(ValueError, match="ndarray is not C-contiguous"): + pm.eval("(typedArray) => {}")(fortran_order_arr) + + # disallow multidimensional array + numpy_2d_array = numpy.array([[1, 2], [3, 4]], order="C") + with pytest.raises(BufferError, match="multidimensional arrays are not allowed"): + pm.eval("(typedArray) => {}")(numpy_2d_array) + + + +def test_bytes_proxy_write(): + with pytest.raises(TypeError, match="'bytes' object has only read-only attributes"): + pm.eval('(bytes) => bytes[0] = 5')(bytes("hello world", "ascii")) + + +def test_bytes_get_index_python(): + b = pm.eval('(bytes) => bytes')(bytes("hello world", "ascii")) + assert b[0] == 104 + + +def test_bytes_get_index_js(): + b = pm.eval('(bytes) => bytes[1]')(bytes("hello world", "ascii")) + assert b == 101.0 + + +def test_bytes_bytes_per_element(): + b = pm.eval('(bytes) => bytes.BYTES_PER_ELEMENT')(bytes("hello world", "ascii")) + assert b == 1.0 + + +def test_bytes_buffer(): + b = pm.eval('(bytes) => bytes.buffer')(bytes("hello world", "ascii")) + assert repr(b).__contains__("memory at") + assert b[2] == 108 + + +def test_bytes_length(): + b = pm.eval('(bytes) => bytes.length')(bytes("hello world", "ascii")) + assert b == 11.0 + +def test_bytes_bytelength(): + b = pm.eval('(bytes) => bytes.byteLength')(bytes("hello world", "ascii")) + assert b == 11.0 + + +def test_bytes_byteoffset(): + b = pm.eval('(bytes) => bytes.byteOffset')(bytes("hello world", "ascii")) + assert b == 0.0 + + +def test_bytes_instanceof(): + result = [None] + pm.eval("(result, bytes) => {result[0] = bytes instanceof Uint8Array}")(result, bytes("hello world", "ascii")) + assert result[0] + + +def test_constructor_creates_typedarray(): + items = bytes("hello world", "ascii") + result = [0] + pm.eval("(result, arr) => { result[0] = arr.constructor; result[0] = new result[0]; result[0] = result[0] instanceof Uint8Array}")(result, items) + assert result[0] == True + + +def test_bytes_valueOf(): + a = pm.eval('(bytes) => bytes.valueOf()')(bytes("hello world", "ascii")) + assert a == "104,101,108,108,111,32,119,111,114,108,100" + + +def test_bytes_toString(): + a = pm.eval('(bytes) => bytes.toString()')(bytes("hello world", "ascii")) + assert a == "104,101,108,108,111,32,119,111,114,108,100" + + +def test_bytes_console(): + temp_out = StringIO() + sys.stdout = temp_out + pm.eval('console.log')(bytes("hello world", "ascii")) + assert temp_out.getvalue().__contains__('104,101,108,108,111,32,119,111,114,108,100') + + +# iterator symbol property + +def test_iterator_type_function(): + items = bytes("hello world", "ascii") + result = [0] + pm.eval("(result, arr) => { result[0] = typeof arr[Symbol.iterator]}")(result, items) + assert result[0] == 'function' + + +def test_iterator_first_next(): + items = bytes("hello world", "ascii") + result = [0] + pm.eval("(result, arr) => { result[0] = arr[Symbol.iterator]().next()}")(result, items) + assert result[0].value == 104.0 + assert not result[0].done + + +def test_iterator_second_next(): + items = bytes("hello world", "ascii") + result = [0] + pm.eval("(result, arr) => { let iterator = arr[Symbol.iterator](); iterator.next(); result[0] = iterator.next()}")( + result, items) + assert result[0].value == 101.0 + assert not result[0].done + + +def test_iterator_last_next(): + items = bytes("hello world", "ascii") + result = [0] + pm.eval(""" + (result, arr) => { + let iterator = arr[Symbol.iterator](); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + result[0] = iterator.next(); + } + """)(result, items) + assert result[0].value is None + assert result[0].done + + +def test_iterator_iterator(): + items = bytes("hell", "ascii") + result = [0] + pm.eval("(result, arr) => {let iter = arr[Symbol.iterator](); let head = iter.next().value; result[0] = [...iter] }")( + result, items) + assert result[0] == [101.0, 108.0, 108.0] + + +# entries + +def test_entries_next(): + items = bytes("abc", "ascii") + result = [0] + pm.eval("(result, arr) => {result[0] = arr.entries(); result[0] = result[0].next().value}")(result, items) + assert items == bytes("abc", "ascii") + assert result[0] == [0, 97.0] + + +def test_entries_next_next(): + items = bytes("abc", "ascii") + result = [0] + pm.eval("(result, arr) => {result[0] = arr.entries(); result[0].next(); result[0] = result[0].next().value}")( + result, items) + assert result[0] == [1, 98.0] + + +def test_entries_next_next_undefined(): + items = bytes("a", "ascii") + result = [0] + pm.eval("(result, arr) => {result[0] = arr.entries(); result[0].next(); result[0] = result[0].next().value}")( + result, items) + assert result[0] is None + + +# keys + +def test_keys_iterator(): + items = bytes("abc", "ascii") + result = [7, 8, 9] + pm.eval(""" + (result, arr) => { + index = 0; + iterator = arr.keys(); + for (const key of iterator) { + result[index] = key; + index++; + } + } + """)(result, items) + assert result == [0, 1, 2] + + +# values + +def test_values_iterator(): + items = bytes("abc", "ascii") + result = [7, 8, 9] + pm.eval(""" + (result, arr) => { + index = 0; + iterator = arr.values(); + for (const value of iterator) { + result[index] = value; + index++; + } + } + """)(result, items) + assert result == [97.0, 98.0, 99.0] + assert result is not items \ No newline at end of file diff --git a/tests/python/test_dict_methods.py b/tests/python/test_dict_methods.py new file mode 100644 index 00000000..d0fccba9 --- /dev/null +++ b/tests/python/test_dict_methods.py @@ -0,0 +1,573 @@ +import pythonmonkey as pm + +# get + + +def test_get_no_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.get('fruit') + assert foundKey == 'apple' + + +def test_get_no_default_not_found(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.get('fuit') + assert foundKey is None + + +def test_get_default_not_found(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.get('fuit', 'orange') + assert foundKey == 'orange' + + +def test_get_no_params(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + try: + likes.get() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "get expected at least 1 argument, got 0" + +# setdefault + + +def test_setdefault_found(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.setdefault('color') + assert foundKey == 'blue' + + +def test_setdefault_found_ignore_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.setdefault('color', 'yello') + assert foundKey == 'blue' + + +def test_setdefault_not_found_no_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.setdefault('colo') + assert likes['colo'] is None + assert foundKey is None + + +def test_setdefault_not_found_with_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.setdefault('colo', 'yello') + assert likes['colo'] == 'yello' + assert foundKey == 'yello' + + +def test_setdefault_no_params(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + try: + likes.setdefault() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "setdefault expected at least 1 argument, got 0" + + +def test_setdefault_with_shadowing(): + jsObj = pm.eval("({get: 'value'})") + a = jsObj.setdefault("get", "val") + assert a == 'value' + +# pop + + +def test_pop_found(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.pop('color') + assert likes['color'] is None + assert foundKey == 'blue' + + +def test_pop_not_found(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + try: + likes.pop('colo') + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "'colo'" + + +def test_pop_twice_not_found(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + likes.pop('color') + try: + likes.pop('color') + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "'color'" + + +def test_pop_found_ignore_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.pop('color', 'u') + assert foundKey == 'blue' + + +def test_pop_not_found_with_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + foundKey = likes.pop('colo', 'unameit') + assert foundKey == 'unameit' + + +def test_pop_not_found_no_default(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + try: + likes.pop('colo') + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "'colo'" + + +def test_pop_no_params(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + try: + likes.pop() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "pop expected at least 1 argument, got 0" + +# clear + + +def test_clear(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + likes.clear() + assert len(likes) == 0 + +# copy + + +def test_copy(): + likes = pm.eval('({"color": "blue", "fruit": "apple", "pet": "dog"})') + otherLikes = likes.copy() + otherLikes["color"] = "yellow" + + assert likes == {"color": "blue", "fruit": "apple", "pet": "dog"} + assert otherLikes == {"color": "yellow", "fruit": "apple", "pet": "dog"} + +# update + + +def test_update_true_dict_right(): + a = pm.eval("({'c':5})") + b = {'d': 6.0} + a.update(b) + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_update_true_dict_left(): + a = {'d': 6.0} + b = pm.eval("({'c':5})") + a.update(b) + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'c': 5.0} + + +def test_update_true_two_pm_dicts(): + a = pm.eval("({'c':5})") + b = pm.eval("({'d':6})") + a.update(b) + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_update_two_args(): + a = pm.eval("({'c':5})") + a.update(B='For', C='Geeks') + assert a == {'c': 5.0, 'B': 'For', 'C': 'Geeks'} + + +def test_update_iterable(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + car.update([('y', 3), ('z', 0)]) + assert car == {'brand': 'Ford', 'model': 'Mustang', 'year': 1964, 'y': 3, 'z': 0} + + +def test_update_iterable_wrong_type(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = [1, 2] + try: + car.update(a) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "cannot convert dictionary update sequence element #0 to a sequence" + +# keys + + +def test_keys_iter(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in obj.keys(): + result.append(i) + assert result == ['a', 'b'] + + +def test_keys_iter_reverse(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in reversed(obj.keys()): + result.append(i) + assert result == ['b', 'a'] + + +def test_keys_list(): + obj = pm.eval("({ a: 123, b: 'test' })") + assert list(obj.keys()) == ['a', 'b'] + + +def test_keys_repr(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.keys() + assert str(a) == "dict_keys(['brand', 'model', 'year'])" + + +def test_keys_substract(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.keys() + b = a - ['brand'] + assert b == {'model', 'year'} + + +def test_keys_richcompare_two_own(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.keys() + b = car.keys() + assert a == b + assert a is not b + assert a <= b + assert not (a < b) + assert a >= b + assert not (a > b) + + +def test_keys_richcompare_one_own(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.keys() + care = {"brand": "Ford", "model": "Mustang", "year": 1964} + b = care.keys() + assert a == b + assert b == a + assert a <= b + assert not (a < b) + assert a >= b + assert not (a > b) + assert b <= a + assert not (b > a) + assert b >= a + assert not (b > a) + + +def test_keys_intersect_one_own_smaller(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + b = keys & {'eggs', 'bacon', 'salad', 'jam'} + c = {'eggs', 'bacon', 'salad', 'jam'} & keys + assert b == {'bacon', 'eggs'} == c + + +def test_keys_intersect_two_own_smaller(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + dishes1 = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1})") + keys1 = dishes1.keys() + b = keys & keys1 + c = keys1 & keys + assert b == {'bacon', 'eggs', 'sausage'} == c + + +def test_keys_intersect_one_own(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + dishes1 = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'peas': 6} + keys1 = dishes1.keys() + b = keys & keys1 + c = keys1 & keys + assert b == {'bacon', 'eggs', 'sausage'} == c + + +def test_keys_or(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + others = keys | ['juice', 'juice', 'juice'] + assert others == {'bacon', 'spam', 'juice', 'sausage', 'eggs'} + others = ['apple'] | keys + assert others == {'bacon', 'spam', 'sausage', 'apple', 'eggs'} + + +def test_keys_xor(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + others = keys ^ {'sausage', 'juice'} + assert others == {'bacon', 'spam', 'juice', 'eggs'} + + +def test_keys_len(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + assert len(keys) == 4 + + +def test_keys_contains(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + assert 'eggs' in keys + assert 'egg' not in keys + + +def test_keys_isdisjoint_self(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + assert not keys.isdisjoint(keys) + + +def test_keys_isdisjoint_true_keys(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + assert keys.isdisjoint({"egg": 4, "e": 5, "f": 6}.keys()) + + +def test_keys_isnotdisjoint_true_keys(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + assert not keys.isdisjoint({"eggs": 4, "e": 5, "f": 6}.keys()) + + +def test_keys_isnotdisjoint_own_keys(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + dishese = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys1 = dishese.keys() + assert not keys.isdisjoint(keys1) + + +def test_keys_isnotdisjoint_own_keys_longer(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + dishese = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500, 'juice':6467})") + keys1 = dishese.keys() + assert not keys.isdisjoint(keys1) + + +def test_keys_isnotdisjoint_true_keys_longer(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + dishese = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500, 'juice': 6467} + keys1 = dishese.keys() + assert not keys.isdisjoint(keys1) + + +def test_keys_update_object_updates_the_keys(): + employee = pm.eval("({'name': 'Phil', 'age': 22})") + dictionaryKeys = employee.keys() + employee.update({'salary': 3500.0}) + assert str(dictionaryKeys) == "dict_keys(['name', 'age', 'salary'])" + + +def test_keys_mapping(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + keys = dishes.keys() + assert str(keys.mapping) == "{'eggs': 2.0, 'sausage': 1.0, 'bacon': 1.0, 'spam': 500.0}" + assert keys.mapping['spam'] == 500 + +# values + + +def test_values_iter(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in obj.values(): + result.append(i) + assert result == [123, 'test'] + + +def test_values_iter_reversed(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in reversed(obj.values()): + result.append(i) + assert result == ['test', 123] + + +def test_values_list(): + obj = pm.eval("({ a: 123, b: 'test' })") + assert list(obj.values()) == [123, 'test'] + + +def test_values_repr(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.values() + assert str(a) == "dict_values(['Ford', 'Mustang', 1964.0])" + + +def test_values_update_object_updates_the_values(): + employee = pm.eval("({'name': 'Phil', 'age': 22})") + dictionaryValues = employee.values() + employee.update({'salary': 3500.0}) + assert str(dictionaryValues) == "dict_values(['Phil', 22.0, 3500.0])" + + +def test_values_mapping(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + values = dishes.values() + assert str(values.mapping) == "{'eggs': 2.0, 'sausage': 1.0, 'bacon': 1.0, 'spam': 500.0}" + assert values.mapping['spam'] == 500 + +# items + + +def test_items_iter(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in obj.items(): + result.append(i) + assert result == [('a', 123.0), ('b', 'test')] + + +def test_items_iter_reversed(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in reversed(obj.items()): + result.append(i) + assert result == [('b', 'test'), ('a', 123.0)] + + +def test_items_list(): + obj = pm.eval("({ a: 123, b: 'test' })") + assert list(obj.items()) == [('a', 123.0), ('b', 'test')] + + +def test_items_repr(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.items() + assert str(a) == "dict_items([('brand', 'Ford'), ('model', 'Mustang'), ('year', 1964.0)])" + + +def test_items_substract(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = car.items() + b = a - [('brand', 'Ford')] + assert b == {('model', 'Mustang'), ('year', 1964.0)} + + +def test_items_or(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + others = items | [('juice', 'apple')] + assert others == {('spam', 500.0), ('juice', 'apple'), ('eggs', 2.0), ('sausage', 1.0), ('bacon', 1.0)} + others = [('juice', 'raisin')] | items + assert others == {('spam', 500.0), ('juice', 'raisin'), ('eggs', 2.0), ('sausage', 1.0), ('bacon', 1.0)} + + +def test_items_xor(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + others = items ^ {('eggs', 2)} + assert others == {('spam', 500), ('bacon', 1), ('sausage', 1)} + + +def test_items_intersect_one_own_smaller(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + b = items & {('eggs', 2), ('bacon', 1), ('salad', 12), ('jam', 34)} + c = {('eggs', 2), ('bacon', 1), ('salad', 12), ('jam', 34)} & items + assert b == {('bacon', 1), ('eggs', 2)} == c + + +def test_items_intersect_one(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + b = items & {('eggs', 2)} + assert b == {('eggs', 2)} + + +def test_items_intersect_none(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + b = items & {('ketchup', 12)} + assert str(b) == "set()" + + +def test_items_len(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + assert len(items) == 4 + +# TODO tuple support +# def test_items_contains(): +# dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") +# items = dishes.items() +# assert {('eggs', 2)} in items + + +def test_items_update_object_updates_the_items(): + employee = pm.eval("({'name': 'Phil', 'age': 22})") + dictionaryItems = employee.items() + employee.update({('salary', 3500.0)}) + assert str(dictionaryItems) == "dict_items([('name', 'Phil'), ('age', 22.0), ('salary', 3500.0)])" + + +def test_items_mapping(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + items = dishes.items() + assert str(items.mapping) == "{'eggs': 2.0, 'sausage': 1.0, 'bacon': 1.0, 'spam': 500.0}" + assert items.mapping['spam'] == 500 + +# get method + + +def test_get_method(): + dishes = pm.eval("({'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500})") + assert dishes.get('eggs') == 2 + +# get method shadowing + + +def test_method_shadowing(): + jsObj = pm.eval("({get: 'value'})") + assert repr(jsObj.get).__contains__(" { return dict; }")(d) + assert d is proxy_d + + +def test_eval_dicts_subdicts(): + d = {"a": 1, "b": {"c": 2}} + + assert pm.eval("(dict) => { return dict.a; }")(d) == 1.0 + assert pm.eval("(dict) => { return dict.b; }")(d) is d["b"] + assert pm.eval("(dict) => { return dict.b.c; }")(d) == 2.0 + + +def test_eval_dicts_cycle(): + d: dict = {"a": 1, "b": 2} + d["recursive"] = d + + assert pm.eval("(dict) => { return dict.a; }")(d) == 1.0 + assert pm.eval("(dict) => { return dict.b; }")(d) == 2.0 + assert pm.eval("(dict) => { return dict.recursive; }")(d) is d["recursive"] + + +def test_eval_objects(): + pyObj = pm.eval("Object({a:1.0})") + assert pyObj == {'a': 1.0} + + +def test_eval_objects_subobjects(): + pyObj = pm.eval("Object({a:1.0, b:{c:2.0}})") + + assert pyObj['a'] == 1.0 + assert pyObj['b'] == {'c': 2.0} + assert pyObj['b']['c'] == 2.0 + + +def test_eval_objects_cycle(): + pyObj = pm.eval("Object({a:1.0, b:2.0, recursive: function() { this.recursive = this; return this; }}.recursive())") + + assert pyObj['a'] == 1.0 + assert pyObj['b'] == 2.0 + assert pyObj['recursive'] == pyObj + + +def test_eval_objects_proxy_get(): + f = pm.eval("(obj) => { return obj.a}") + assert f({'a': 42.0}) == 42.0 + + +def test_eval_objects_proxy_set(): + f = pm.eval("(obj) => { obj.a = 42.0; return;}") + pyObj = {} + f(pyObj) + assert pyObj['a'] == 42.0 + + +def test_eval_objects_proxy_keys(): + f = pm.eval("(obj) => { return Object.keys(obj)[0]}") + assert f({'a': 42.0}) == 'a' + + +def test_eval_objects_proxy_delete(): + f = pm.eval("(obj) => { delete obj.a }") + pyObj = {'a': 42.0} + f(pyObj) + assert 'a' not in pyObj + + +def test_eval_objects_proxy_has(): + f = pm.eval("(obj) => { return 'a' in obj }") + pyObj = {'a': 42.0} + assert (f(pyObj)) + + +def test_eval_objects_proxy_not_extensible(): + assert not pm.eval("(o) => Object.isExtensible(o)")({}) + assert not pm.eval("(o) => Object.isExtensible(o)")({"abc": 1}) + assert pm.eval("(o) => Object.preventExtensions(o) === o")({}) + + +def test_eval_objects_jsproxy_contains(): + a = pm.eval("({'c':5})") + assert 'c' in a + + +def test_eval_objects_jsproxy_does_not_contain(): + a = pm.eval("({'c':5})") + assert not (4 in a) + + +def test_eval_objects_jsproxy_does_not_contain_value(): + a = pm.eval("({'c':5})") + assert not (5 in a) + + +def test_eval_objects_jsproxy_or(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = pm.eval("({'d':6})") + c = a | b + assert a == {'c': 5.0} + assert c == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_or_true_dict_right(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = {'d': 6.0} + c = a | b + assert a == {'c': 5.0} + assert c == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_or_true_dict_left(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = {'c': 5} + b = pm.eval("({'d':6})") + c = a | b + assert a == {'c': 5.0} + assert c == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = pm.eval("({'d':6})") + a |= b + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or_true_dict_right(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = {'d': 6.0} + a |= b + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or_true_dict_left(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = {'c': 5.0} + b = pm.eval("({'d':6})") + a |= b + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + assert pm.eval("(o) => Object.preventExtensions(o) === o")({"abc": 1}) + + +def test_instanceof_object(): + a = {'c': 5.0} + result = [None] + pm.eval("(result, obj) => {result[0] = obj instanceof Object}")(result, a) + assert result[0] + +# iter + + +def test_eval_objects_proxy_iterate(): + obj = pm.eval("({ a: 123, b: 'test' })") + result = [] + for i in obj: + result.append(i) + assert result == ['a', 'b'] + + +def test_eval_objects_proxy_min(): + obj = pm.eval("({ a: 123, b: 'test' })") + assert min(obj) == 'a' + + +def test_eval_objects_proxy_max(): + obj = pm.eval("({ a: 123, b: 'test' })") + assert max(obj) == 'b' + + +def test_eval_objects_proxy_repr(): + obj = pm.eval("({ a: 123, b: 'test' , c: { d: 1 }})") + obj.e = obj # supporting circular references + expected = "{'a': 123.0, 'b': 'test', 'c': {'d': 1.0}, 'e': {...}}" + assert repr(obj) == expected + assert str(obj) == expected + + +def test_eval_objects_proxy_dict_conversion(): + obj = pm.eval("({ a: 123, b: 'test' , c: { d: 1 }})") + d = dict(obj) + assert type(obj) is not dict # dict subclass + assert type(d) is dict # strict dict + assert repr(d) == "{'a': 123.0, 'b': 'test', 'c': {'d': 1.0}}" + assert str(obj.keys()) == "dict_keys(['a', 'b', 'c'])" + assert obj == d + + +def test_eval_objects_jsproxy_get(): + proxy = pm.eval("({a: 1})") + assert 1.0 == proxy['a'] + assert 1.0 == proxy.a + + +def test_eval_objects_jsproxy_set(): + proxy = pm.eval("({a: 1})") + proxy.a = 2.0 + assert 2.0 == proxy['a'] + proxy['a'] = 3.0 + assert 3.0 == proxy.a + proxy.b = 1.0 + assert 1.0 == proxy['b'] + proxy['b'] = 2.0 + assert 2.0 == proxy.b + + +def test_eval_objects_jsproxy_length(): + proxy = pm.eval("({a: 1, b:2})") + assert 2 == len(proxy) + + +def test_eval_objects_jsproxy_delete(): + proxy = pm.eval("({a: 1})") + del proxy.a + assert None is proxy.a + assert None is proxy['a'] + + +def test_eval_objects_jsproxy_compare(): + proxy = pm.eval("({a: 1, b:2})") + assert proxy == {'a': 1.0, 'b': 2.0} + + +def test_eval_objects_jsproxy_contains(): + a = pm.eval("({'c':5})") + assert 'c' in a + + +def test_eval_objects_jsproxy_does_not_contain(): + a = pm.eval("({'c':5})") + assert not (4 in a) + + +def test_eval_objects_jsproxy_does_not_contain_value(): + a = pm.eval("({'c':5})") + assert not (5 in a) + + +def test_eval_objects_jsproxy_or(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = pm.eval("({'d':6})") + c = a | b + assert a == {'c': 5.0} + assert c == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_or_true_dict_right(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = {'d': 6.0} + c = a | b + assert a == {'c': 5.0} + assert c == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_or_true_dict_left(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = {'c': 5} + b = pm.eval("({'d':6})") + c = a | b + assert a == {'c': 5.0} + assert c == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = pm.eval("({'d':6})") + a |= b + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or_true_dict_right(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = pm.eval("({'c':5})") + b = {'d': 6.0} + a |= b + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or_true_dict_left(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = {'c': 5.0} + b = pm.eval("({'d':6})") + a |= b + assert a == {'c': 5.0, 'd': 6.0} + assert b == {'d': 6.0} + + +def test_eval_objects_jsproxy_inplace_or_true_dict_left_iterable_right(): + if sys.version_info[0] >= 3 and sys.version_info[1] >= 9: # | is not implemented for dicts in 3.8 or less + a = {'c': 5} + a |= [('y', 3), ('z', 0)] + assert a == {'c': 5, 'y': 3, 'z': 0} + + +def test_update_inplace_or_iterable_wrong_type(): + car = pm.eval('({"brand": "Ford","model": "Mustang","year": 1964})') + a = [1, 2] + try: + car |= a + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "cannot convert dictionary update sequence element #0 to a sequence" + + +def test_instanceof_object(): + items = {'a': 10} + result = [None] + pm.eval("(result, obj) => {result[0] = obj instanceof Object}")(result, items) + assert result[0] + + +def test_not_instanceof_string(): + items = {'a': 10} + result = [None] + pm.eval("(result, obj) => {result[0] = obj instanceof String}")(result, items) + assert not result[0] + +# valueOf + + +def test_valueOf(): + items = {'a': 10} + result = [0] + pm.eval("(result, obj) => {result[0] = obj.valueOf()}")(result, items) + assert items == {'a': 10} + assert result[0] is items + +# toString + + +def test_toString(): + items = {'a': 10} + result = [0] + pm.eval("(result, obj) => {result[0] = obj.toString()}")(result, items) + assert result[0] == '[object Object]' + +# toLocaleString + + +def test_toLocaleString(): + items = {'a': 10} + result = [0] + pm.eval("(result, obj) => {result[0] = obj.toLocaleString()}")(result, items) + assert result[0] == '[object Object]' + + +# __class__ +def test___class__attribute(): + items = pm.eval("({'a': 10})") + assert repr(items.__class__) == "" + +# none value attribute + + +def test___none__attribute(): + a = pm.eval("({'0': 1, '1': 2})") + assert a[2] is None diff --git a/tests/python/test_dicts_lists.py b/tests/python/test_dicts_lists.py deleted file mode 100644 index 0f7e6618..00000000 --- a/tests/python/test_dicts_lists.py +++ /dev/null @@ -1,128 +0,0 @@ -import pythonmonkey as pm - -def test_eval_dicts(): - d = {"a":1} - proxy_d = pm.eval("(dict) => { return dict; }")(d) - assert d is proxy_d - -def test_eval_dicts_subdicts(): - d = {"a":1, "b":{"c": 2}} - - assert pm.eval("(dict) => { return dict.a; }")(d) == 1.0 - assert pm.eval("(dict) => { return dict.b; }")(d) is d["b"] - assert pm.eval("(dict) => { return dict.b.c; }")(d) == 2.0 - -def test_eval_dicts_cycle(): - d: dict = {"a":1, "b":2} - d["recursive"] = d - - assert pm.eval("(dict) => { return dict.a; }")(d) == 1.0 - assert pm.eval("(dict) => { return dict.b; }")(d) == 2.0 - assert pm.eval("(dict) => { return dict.recursive; }")(d) is d["recursive"] - -def test_eval_objects(): - pyObj = pm.eval("Object({a:1.0})") - assert pyObj == {'a':1.0} - -def test_eval_objects_subobjects(): - pyObj = pm.eval("Object({a:1.0, b:{c:2.0}})") - - assert pyObj['a'] == 1.0 - assert pyObj['b'] == {'c': 2.0} - assert pyObj['b']['c'] == 2.0 - -def test_eval_objects_cycle(): - pyObj = pm.eval("Object({a:1.0, b:2.0, recursive: function() { this.recursive = this; return this; }}.recursive())") - - assert pyObj['a'] == 1.0 - assert pyObj['b'] == 2.0 - assert pyObj['recursive'] == pyObj - -def test_eval_objects_proxy_get(): - f = pm.eval("(obj) => { return obj.a}") - assert f({'a':42.0}) == 42.0 - -def test_eval_objects_proxy_set(): - f = pm.eval("(obj) => { obj.a = 42.0; return;}") - pyObj = {} - f(pyObj) - assert pyObj['a'] == 42.0 - -def test_eval_objects_proxy_keys(): - f = pm.eval("(obj) => { return Object.keys(obj)[0]}") - assert f({'a':42.0}) == 'a' - -def test_eval_objects_proxy_delete(): - f = pm.eval("(obj) => { delete obj.a }") - pyObj = {'a': 42.0} - f(pyObj) - assert 'a' not in pyObj - -def test_eval_objects_proxy_has(): - f = pm.eval("(obj) => { return 'a' in obj }") - pyObj = {'a': 42.0} - assert(f(pyObj)) - -def test_eval_objects_proxy_not_extensible(): - assert False == pm.eval("(o) => Object.isExtensible(o)")({}) - assert False == pm.eval("(o) => Object.isExtensible(o)")({ "abc": 1 }) - assert True == pm.eval("(o) => Object.preventExtensions(o) === o")({}) - assert True == pm.eval("(o) => Object.preventExtensions(o) === o")({ "abc": 1 }) - -def test_eval_objects_proxy_proto(): - assert pm.null == pm.eval("(o) => Object.getPrototypeOf(o)")({}) - assert pm.null == pm.eval("(o) => Object.getPrototypeOf(o)")({ "abc": 1 }) - -def test_eval_objects_proxy_iterate(): - obj = pm.eval("({ a: 123, b: 'test' })") - result = [] - for i in obj: - result.append(i) - assert result == [('a', 123.0), ('b', 'test')] - -def test_eval_objects_proxy_repr(): - obj = pm.eval("({ a: 123, b: 'test' , c: { d: 1 }})") - obj.e = obj # supporting circular references - expected = "{'a': 123.0, 'b': 'test', 'c': {'d': 1.0}, 'e': [Circular]}" - assert repr(obj) == expected - assert str(obj) == expected - -def test_eval_objects_proxy_dict_conversion(): - obj = pm.eval("({ a: 123, b: 'test' , c: { d: 1 }})") - d = dict(obj) - assert type(obj) is not dict # dict subclass - assert type(d) is dict # strict dict - assert repr(d) == "{'a': 123.0, 'b': 'test', 'c': {'d': 1.0}}" - assert obj.keys() == ['a', 'b', 'c'] # Conversion from a dict-subclass to a strict dict internally calls the .keys() method - assert list(d.keys()) == obj.keys() - assert obj == d - -def test_eval_objects_jsproxy_get(): - proxy = pm.eval("({a: 1})") - assert 1.0 == proxy['a'] - assert 1.0 == proxy.a - -def test_eval_objects_jsproxy_set(): - proxy = pm.eval("({a: 1})") - proxy.a = 2.0 - assert 2.0 == proxy['a'] - proxy['a'] = 3.0 - assert 3.0 == proxy.a - proxy.b = 1.0 - assert 1.0 == proxy['b'] - proxy['b'] = 2.0 - assert 2.0 == proxy.b - -def test_eval_objects_jsproxy_length(): - proxy = pm.eval("({a: 1, b:2})") - assert 2 == len(proxy) - -def test_eval_objects_jsproxy_delete(): - proxy = pm.eval("({a: 1})") - del proxy.a - assert None == proxy.a - assert None == proxy['a'] - -def test_eval_objects_jsproxy_compare(): - proxy = pm.eval("({a: 1, b:2})") - assert proxy == {'a': 1.0, 'b': 2.0} diff --git a/tests/python/test_event_loop.py b/tests/python/test_event_loop.py index 1377f400..2e4215ea 100644 --- a/tests/python/test_event_loop.py +++ b/tests/python/test_event_loop.py @@ -2,236 +2,420 @@ import pythonmonkey as pm import asyncio + +def test_setTimeout_unref(): + async def async_fn(): + obj = {'val': 0} + pm.eval("""(obj) => { + setTimeout(()=>{ obj.val = 2 }, 1000).ref().ref().unref().ref().unref().unref(); + // chaining, no use on the first two ref calls since it's already refed initially + setTimeout(()=>{ obj.val = 1 }, 100); + }""")(obj) + await pm.wait() # we shouldn't wait until the first timer is fired since it's currently unrefed + assert obj['val'] == 1 + + # making sure the async_fn is run + return True + assert asyncio.run(async_fn()) + + +def test_setInterval_unref(): + async def async_fn(): + obj = {'val': 0} + pm.eval("""(obj) => { + setInterval(()=>{ obj.val++ }, 200).unref() + setTimeout(()=>{ }, 500) + }""")(obj) + await pm.wait() # It should stop after the setTimeout timer's 500ms. + assert obj['val'] == 2 # The setInterval timer should only run twice (500 // 200 == 2) + return True + assert asyncio.run(async_fn()) + + +def test_clearInterval(): + async def async_fn(): + obj = {'val': 0} + pm.eval("""(obj) => { + const interval = setInterval(()=>{ obj.val++ }, 200) + setTimeout(()=>{ clearInterval(interval) }, 500) + }""")(obj) + await pm.wait() # It should stop after 500ms on the clearInterval + assert obj['val'] == 2 # The setInterval timer should only run twice (500 // 200 == 2) + return True + assert asyncio.run(async_fn()) + + +def test_finished_timer_ref(): + async def async_fn(): + # Making sure the event-loop won't be activated again when a finished timer gets re-refed. + pm.eval(""" + const timer = setTimeout(()=>{}, 100); + setTimeout(()=>{ timer.ref() }, 200); + """) + await pm.wait() + return True + assert asyncio.run(async_fn()) + + def test_set_clear_timeout(): - # throw RuntimeError outside a coroutine - with pytest.raises(RuntimeError, match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): - pm.eval("setTimeout")(print) - - async def async_fn(): - # standalone `setTimeout` - loop = asyncio.get_running_loop() - f0 = loop.create_future() - def add(a, b, c): - f0.set_result(a + b + c) - pm.eval("setTimeout")(add, 0, 1, 2, 3) - assert 6.0 == await f0 - - # test `clearTimeout` - f1 = loop.create_future() - def to_raise(msg): - f1.set_exception(TypeError(msg)) - timeout_id0 = pm.eval("setTimeout")(to_raise, 100, "going to be there") - assert type(timeout_id0) == float - assert timeout_id0 > 0 # `setTimeout` should return a positive integer value - assert int(timeout_id0) == timeout_id0 - with pytest.raises(TypeError, match="going to be there"): - await f1 # `clearTimeout` not called - f1 = loop.create_future() - timeout_id1 = pm.eval("setTimeout")(to_raise, 100, "shouldn't be here") - pm.eval("clearTimeout")(timeout_id1) - with pytest.raises(asyncio.exceptions.TimeoutError): - await asyncio.wait_for(f1, timeout=0.5) # `clearTimeout` is called - - # `this` value in `setTimeout` callback should be the global object, as spec-ed - assert await pm.eval("new Promise(function (resolve) { setTimeout(function(){ resolve(this == globalThis) }) })") - # `setTimeout` should allow passing additional arguments to the callback, as spec-ed - assert 3.0 == await pm.eval("new Promise((resolve) => setTimeout(function(){ resolve(arguments.length) }, 100, 90, 91, 92))") - assert 92.0 == await pm.eval("new Promise((resolve) => setTimeout((...args) => { resolve(args[2]) }, 100, 90, 91, 92))") - # TODO (Tom Tang): test `setTimeout` setting delay to 0 if < 0 - # TODO (Tom Tang): test `setTimeout` accepting string as the delay, coercing to a number like parseFloat - - # passing an invalid ID to `clearTimeout` should silently do nothing; no exception is thrown. - pm.eval("clearTimeout(NaN)") - pm.eval("clearTimeout(999)") - pm.eval("clearTimeout(-1)") - pm.eval("clearTimeout('a')") - pm.eval("clearTimeout(undefined)") - pm.eval("clearTimeout()") - - # passing a `code` string to `setTimeout` as the callback function - assert "code string" == await pm.eval(""" + # throw RuntimeError outside a coroutine + with pytest.raises(RuntimeError, + match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): + pm.eval("setTimeout")(print) + + async def async_fn(): + # standalone `setTimeout` + loop = asyncio.get_running_loop() + f0 = loop.create_future() + + def add(a, b, c): + f0.set_result(a + b + c) + pm.eval("setTimeout")(add, 0, 1, 2, 3) + assert 6.0 == await f0 + + # test `clearTimeout` + f1 = loop.create_future() + + def to_raise(msg): + f1.set_exception(TypeError(msg)) + timeout_id0 = pm.eval("setTimeout")(to_raise, 100, "going to be there") + # `setTimeout` should return a `Timeout` instance wrapping a positive integer value + assert pm.eval("(t) => t instanceof setTimeout.Timeout")(timeout_id0) + assert pm.eval("(t) => Number(t) > 0")(timeout_id0) + assert pm.eval("(t) => Number.isInteger(Number(t))")(timeout_id0) + with pytest.raises(TypeError, match="going to be there"): + await f1 # `clearTimeout` not called + f1 = loop.create_future() + timeout_id1 = pm.eval("setTimeout")(to_raise, 100, "shouldn't be here") + pm.eval("clearTimeout")(timeout_id1) + with pytest.raises(asyncio.exceptions.TimeoutError): + await asyncio.wait_for(f1, timeout=0.5) # `clearTimeout` is called + + # `this` value in `setTimeout` callback should be the global object, as spec-ed + assert await pm.eval("new Promise(function (resolve) { setTimeout(function(){ resolve(this == globalThis) }) })") + # `setTimeout` should allow passing additional arguments to the callback, as spec-ed + assert 3.0 == await pm.eval(""" + new Promise((resolve) => setTimeout(function(){ + resolve(arguments.length); + }, + 100, 90, 91, 92)) + """) + assert 92.0 == await pm.eval(""" + new Promise((resolve) => setTimeout((...args) => { + resolve(args[2]); + }, + 100, 90, 91, 92)) + """) + # test `setTimeout` setting delay to 0 if < 0 + await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, 0))"), timeout=0.05) + # won't be precisely 0s + await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, -10000))"), timeout=0.05) + # test `setTimeout` accepting string as the delay, coercing to a number. + # Number('100') -> 100, pass if the actual delay is > 90ms and < 150ms + # won't be precisely 100ms + await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, '100'))"), timeout=0.15) + with pytest.raises(asyncio.exceptions.TimeoutError): + await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, '100'))"), timeout=0.09) + # Number("1 second") -> NaN -> delay turns to be 0s + # won't be precisely 0s + await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, '1 second'))"), timeout=0.5) + + # passing an invalid ID to `clearTimeout` should silently do nothing; no exception is thrown. + pm.eval("clearTimeout(NaN)") + pm.eval("clearTimeout(999)") + pm.eval("clearTimeout(-1)") + pm.eval("clearTimeout('a')") + pm.eval("clearTimeout(undefined)") + pm.eval("clearTimeout()") + + # passing a `code` string to `setTimeout` as the callback function + assert "code string" == await pm.eval(""" new Promise((resolve) => { globalThis._resolve = resolve setTimeout("globalThis._resolve('code string'); delete globalThis._resolve", 100) }) """) - # making sure the async_fn is run - return True - assert asyncio.run(async_fn()) + # making sure the async_fn is run + return True + assert asyncio.run(async_fn()) + + # throw RuntimeError outside a coroutine (the event-loop has ended) + with pytest.raises(RuntimeError, + match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): + pm.eval("setTimeout")(print) - # throw RuntimeError outside a coroutine (the event-loop has ended) - with pytest.raises(RuntimeError, match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): - pm.eval("setTimeout")(print) def test_promises(): - # should throw RuntimeError if Promises are created outside a coroutine - create_promise = pm.eval("() => Promise.resolve(1)") - with pytest.raises(RuntimeError, match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): - create_promise() - - async def async_fn(): - create_promise() # inside a coroutine, no error - - # Python awaitables to JS Promise coercion - # 1. Python asyncio.Future to JS promise - loop = asyncio.get_running_loop() - f0 = loop.create_future() - f0.set_result(2561) - assert type(f0) == asyncio.Future - assert 2561 == await f0 - assert pm.eval("(p) => p instanceof Promise")(f0) is True - assert 2561 == await pm.eval("(p) => p")(f0) - del f0 - - # 2. Python asyncio.Task to JS promise - async def coro_fn(x): - await asyncio.sleep(0.01) - return x - task = loop.create_task(coro_fn("from a Task")) - assert type(task) == asyncio.Task - assert type(task) != asyncio.Future - assert isinstance(task, asyncio.Future) - assert "from a Task" == await task - assert pm.eval("(p) => p instanceof Promise")(task) is True - assert "from a Task" == await pm.eval("(p) => p")(task) - del task - - # 3. Python coroutine to JS promise - coro = coro_fn("from a Coroutine") - assert asyncio.iscoroutine(coro) - # assert "a Coroutine" == await coro # coroutines cannot be awaited more than once - # assert pm.eval("(p) => p instanceof Promise")(coro) is True # RuntimeError: cannot reuse already awaited coroutine - assert "from a Coroutine" == await pm.eval("(p) => (p instanceof Promise) && p")(coro) - del coro - - # JS Promise to Python awaitable coercion - assert 100 == await pm.eval("new Promise((r)=>{ r(100) })") - assert 10010 == await pm.eval("Promise.resolve(10010)") - with pytest.raises(pm.SpiderMonkeyError, match="^TypeError: (.|\\n)+ is not a constructor$"): - await pm.eval("Promise.resolve")(10086) - assert 10086 == await pm.eval("Promise.resolve.bind(Promise)")(10086) - - assert "promise returning a function" == (await pm.eval("Promise.resolve(() => { return 'promise returning a function' })"))() - assert "function 2" == (await pm.eval("Promise.resolve(x=>x)"))("function 2") - def aaa(n): - return n - ident0 = await (pm.eval("Promise.resolve.bind(Promise)")(aaa)) - assert "from aaa" == ident0("from aaa") - ident1 = await pm.eval("async (aaa) => x=>aaa(x)")(aaa) - assert "from ident1" == ident1("from ident1") - ident2 = await pm.eval("() => Promise.resolve(x=>x)")() - assert "from ident2" == ident2("from ident2") - ident3 = await pm.eval("(aaa) => Promise.resolve(x=>aaa(x))")(aaa) - assert "from ident3" == ident3("from ident3") - del aaa - - # promise returning a JS Promise that calls a Python function inside - def fn0(n): - return n + 100 - def fn1(): - return pm.eval("async x=>x")(fn0) - fn2 = await pm.eval("async (fn1) => { const fn0 = await fn1(); return Promise.resolve(x=>fn0(x)) }")(fn1) - assert 101.2 == fn2(1.2) - fn3 = await pm.eval("async (fn1) => { const fn0 = await fn1(); return Promise.resolve(async x => { return fn0(x) }) }")(fn1) - assert 101.3 == await fn3(1.3) - fn4 = await pm.eval("async (fn1) => { return Promise.resolve(async x => { const fn0 = await fn1(); return fn0(x) }) }")(fn1) - assert 101.4 == await fn4(1.4) - - # chained JS promises - assert "chained" == await (pm.eval("async () => new Promise((resolve) => resolve( Promise.resolve().then(()=>'chained') ))")()) - - # chained Python awaitables - async def a(): - await asyncio.sleep(0.01) - return "nested" - async def b(): - await asyncio.sleep(0.01) - return a() - async def c(): - await asyncio.sleep(0.01) - return b() - # JS `await` supports chaining. However, on Python-land, it actually requires `await (await (await c()))` - assert "nested" == await pm.eval("async (promise) => await promise")(c()) - assert "nested" == await pm.eval("async (promise) => await promise")(await c()) - assert "nested" == await pm.eval("async (promise) => await promise")(await (await c())) - assert "nested" == await pm.eval("async (promise) => await promise")(await (await (await c()))) - assert "nested" == await pm.eval("async (promise) => promise")(c()) - assert "nested" == await pm.eval("async (promise) => promise")(await c()) - assert "nested" == await pm.eval("async (promise) => promise")(await (await c())) - assert "nested" == await pm.eval("async (promise) => promise")(await (await (await c()))) - assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(c()) - assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(await c()) - assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(await (await c())) - assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(await (await (await c()))) - assert "nested" == await pm.eval("(promise) => promise")(c()) - assert "nested" == await pm.eval("(promise) => promise")(await c()) - assert "nested" == await pm.eval("(promise) => promise")(await (await c())) - with pytest.raises(TypeError, match="object str can't be used in 'await' expression"): - await pm.eval("(promise) => promise")(await (await (await c()))) - - # Python awaitable throwing exceptions - async def coro_to_throw0(): - await asyncio.sleep(0.01) - print([].non_exist) # type: ignore - with pytest.raises(pm.SpiderMonkeyError, match="Python AttributeError: 'list' object has no attribute 'non_exist'"): - await (pm.eval("(promise) => promise")(coro_to_throw0())) - with pytest.raises(pm.SpiderMonkeyError, match="Python AttributeError: 'list' object has no attribute 'non_exist'"): - await (pm.eval("async (promise) => promise")(coro_to_throw0())) - with pytest.raises(pm.SpiderMonkeyError, match="Python AttributeError: 'list' object has no attribute 'non_exist'"): - await (pm.eval("(promise) => Promise.resolve().then(async () => await promise)")(coro_to_throw0())) - async def coro_to_throw1(): - await asyncio.sleep(0.01) - raise TypeError("reason") - with pytest.raises(pm.SpiderMonkeyError, match="Python TypeError: reason"): - await (pm.eval("(promise) => promise")(coro_to_throw1())) - assert 'rejected ' == await pm.eval("(promise) => promise.then(()=>{}, (err)=>`rejected <${err.message}>`)")(coro_to_throw1()) - - # JS Promise throwing exceptions - with pytest.raises(pm.SpiderMonkeyError, match="nan"): - await pm.eval("Promise.reject(NaN)") # JS can throw anything - with pytest.raises(pm.SpiderMonkeyError, match="123.0"): - await (pm.eval("async () => { throw 123 }")()) - # await (pm.eval("async () => { throw {} }")()) - with pytest.raises(pm.SpiderMonkeyError, match="anything"): - await pm.eval("Promise.resolve().then(()=>{ throw 'anything' })") - # FIXME (Tom Tang): We currently handle Promise exceptions by converting the object thrown to a Python Exception object through `pyTypeFactory` - # - # await pm.eval("Promise.resolve().then(()=>{ throw {a:1,toString(){return'anything'}} })") - with pytest.raises(pm.SpiderMonkeyError, match="on line 1:\nTypeError: undefined has no properties"): # not going through the conversion - await pm.eval("Promise.resolve().then(()=>{ (undefined).prop })") - - # TODO (Tom Tang): Modify this testcase once we support ES2020-style dynamic import - # pm.eval("import('some_module')") # dynamic import returns a Promise, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import - with pytest.raises(pm.SpiderMonkeyError, match="\nError: Dynamic module import is disabled or not supported in this context"): - await pm.eval("import('some_module')") - # TODO (Tom Tang): properly test unhandled rejection - - # await scheduled jobs on the Python event-loop - js_sleep = pm.eval("(second) => new Promise((resolve) => setTimeout(resolve, second*1000))") - def py_sleep(second): # asyncio.sleep has issues on Python 3.8 - loop = asyncio.get_running_loop() - future = loop.create_future() - loop.call_later(second, lambda:future.set_result(None)) - return future - both_sleep = pm.eval("(js_sleep, py_sleep) => async (second) => { await js_sleep(second); await py_sleep(second) }")(js_sleep, py_sleep) - await asyncio.wait_for(both_sleep(0.1), timeout=0.3) # won't be precisely 0.2s - with pytest.raises(asyncio.exceptions.TimeoutError): - await asyncio.wait_for(both_sleep(0.1), timeout=0.19) - - # making sure the async_fn is run - return True - assert asyncio.run(async_fn()) - - # should throw a RuntimeError if created outside a coroutine (the event-loop has ended) - with pytest.raises(RuntimeError, match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): - pm.eval("new Promise(() => { })") - -# off-thread promises + # should throw RuntimeError if Promises are created outside a coroutine + create_promise = pm.eval("() => Promise.resolve(1)") + with pytest.raises(RuntimeError, + match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): + create_promise() + + async def async_fn(): + create_promise() # inside a coroutine, no error + + # Python awaitables to JS Promise coercion + # 1. Python asyncio.Future to JS promise + loop = asyncio.get_running_loop() + f0 = loop.create_future() + f0.set_result(2561) + + assert type(f0) is asyncio.Future + assert 2561 == await f0 + assert pm.eval("(p) => p instanceof Promise")(f0) is True + assert 2561 == await pm.eval("(p) => p")(f0) + del f0 + + # 2. Python asyncio.Task to JS promise + async def coro_fn(x): + await asyncio.sleep(0.01) + return x + task = loop.create_task(coro_fn("from a Task")) + assert type(task) is asyncio.Task + assert type(task) is not asyncio.Future + assert isinstance(task, asyncio.Future) + assert "from a Task" == await task + assert pm.eval("(p) => p instanceof Promise")(task) is True + assert "from a Task" == await pm.eval("(p) => p")(task) + del task + + # 3. Python coroutine to JS promise + coro = coro_fn("from a Coroutine") + assert asyncio.iscoroutine(coro) + # assert "a Coroutine" == await coro # coroutines cannot be awaited more than once + # assert pm.eval("(p) => p instanceof Promise")(coro) # RuntimeError: cannot reuse already awaited coroutine + assert "from a Coroutine" == await pm.eval("(p) => (p instanceof Promise) && p")(coro) + del coro + + # JS Promise to Python awaitable coercion + assert 100 == await pm.eval("new Promise((r)=>{ r(100) })") + assert 10010 == await pm.eval("Promise.resolve(10010)") + with pytest.raises(pm.SpiderMonkeyError, match="^TypeError: (.|\\n)+ is not a constructor$"): + await pm.eval("Promise.resolve")(10086) + assert 10086 == await pm.eval("Promise.resolve.bind(Promise)")(10086) + + assert "promise returning a function" == (await pm.eval(""" + Promise.resolve(() => { + return 'promise returning a function'; + }); + """))() + assert "function 2" == (await pm.eval("Promise.resolve(x=>x)"))("function 2") + + def aaa(n): + return n + ident0 = await (pm.eval("Promise.resolve.bind(Promise)")(aaa)) + assert "from aaa" == ident0("from aaa") + ident1 = await pm.eval("async (aaa) => x=>aaa(x)")(aaa) + assert "from ident1" == ident1("from ident1") + ident2 = await pm.eval("() => Promise.resolve(x=>x)")() + assert "from ident2" == ident2("from ident2") + ident3 = await pm.eval("(aaa) => Promise.resolve(x=>aaa(x))")(aaa) + assert "from ident3" == ident3("from ident3") + del aaa + + # promise returning a JS Promise that calls a Python function inside + def fn0(n): + return n + 100 + + def fn1(): + return pm.eval("async x=>x")(fn0) + fn2 = await pm.eval("async (fn1) => { const fn0 = await fn1(); return Promise.resolve(x=>fn0(x)) }")(fn1) + assert 101.2 == fn2(1.2) + fn3 = await pm.eval(""" + async (fn1) => { + const fn0 = await fn1(); + return Promise.resolve(async x => { return fn0(x); }); + } + """)(fn1) + assert 101.3 == await fn3(1.3) + fn4 = await pm.eval(""" + async (fn1) => { + return Promise.resolve(async x => { + const fn0 = await fn1(); + return fn0(x); + }); + } + """)(fn1) + assert 101.4 == await fn4(1.4) + + # chained JS promises + assert "chained" == await pm.eval(""" + async () => new Promise((resolve) => resolve( Promise.resolve().then(()=>'chained') )) + """)() + + # chained Python awaitables + async def a(): + await asyncio.sleep(0.01) + return "nested" + + async def b(): + await asyncio.sleep(0.01) + return a() + + async def c(): + await asyncio.sleep(0.01) + return b() + # JS `await` supports chaining. However, on Python-land, it actually requires `await (await (await c()))` + assert "nested" == await pm.eval("async (promise) => await promise")(c()) + assert "nested" == await pm.eval("async (promise) => await promise")(await c()) + assert "nested" == await pm.eval("async (promise) => await promise")(await (await c())) + assert "nested" == await pm.eval("async (promise) => await promise")(await (await (await c()))) + assert "nested" == await pm.eval("async (promise) => promise")(c()) + assert "nested" == await pm.eval("async (promise) => promise")(await c()) + assert "nested" == await pm.eval("async (promise) => promise")(await (await c())) + assert "nested" == await pm.eval("async (promise) => promise")(await (await (await c()))) + assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(c()) + assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(await c()) + assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(await (await c())) + assert "nested" == await pm.eval("(promise) => Promise.resolve(promise)")(await (await (await c()))) + assert "nested" == await pm.eval("(promise) => promise")(c()) + assert "nested" == await pm.eval("(promise) => promise")(await c()) + assert "nested" == await pm.eval("(promise) => promise")(await (await c())) + with pytest.raises(TypeError, match="(object str can't be used in 'await' expression)|('str' object can't be awaited)"): + # Python 3.14 changed the error message format + await pm.eval("(promise) => promise")(await (await (await c()))) + + # Python awaitable throwing exceptions + async def coro_to_throw0(): + await asyncio.sleep(0.01) + print([].non_exist) # type: ignore + with pytest.raises(pm.SpiderMonkeyError, match="Python AttributeError: 'list' object has no attribute 'non_exist'"): + await (pm.eval("(promise) => promise")(coro_to_throw0())) + with pytest.raises(pm.SpiderMonkeyError, match="Python AttributeError: 'list' object has no attribute 'non_exist'"): + await (pm.eval("async (promise) => promise")(coro_to_throw0())) + with pytest.raises(pm.SpiderMonkeyError, match="Python AttributeError: 'list' object has no attribute 'non_exist'"): + await (pm.eval("(promise) => Promise.resolve().then(async () => await promise)")(coro_to_throw0())) + + async def coro_to_throw1(): + await asyncio.sleep(0.01) + raise TypeError("reason") + with pytest.raises(pm.SpiderMonkeyError, match="Python TypeError: reason"): + await (pm.eval("(promise) => promise")(coro_to_throw1())) + assert 'rejected ' == await pm.eval(""" + (promise) => promise.then( + ()=>{}, + (err)=>`rejected <${err.message}>` + ) + """)(coro_to_throw1()) + + # JS Promise throwing exceptions + with pytest.raises(pm.SpiderMonkeyError, match="nan"): + await pm.eval("Promise.reject(NaN)") # JS can throw anything + with pytest.raises(pm.SpiderMonkeyError, match="123.0"): + await (pm.eval("async () => { throw 123 }")()) + # await (pm.eval("async () => { throw {} }")()) + with pytest.raises(pm.SpiderMonkeyError, match="anything"): + await pm.eval("Promise.resolve().then(()=>{ throw 'anything' })") + # FIXME (Tom Tang): We currently handle Promise exceptions by converting the object thrown to a Python Exception + # object through `pyTypeFactory + # + # await pm.eval("Promise.resolve().then(()=>{ throw {a:1,toString(){return'anything'}} })") + # not going through the conversion + with pytest.raises(pm.SpiderMonkeyError, match="on line 1, column 31:\nTypeError: can\'t access property \"prop\" of undefined"): + await pm.eval("Promise.resolve().then(()=>{ (undefined).prop })") + + # TODO (Tom Tang): Modify this testcase once we support ES2020-style dynamic import + # pm.eval("import('some_module')") # dynamic import returns a Promise, see + # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import + with pytest.raises(pm.SpiderMonkeyError, + match="\nError: Dynamic module import is disabled or not supported in this context"): + await pm.eval("import('some_module')") + # TODO (Tom Tang): properly test unhandled rejection + + # await scheduled jobs on the Python event-loop + js_sleep = pm.eval("(seconds) => new Promise((resolve) => setTimeout(resolve, seconds*1000))") + + def py_sleep(seconds): # asyncio.sleep has issues on Python 3.8 + loop = asyncio.get_running_loop() + future = loop.create_future() + loop.call_later(seconds, lambda: future.set_result(None)) + return future + both_sleep = pm.eval(""" + (js_sleep, py_sleep) => async (seconds) => { + await js_sleep(seconds); + await py_sleep(seconds); + } + """)(js_sleep, py_sleep) + await asyncio.wait_for(both_sleep(0.1), timeout=0.3) # won't be precisely 0.2s + with pytest.raises(asyncio.exceptions.TimeoutError): + await asyncio.wait_for(both_sleep(0.1), timeout=0.19) + + # making sure the async_fn is run + return True + assert asyncio.run(async_fn()) + + # should throw a RuntimeError if created outside a coroutine (the event-loop has ended) + with pytest.raises(RuntimeError, + match="PythonMonkey cannot find a running Python event-loop to make asynchronous calls."): + pm.eval("new Promise(() => { })") + + +def test_errors_thrown_in_promise(): + async def async_fn(): + loop = asyncio.get_running_loop() + future = loop.create_future() + + def exceptionHandler(loop, context): + future.set_exception(context["exception"]) + loop.set_exception_handler(exceptionHandler) + + pm.eval(""" + new Promise(function (resolve, reject) + { + reject(new Error('in Promise')); + }); + + new Promise(function (resolve, reject) + { + console.log('ok'); + }); + """) + with pytest.raises(pm.SpiderMonkeyError, match="Error: in Promise"): + await asyncio.wait_for(future, timeout=0.1) + + loop.set_exception_handler(None) + return True + assert asyncio.run(async_fn()) + + +def test_errors_thrown_in_async_function(): + async def async_fn(): + loop = asyncio.get_running_loop() + future = loop.create_future() + + def exceptionHandler(loop, context): + future.set_exception(context["exception"]) + loop.set_exception_handler(exceptionHandler) + + pm.eval(""" + async function aba() { + throw new Error('in async function'); + } + + async function abb() { + console.log('ok'); + } + + aba(); + abb(); + """) + with pytest.raises(pm.SpiderMonkeyError, match="Error: in async function"): + await asyncio.wait_for(future, timeout=0.1) + + loop.set_exception_handler(None) + return True + assert asyncio.run(async_fn()) + + def test_webassembly(): - async def async_fn(): - # off-thread promises can run - assert 'instantiated' == await pm.eval(""" + """ + Tests for off-thread promises + """ + async def async_fn(): + # off-thread promises can run + assert 'instantiated' == await pm.eval(""" // https://github.com/mdn/webassembly-examples/blob/main/js-api-examples/simple.wasm var code = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, @@ -246,6 +430,6 @@ async def async_fn(): WebAssembly.instantiate(code, { imports: { imported_func() {} } }).then(() => 'instantiated') """) - # making sure the async_fn is run - return True - assert asyncio.run(async_fn()) + # making sure the async_fn is run + return True + assert asyncio.run(async_fn()) diff --git a/tests/python/test_finalizationregistry.py b/tests/python/test_finalizationregistry.py new file mode 100644 index 00000000..26b0a0b6 --- /dev/null +++ b/tests/python/test_finalizationregistry.py @@ -0,0 +1,23 @@ +import pythonmonkey as pm + + +def test_finalizationregistry(): + result = pm.eval(""" + (collect) => { + let arr = [42, 43]; + const registry = new FinalizationRegistry(heldValue => { arr[heldValue] = heldValue; }); + let obj1 = {}; + let obj2 = {}; + registry.register(obj1, 0); + registry.register(obj2, 1); + obj1 = null; + obj2 = null; + + collect(); + + return arr; + } + """)(pm.collect) + + assert result[0] == 0 + assert result[1] == 1 diff --git a/tests/python/test_functions.py b/tests/python/test_functions.py new file mode 100644 index 00000000..5b61acb8 --- /dev/null +++ b/tests/python/test_functions.py @@ -0,0 +1,121 @@ +import pythonmonkey as pm + + +def test_func_no_args(): + def f(): + return 42 + assert 42 == pm.eval("(f) => f()")(f) + + +def test_func_too_many_args(): + def f(a, b): + return [a, b] + assert [1, 2] == pm.eval("(f) => f(1, 2, 3)")(f) + + +def test_func_equal_args(): + def f(a, b): + return [a, b] + assert [1, 2] == pm.eval("(f) => f(1, 2)")(f) + + +def test_func_too_few_args(): + def f(a, b): + return [a, b] + assert [1, None] == pm.eval("(f) => f(1)")(f) + + +def test_default_func_no_args(): + def f(a, b, c=42, d=43): + return [a, b, c, d] + assert [None, None, 42, 43] == pm.eval("(f) => f()")(f) + + +def test_default_func_too_many_default_args(): + def f(a, b, c=42, d=43): + return [a, b, c, d] + assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4, 5)")(f) + + +def test_default_func_equal_default_args(): + def f(a, b, c=42, d=43): + return [a, b, c, d] + assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4)")(f) + + +def test_default_func_too_few_default_args(): + def f(a, b, c=42, d=43): + return [a, b, c, d] + assert [1, 2, 3, 43] == pm.eval("(f) => f(1, 2, 3)")(f) + + +def test_default_func_equal_args(): + def f(a, b, c=42, d=43): + return [a, b, c, d] + assert [1, 2, 42, 43] == pm.eval("(f) => f(1, 2)")(f) + + +def test_default_func_too_few_args(): + def f(a, b, c=42, d=43): + return [a, b, c, d] + assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f) + + +def test_vararg_func_no_args(): + def f(a, b, *args): + return [a, b, *args] + assert [None, None] == pm.eval("(f) => f()")(f) + + +def test_vararg_func_too_many_args(): + def f(a, b, *args): + return [a, b, *args] + assert [1, 2, 3] == pm.eval("(f) => f(1, 2, 3)")(f) + + +def test_vararg_func_equal_args(): + def f(a, b, *args): + return [a, b, *args] + assert [1, 2] == pm.eval("(f) => f(1, 2)")(f) + + +def test_vararg_func_too_few_args(): + def f(a, b, *args): + return [a, b, *args] + assert [1, None] == pm.eval("(f) => f(1)")(f) + + +def test_default_vararg_func_no_args(): + def f(a, b, c=42, d=43, *args): + return [a, b, c, d, *args] + assert [None, None, 42, 43] == pm.eval("(f) => f()")(f) + + +def test_default_vararg_func_too_many_default_args(): + def f(a, b, c=42, d=43, *args): + return [a, b, c, d, *args] + assert [1, 2, 3, 4, 5] == pm.eval("(f) => f(1, 2, 3, 4, 5)")(f) + + +def test_default_vararg_func_equal_default_args(): + def f(a, b, c=42, d=43, *args): + return [a, b, c, d, *args] + assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4)")(f) + + +def test_default_vararg_func_too_few_default_args(): + def f(a, b, c=42, d=43, *args): + return [a, b, c, d, *args] + assert [1, 2, 3, 43] == pm.eval("(f) => f(1, 2, 3)")(f) + + +def test_default_vararg_func_equal_args(): + def f(a, b, c=42, d=43, *args): + return [a, b, c, d, *args] + assert [1, 2, 42, 43] == pm.eval("(f) => f(1, 2)")(f) + + +def test_default_vararg_func_too_few_args(): + def f(a, b, c=42, d=43, *args): + return [a, b, c, d, *args] + assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f) diff --git a/tests/python/test_functions_this.py b/tests/python/test_functions_this.py new file mode 100644 index 00000000..52838781 --- /dev/null +++ b/tests/python/test_functions_this.py @@ -0,0 +1,210 @@ +import pythonmonkey as pm +import subprocess +import weakref +import sys + + +def test_python_functions_self(): + def pyFunc(param): + return param + + assert 1 == pyFunc(1) + assert 2 == pm.eval("""(pyFunc) => { + return pyFunc(2); + } + """)(pyFunc) + assert 3 == pm.eval("""(pyFunc) => { + let jsObj = {}; + jsObj.pyFunc = pyFunc; + return pyFunc(3); + } + """)(pyFunc) + + +def test_python_methods_self(): + def pyFunc(self, param): + return [self, param] + + class Class: + pass + Class.pyMethod = pyFunc + + pyObj = Class() + result = pyObj.pyMethod(1) + assert pyObj == result[0] and 1 == result[1] + pyMethod = pyObj.pyMethod + result = pyMethod(2) + assert pyObj == result[0] and 2 == result[1] + result = pm.eval("""(pyObj) => { + return pyObj.pyMethod(3); + } + """)(pyObj) + assert pyObj == result[0] and 3 == result[1] + result = pm.eval("""(pyObj) => { + let jsObj = {}; + jsObj.pyMethod = pyObj.pyMethod; + return jsObj.pyMethod(4); + } + """)(pyObj) + assert pyObj == result[0] and 4 == result[1] # pyMethod is bound to pyObj, so `self` is not `jsObj` + result = pm.eval("""(pyObj) => { + let pyMethod = pyObj.pyMethod; + return pyMethod(5); + } + """)(pyObj) + assert pyObj == result[0] and 5 == result[1] # pyMethod is bound to pyObj, so `self` is not `globalThis` + + +def test_javascript_functions_this(): + jsFunc = pm.eval(""" + (function(param) { + return [this, param]; + }) + """) + + class Class: + pass + Class.jsFunc = jsFunc # jsFunc is not bound to Class, so `this` will be `globalThis`, not the object + + pyObj = Class() + jsObj = pm.eval("({})") + jsObj.jsFunc = jsFunc + globalThis = pm.eval("globalThis") + result = jsFunc(1) + assert globalThis == result[0] and 1 == result[1] + result = pyObj.jsFunc(2) + assert globalThis == result[0] and 2 == result[1] + result = jsObj.jsFunc(3) + assert jsObj == result[0] and 3 == result[1] + result = pm.eval("""(jsFunc) => { + return jsFunc(4); + } + """)(jsFunc) + assert globalThis == result[0] and 4 == result[1] + result = pm.eval("""(pyObj) => { + return pyObj.jsFunc(5); + } + """)(pyObj) + assert pyObj == result[0] and 5 == result[1] + result = pm.eval("""(jsObj) => { + return jsObj.jsFunc(6); + } + """)(jsObj) + assert jsObj == result[0] and 6 == result[1] + + +def test_JSMethodProxy_this(): + jsFunc = pm.eval(""" + (function(param) { + return [this, param]; + }) + """) + + class Class: + pass + + pyObj = Class() + pyObj.jsMethod = pm.JSMethodProxy(jsFunc, pyObj) # jsMethod is bound to pyObj, so `this` will always be `pyObj` + jsObj = pm.eval("({})") + jsObj.jsMethod = pyObj.jsMethod + globalThis = pm.eval("globalThis") + result = pyObj.jsMethod(1) + assert pyObj == result[0] and 1 == result[1] + result = jsObj.jsMethod(2) + assert pyObj == result[0] and 2 == result[1] + result = pm.eval("""(pyObj) => { + return pyObj.jsMethod(3); + } + """)(pyObj) + assert pyObj == result[0] and 3 == result[1] + result = pm.eval("""(jsObj) => { + return jsObj.jsMethod(4); + } + """)(jsObj) + assert pyObj == result[0] and 4 == result[1] + +# require + + +def test_require_correct_this_old_style_class(): + example = pm.eval(""" + () => { + // old style class + function Rectangle(w, h) { + this.w = w; + this.h = h; + } + + Rectangle.prototype = { + getThis: function () { + return this; + }, + getArea: function () { + return this.w * this.h; + }, + }; + + // es5 class + class Rectangle2 { + constructor(w,h) { + this.w = w; + this.h = h; + } + + getThis() { + return this; + } + + getArea() { + return this.w * this.h; + } + } + + return { Rectangle: Rectangle, Rectangle2: Rectangle2}; + } + """)() + r = pm.new(example.Rectangle)(1, 2) + + assert r.getArea() == 2 + assert r.getThis() == r + + r2 = pm.new(example.Rectangle2)(1, 2) + + assert r2.getArea() == 2 + assert r2.getThis() == r2 + + +def test_function_finalization(): + ref = [] + starting_ref_count = [] + + def outerScope(): + def pyFunc(): + return 42 + + ref.append(weakref.ref(pyFunc)) + starting_ref_count.append(sys.getrefcount(pyFunc)) + assert 42 == pm.eval("(func) => func()")(pyFunc) + + assert ref[0]() is pyFunc + current_ref_count = sys.getrefcount(pyFunc) + assert current_ref_count == starting_ref_count[0] + 1 + + outerScope() + pm.collect() # this should collect the JS proxy to pyFunc, which should decref pyFunc + # pyFunc should be collected by now + assert ref[0]() is None + + +def test_method_no_self(): + class What: + def some_method(): + return 3 + + obj = What() + + try: + pm.eval('x => x.some_method()')(obj) + assert (False) + except pm.SpiderMonkeyError as e: + assert 'takes 0 positional arguments but 1 was given' in str(e) diff --git a/tests/python/test_list_array.py b/tests/python/test_list_array.py new file mode 100644 index 00000000..8adb4f3d --- /dev/null +++ b/tests/python/test_list_array.py @@ -0,0 +1,47 @@ +import pythonmonkey as pm + + +def test_eval_array_is_list(): + pythonList = pm.eval('[]') + assert isinstance(pythonList, list) + +# extra nice but not necessary + + +def test_eval_array_is_list_type_string(): + pythonListTypeString = str(type(pm.eval('[]'))) + assert pythonListTypeString == "" + + +def test_eval_list_is_array(): + items = [1, 2, 3] + isArray = pm.eval('Array.isArray')(items) + assert isArray + + +def test_typeof_array(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = typeof arr}")(result, items) + assert result[0] == 'object' + + +def test_instanceof_array(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr instanceof Array}")(result, items) + assert result[0] + + +def test_instanceof_object(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr instanceof Object}")(result, items) + assert result[0] + + +def test_not_instanceof_string(): + items = [1, 2, 3] + result = [None] + pm.eval("(result, arr) => {result[0] = arr instanceof String}")(result, items) + assert not result[0] diff --git a/tests/python/test_lists.py b/tests/python/test_lists.py new file mode 100644 index 00000000..475f873b --- /dev/null +++ b/tests/python/test_lists.py @@ -0,0 +1,1246 @@ +import pythonmonkey as pm +from functools import reduce + + +def test_eval_lists(): + d = [1] + proxy_d = pm.eval("(list) => { return list; }")(d) + assert d is proxy_d + + +def test_equal_lists_left_literal_right(): + pyArray = pm.eval("Array(1,2)") + assert pyArray == [1, 2] + + +def test_equal_no_object_lists_left_literal_right(): + pyArray = pm.eval("[1,2]") + assert pyArray == [1, 2] + + +def test_equal_lists_right_literal_left(): + pyArray = pm.eval("Array(1,2)") + assert [1, 2] == pyArray + + +def test_equal_lists_left_list_right(): + pyArray = pm.eval("Array(1,2)") + b = [1, 2] + assert pyArray == b + + +def test_equal_lists_right_literal_left(): + pyArray = pm.eval("Array(1,2)") + b = [1, 2] + assert b == pyArray + + +def test_not_equal_lists_left_literal_right(): + pyArray = pm.eval("Array(1,2)") + assert pyArray != [1, 3] + + +def test_smaller_lists_left_literal_right(): + pyArray = pm.eval("Array(1,2)") + assert pyArray < [1, 3] + + +def test_equal_sublist(): + pyArray = pm.eval("Array(1,2,[3,4])") + assert pyArray == [1, 2, [3, 4]] + + +def test_not_equal_sublist_right_not_sublist(): + pyArray = pm.eval("Array(1,2,[3,4])") + assert pyArray != [1, 2, 3, 4] + + +def test_equal_self(): + a = pm.eval("([1,2,3,4])") + b = a + assert b == a + + +def test_equal_other_instance(): + a = pm.eval("([1,2,3,4])") + b = [1, 2, 3, 4] + assert b == a + + +def test_not_equal_size(): + a = pm.eval("([1,2,3,4])") + b = [1, 2] + assert b != a + + +def test_equal_other_js_instance(): + a = pm.eval("([1,2,3,4])") + b = pm.eval("([1,2,3,4])") + assert b == a + + +def test_not_equal_other_js_instance(): + a = pm.eval("([1,2,3,4])") + b = pm.eval("([1,2,4,3])") + assert b != a + + +def test_not_smaller_self(): + a = pm.eval("([1,2,3,4])") + b = a + assert not (b < a) + + +def test_not_smaller_equal_self(): + a = pm.eval("([1,2,3,4])") + b = a + assert b <= a + + +def test_eval_arrays_proxy_get(): + f = pm.eval("(arr) => { return arr[0]}") + assert f([1, 2]) == 1.0 + + +def test_eval_arrays_proxy_set(): + f = pm.eval("(arr) => { arr[0] = 42.0; return;}") + pyObj = [1] + f(pyObj) + assert pyObj[0] == 42.0 + +# get + + +def test_get(): + pyArray = pm.eval("[1,2]") + assert pyArray[0] == 1 + + +def test_get_negative_index(): + pyArray = pm.eval("[1,2]") + assert pyArray[-1] == 2 + + +def test_get_index_out_of_range(): + pyArray = pm.eval("[1,2]") + try: + pyArray[3] + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'list index out of range' + + +def test_get_index_wrong_type_str(): + pyArray = pm.eval("[1,2]") + try: + pyArray["g"] + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'list indices must be integers or slices, not str' + + +def test_get_index_wrong_type_list(): + pyArray = pm.eval("[1,2]") + try: + pyArray[[2]] + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'list indices must be integers or slices, not list' + + +def test_delete(): + pyArray = pm.eval("[1,2]") + del pyArray[0] + assert pyArray == [None, 2] + + +# assign +def test_assign(): + pyArray = pm.eval("[1,2]") + pyArray[0] = 6 + assert pyArray[0] == 6 + + +def test_assign_negative_index(): + pyArray = pm.eval("[1,2]") + pyArray[-1] = 6 + assert pyArray[1] == 6 + + +def test_assign_index_out_of_range(): + pyArray = pm.eval("[1,2]") + try: + pyArray[3] = 8 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'list assignment index out of range' + + +def test_assign_index_wrong_type_str(): + pyArray = pm.eval("[1,2]") + try: + pyArray["g"] = 4 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'list indices must be integers or slices, not str' + + +def test_assign_index_wrong_type_list(): + pyArray = pm.eval("[1,2]") + try: + pyArray[[2]] = 9 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'list indices must be integers or slices, not list' + + +# repr +def test_repr_empty_array(): + pyArray = pm.eval("[]") + expected = "[]" + assert repr(pyArray) == expected + assert str(pyArray) == expected + + +def test_repr_non_empty_array(): + pyArray = pm.eval("[1,2]") + expected = "[1.0, 2.0]" + assert repr(pyArray) == expected + assert str(pyArray) == expected + + +def test_repr_recursion(): + pyArray = pm.eval(""" + () => { + let arr = [1,2]; + arr.push(arr); + return arr; + } + """)() + expected = "[1.0, 2.0, [...]]" + assert repr(pyArray) == expected + assert str(pyArray) == expected + +# concat + + +def test_concat_wrong_type(): + likes = pm.eval('([1,2])') + try: + likes + 3 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == 'can only concatenate list (not "int") to list' + + +def test_concat_empty_empty(): + pyArray = pm.eval("[]") + pyArray + [] + assert pyArray == [] + + +def test_concat_not_empty_empty(): + pyArray = pm.eval("[1,2]") + pyArray + [] + assert pyArray == [1, 2] + + +def test_concat_not_in_place(): + pyArray = pm.eval("[1,2]") + b = pyArray + [3, 4] + assert pyArray == [1, 2] + +# TODO +# def test_concat_array_right(): +# pyArray = pm.eval("[1,2]") +# b = [3,4] + pyArray +# assert b == [1,2,3,4] + + +def test_concat_pm_array_right(): + a = pm.eval("[1,2]") + b = pm.eval("[3,4]") + c = a + b + assert c == [1, 2, 3, 4] + +# repeat + + +def test_repeat_negative(): + a = pm.eval("[1,2]") + b = a * -1 + assert b == [] + + +def test_repeat_zero(): + a = pm.eval("[1,2]") + b = a * 0 + assert b == [] + + +def test_repeat_once(): + a = pm.eval("[1,2]") + b = a * 1 + assert b == [1, 2] + assert a is not b + + +def test_repeat_twice(): + a = pm.eval("[1,2]") + b = a * 2 + assert b == [1, 2, 1, 2] + + +def test_repeat_wrong_type(): + a = pm.eval('([1,2])') + try: + a * 1.0 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "can't multiply sequence by non-int of type 'float'" + + +def test_repeat_not_in_place(): + a = pm.eval("[1,2]") + b = a * 3 + assert b == [1.0, 2.0, 1.0, 2.0, 1.0, 2.0] + assert a == [1, 2] + +# contains + + +def test_contains_found(): + a = pm.eval("[1,2]") + assert 1 in a + + +def test_contains_not_found(): + a = pm.eval("[1,2]") + assert 7 not in a + + +def test_contains_not_found_odd_type(): + a = pm.eval("[1,2]") + assert [1] not in a + +# concat in place += + + +def test_concat_is_inplace(): + pyArray = pm.eval("[1,2]") + pyArray += [3] + assert pyArray == [1, 2, 3] + + +def test_concat_in_place_empty_empty(): + pyArray = pm.eval("[]") + pyArray += [] + assert pyArray == [] + + +def test_concat_not_empty_empty(): + pyArray = pm.eval("[1,2]") + pyArray += [] + assert pyArray == [1, 2] + +# repeat in place *= + + +def test_repeat_is_in_place(): + a = pm.eval("[1,2]") + a *= 3 + assert a == [1, 2, 1, 2, 1, 2] + + +def test_repeat_negative(): + a = pm.eval("[1,2]") + a *= -1 + assert a == [] + + +def test_repeat_zero(): + a = pm.eval("[1,2]") + a *= 0 + assert a == [] + + +def test_repeat_twice(): + a = pm.eval("[1,2]") + a *= 2 + assert a == [1, 2, 1, 2] + + +def test_repeat_wrong_type(): + a = pm.eval('([1,2])') + try: + a *= 1.7 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "can't multiply sequence by non-int of type 'float'" + + +# clear +def test_clear(): + a = pm.eval("[1,2]") + a.clear() + assert a == [] + + +def test_clear_with_arg(): + a = pm.eval('([1,2])') + try: + a.clear(8) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__('clear() takes no arguments (1 given)') + +# copy + + +def test_copy(): + a = pm.eval("[1,2]") + b = a.copy() + b[0] = 8 + assert a[0] == 1 + assert b[0] == 8 + + +def test_copy_with_arg(): + a = pm.eval('([1,2])') + try: + b = a.copy(8) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__('copy() takes no arguments (1 given)') + +# append + + +def test_append(): + a = pm.eval("[1,2]") + b = a.append(3) + assert a == [1, 2, 3] + assert b is None + +# insert + + +def test_insert_list(): + a = pm.eval("[1,2]") + a.insert(0, [3, 4]) + assert a == [[3, 4], 1, 2] + + +def test_insert_boolean(): + a = pm.eval("[1,2]") + a.insert(0, True) + assert a == [True, 1, 2] + + +def test_insert_int(): + a = pm.eval("[1,2]") + a.insert(0, 3) + assert a == [3, 1, 2] + + +def test_insert_float(): + a = pm.eval("[1,2]") + a.insert(0, 4.5) + assert a == [4.5, 1, 2] + + +def test_insert_string(): + a = pm.eval("[1,2]") + a.insert(0, "Hey") + assert a == ["Hey", 1, 2] + + +def test_insert_none(): + a = pm.eval("[1,2]") + a.insert(0, None) + assert a == [None, 1, 2] + + +def test_insert_function(): + def f(): + return + a = pm.eval("[1,2]") + a.insert(0, f) + assert len(a) == 3 + + +def test_insert_int_neg_index(): + a = pm.eval("[1,2]") + a.insert(-1, 3) + assert a == [1, 3, 2] + + +def test_insert_int_too_large_index(): + a = pm.eval("[1,2]") + a.insert(5, 3) + assert a == [1, 2, 3] + + +def test_insert_int_too_many_args(): + a = pm.eval('([1,2])') + try: + a.insert(5, 6, 7) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "insert expected 2 arguments, got 3" + + +def test_insert_int_too_few_args(): + a = pm.eval('([1,2])') + try: + a.insert() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "insert expected 2 arguments, got 0" + +# extend + + +def test_extend_with_list(): + a = pm.eval("[1,2]") + a.extend([3, 4]) + assert a == [1, 2, 3, 4] + + +def test_extend_with_dict_single(): + a = pm.eval("[1,2]") + a.extend({'key': 5}) + assert a == [1, 2, 'key'] + + +def test_extend_with_dict_double(): + a = pm.eval("[1,2]") + a.extend({'key': 5, 'otherKey': 6}) + assert a == [1, 2, 'key', 'otherKey'] + + +def test_extend_with_number(): + a = pm.eval('([1,2])') + try: + a.extend(3) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "'int' object is not iterable" + + +def test_extend_with_own_list(): + a = pm.eval("[1,2]") + a.extend(a) + assert a == [1, 2, 1, 2] + + +def test_extend_with_pm_list(): + a = pm.eval("[1,2]") + b = pm.eval("[3,4]") + a.extend(b) + assert a == [1, 2, 3, 4] + +# TODO iterable dict + + +def test_extend_with_own_dict(): + a = pm.eval("[1,2]") + b = pm.eval("({'key':5, 'key2':6})") + a.extend(b) + assert a == [1, 2, "key", "key2"] + +# pop + + +def test_pop_no_arg(): + a = pm.eval("[1,2]") + b = a.pop() + assert a == [1] + assert b == 2 + + +def test_pop_empty_list(): + a = pm.eval('([])') + try: + a.pop() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "pop from empty list" + + +def test_pop_list_no_arg(): + a = pm.eval("[1,[2,3]]") + b = a.pop() + assert a == [1] + assert b == [2, 3] + + +def test_pop_first_item(): + a = pm.eval("[1,2]") + b = a.pop(0) + assert a == [2] + assert b == 1 + + +def test_pop_second_item(): + a = pm.eval("[1,2]") + b = a.pop(1) + assert a == [1] + assert b == 2 + + +def test_pop_negative_item(): + a = pm.eval("[1,2]") + b = a.pop(-1) + assert a == [1] + assert b == 2 + + +def test_pop_out_of_bounds_item(): + a = pm.eval('([1,2])') + try: + a.pop(3) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "pop index out of range" + +# remove + + +def test_remove_found_once(): + a = pm.eval("[1,2]") + a.remove(1) + assert a == [2] + + +def test_remove_found_twice(): + a = pm.eval("[1,2,1,2]") + a.remove(1) + assert a == [2, 1, 2] + + +def test_remove_no_args(): + a = pm.eval('([1,2])') + try: + a.remove() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__('remove() takes exactly one argument (0 given)') + + +def test_remove_not_found(): + a = pm.eval('([1,2])') + try: + a.remove(3) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "list.remove(x): x not in list" + +# index + + +def test_index_found(): + a = pm.eval("[1,2,3,4]") + b = a.index(3) + assert b == 2 + + +def test_index_not_found(): + a = pm.eval('([1,2])') + try: + a.index(3) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "3 is not in list" + + +def test_index_no_args(): + a = pm.eval('([1,2,3,4])') + try: + a.index() + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "index expected at least 1 argument, got 0" + + +def test_index_too_many_args(): + a = pm.eval('([1,2,3,4])') + try: + a.index(2, 3, 4, 5) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "index expected at most 3 arguments, got 4" + + +def test_index_found_with_start(): + a = pm.eval("[1,2,3,4]") + b = a.index(3, 0) + assert b == 2 + + +def test_index_found_with_negative_start(): + a = pm.eval("[1,2,3,4]") + b = a.index(3, -3) + assert b == 2 + + +def test_index_not_found_with_start(): + a = pm.eval('([1,2,3,4])') + try: + a.index(3, 4) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "3 is not in list" + + +def test_index_found_with_start_and_stop(): + a = pm.eval("[1,2,3,4]") + b = a.index(3, 1, 4) + assert b == 2 + + +def test_index_found_with_start_and_negative_stop(): + a = pm.eval("[1,2,3,4]") + b = a.index(3, 1, -1) + assert b == 2 + + +def test_index_not_found_with_start_and_stop(): + a = pm.eval('([1,2,3,4])') + try: + a.index(3, 4, 4) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "3 is not in list" + + +def test_index_not_found_with_start_and_outofbounds_stop(): + a = pm.eval('([1,2,3,4])') + try: + a.index(3, 4, 7) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "3 is not in list" + +# count + + +def test_count_found_once(): + a = pm.eval("[1,2,3,4]") + b = a.count(1) + assert b == 1 + + +def test_count_found_once_non_primitive_type(): + a = pm.eval("[1,2,[3,4]]") + b = a.count([3, 4]) + assert b == 1 + + +def test_count_found_twice(): + a = pm.eval("[1,2,3,4,5,1,2]") + b = a.count(2) + assert b == 2 + + +def test_count_not_found(): + a = pm.eval("[1,2,3,4,5,1,2]") + b = a.count(7) + assert b == 0 + +# reverse + + +def test_reverse(): + a = pm.eval("[1,2,3,4]") + b = a.reverse() + assert a == [4, 3, 2, 1] + assert b is None + + +def test_reverse_zero_length(): + a = pm.eval("[]") + a.reverse() + assert a == [] + + +def test_reverse_one_length(): + a = pm.eval("[2]") + a.reverse() + assert a == [2] + + +def test_reverse_too_many_args(): + a = pm.eval('([1,2,3,4])') + try: + a.reverse(3) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e).__contains__('reverse() takes no arguments (1 given)') + +# sort + + +def test_sort(): + a = pm.eval("[5,1,2,4,9,6,3,7,8]") + a.sort() + assert a == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + +def test_sort_reverse(): + a = pm.eval("[5,1,2,4,9,6,3,7,8]") + a.sort(reverse=True) + assert a == [9, 8, 7, 6, 5, 4, 3, 2, 1] + + +def test_sort_reverse_false(): + a = pm.eval("[5,1,2,4,9,6,3,7,8]") + a.sort(reverse=False) + assert a == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + +def test_sort_with_function(): + def myFunc(e, f): + return len(e) - len(f) + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + a.sort(key=myFunc) + assert a == ['VW', 'BMW', 'Ford', 'Mitsubishi'] + + +def test_sort_with_js_function(): + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + myFunc = pm.eval("((a, b) => a.toLocaleUpperCase() < b.toLocaleUpperCase() ? -1 : 1)") + a.sort(key=myFunc) + assert a == ['BMW', 'Ford', 'Mitsubishi', 'VW'] + + +def test_sort_with_one_arg_function(): + def myFunc(e): + return len(e) + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + a.sort(key=myFunc) + assert a == ['VW', 'BMW', 'Ford', 'Mitsubishi'] + + +def test_sort_with_one_arg_function_wrong_data_type(): + def myFunc(e): + return len(e) + a = pm.eval('([1,2,3,4])') + try: + a.sort(key=myFunc) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "object of type 'float' has no len()" + + +def test_sort_with_function_two_args_and_reverse_false(): + def myFunc(e, f): + return len(e) - len(f) + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + a.sort(key=myFunc, reverse=False) + assert a == ['VW', 'BMW', 'Ford', 'Mitsubishi'] + + +def test_sort_with_js_function_and_reverse_false(): + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + myFunc = pm.eval("((a, b) => a.toLocaleUpperCase() < b.toLocaleUpperCase() ? -1 : 1)") + a.sort(key=myFunc, reverse=False) + assert a == ['BMW', 'Ford', 'Mitsubishi', 'VW'] + + +def test_sort_with_function_and_reverse(): + def myFunc(e, f): + return len(e) - len(f) + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + a.sort(key=myFunc, reverse=True) + assert a == ['Mitsubishi', 'Ford', 'BMW', 'VW'] + + +def test_sort_with_js_function_and_reverse(): + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + myFunc = pm.eval("((a, b) => a.toLocaleUpperCase() < b.toLocaleUpperCase() ? -1 : 1)") + a.sort(key=myFunc, reverse=True) + assert a == ['VW', 'Mitsubishi', 'Ford', 'BMW'] + + +def test_sort_with_function_wrong_type(): + a = pm.eval('([1,2,3,4])') + try: + b = 9 + a.sort(key=b) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "'int' object is not callable" + + +def test_tricky_sort(): + a = pm.eval("[6, -2, 2, -7]") + a.sort() + assert a == [-7, -2, 2, 6] + + +def test_tricky_sort_reverse(): + a = pm.eval("[6, -2, 2, -7]") + a.sort(reverse=True) + assert a == [6, 2, -2, -7] + + +def test_sort_with_builtin_function(): # + wrong type of entries + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + a.sort(key=len) + assert a == ['VW', 'BMW', 'Ford', 'Mitsubishi'] + + +def test_sort_with_builtin_function_and_reverse(): # + wrong type of entries + a = pm.eval("(['Ford', 'Mitsubishi', 'BMW', 'VW'])") + a.sort(key=len, reverse=True) + assert a == ['Mitsubishi', 'Ford', 'BMW', 'VW'] + + +def test_sort_with_builtin_function_wrong_data_type(): + a = pm.eval('([1,2,3,4])') + try: + a.sort(key=len) + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "object of type 'float' has no len()" + +# iter + + +def iter_min(): + a = pm.eval("([7,9,1,2,3,4,5,6])") + b = min(a) + assert b == 1 + + +def iter_max(): + a = pm.eval("([7,9,1,2,3,4,5,6])") + b = max(a) + assert b == 9 + + +def iter_for(): + a = pm.eval("(['this is a test', 'another test'])") + b = [item.upper() for item in a] + assert b == ['THIS IS A TEST', 'ANOTHER TEST'] + + +def test_reduce(): + a = pm.eval("([1, 3, 5, 6, 2])") + result = reduce(lambda a, b: a + b, a) + assert result == 17 + + +def test_iter_next(): + a = pm.eval("([1, 3, 5, 6, 2])") + iterator = iter(a) + try: + while True: + element = next(iterator) + assert (False) + except StopIteration: + assert (True) + +# reverse_iter + + +def iter_reverse(): + a = pm.eval("(['7','9','1','2','3','4','5','6'])") + b = "" + for i in reversed(a): + b += i + assert b == '65432197' + +# slice subscript + + +def test_slice_full_array_single_subscript(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[:] + assert b == [1, 2, 3, 4, 5, 6] + + +def test_slice_full_array_double_subscript(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[::] + assert b == [1, 2, 3, 4, 5, 6] + + +def test_slice_empty_length_left(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[len(a):] + assert b == [] + + +def test_slice_full_length_right(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[:len(a)] + assert b == [1, 2, 3, 4, 5, 6] + + +def test_slice_zero_to_length(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[0:6] + assert b == [1, 2, 3, 4, 5, 6] + + +def test_slice(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[1:5] + assert b == [2, 3, 4, 5] + + +def test_slice_negative_start(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[-3:5] + assert b == [4, 5] + + +def test_slice_negative_end(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[2:-1] + assert b == [3, 4, 5] + + +def test_slice_negative_start_negative_end(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[-3:-1] + assert b == [4, 5] + + +def test_slice_step_zero(): + a = pm.eval('([1,2,3,4])') + try: + a[0:6:0] + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "slice step cannot be zero" + + +def test_slice_step_negative(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[0:5:-1] + assert b == [] + + +def test_slice_step_one(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[1:5:1] + assert b == [2, 3, 4, 5] + + +def test_slice_step_two(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[1:5:2] + assert b == [2, 4] + + +def test_slice_step_three(): + a = pm.eval("([1,2,3,4,5,6])") + b = a[1:5:3] + assert b == [2, 5] + +# slice subscript assign + + +def test_slice_assign_partial_array(): + a = pm.eval("([1,2,3,4,5,6])") + a[2:4] = [7, 8, 9, 0, 1, 2] + assert a == [1, 2, 7, 8, 9, 0, 1, 2, 5, 6] + + +def test_slice_delete_partial_array(): + a = pm.eval("([1,2,3,4,5,6])") + del a[2:4] + assert a == [1, 2, 5, 6] + + +def test_slice_assign_own_array(): + a = pm.eval("([1,2,3,4,5,6])") + a[2:4] = a + assert a == [1, 2, 1, 2, 3, 4, 5, 6, 5, 6] + + +def test_slice_assign_pm_array(): + a = pm.eval("([1,2,3,4,5,6])") + b = pm.eval("([7,8])") + a[2:4] = b + assert a == [1, 2, 7, 8, 5, 6] + + +def test_slice_assign_wrong_type(): + a = pm.eval('([1,2,3,4])') + try: + a[2:4] = 6 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "can only assign an iterable" + + +def test_slice_assign_negative_low(): + a = pm.eval("([1,2,3,4,5,6])") + a[-3:6] = [7, 8] + assert a == [1, 2, 3, 7, 8] + + +def test_slice_assign_negative_low_negative_high(): + a = pm.eval("([1,2,3,4,5,6])") + a[-3:-1] = [7, 8] + assert a == [1, 2, 3, 7, 8, 6] + + +def test_slice_assign_high_larger_than_length(): + a = pm.eval("([1,2,3,4,5,6])") + a[1:8] = [7, 8] + assert a == [1, 7, 8] + + +def test_slice_assign_clear(): + a = pm.eval("([1,2,3,4,5,6,7,8,9,1,2])") + a[0:32] = [] + assert a == [] + + +def test_slice_delete_partial_array_step_negative(): + a = pm.eval("([1,2,3,4,5,6])") + del a[0:4:-1] + assert a == [1, 2, 3, 4, 5, 6] + + +def test_slice_delete_partial_array_step_one(): + a = pm.eval("([1,2,3,4,5,6])") + del a[2:4:1] + assert a == [1, 2, 5, 6] + + +def test_slice_delete_partial_array_step_two(): + a = pm.eval("([1,2,3,4,5,6])") + del a[2:6:2] + assert a == [1, 2, 4, 6] + + +def test_slice_delete_partial_array_step_three(): + a = pm.eval("([1,2,3,4,5,6])") + del a[0:6:3] + assert a == [2, 3, 5, 6] + + +def test_slice_delete_partial_array_step_two_negative_start(): + a = pm.eval("([1,2,3,4,5,6])") + del a[-5:6:2] + assert a == [1, 3, 5] + + +def test_slice_delete_partial_array_step_two_negative_start_negative_end(): + a = pm.eval("([1,2,3,4,5,6])") + del a[-5:-2:2] + assert a == [1, 3, 5, 6] + + +def test_slice_assign_step_wrong_list_size(): + a = pm.eval('([1,2,3,4])') + try: + a[0:4:2] = [1] + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "attempt to assign sequence of size 1 to extended slice of size 2" + + +def test_slice_assign_partial_array_step_negative(): + a = pm.eval("([1,2,3,4,5,6])") + a[2:4:2] = [7] + assert a == [1, 2, 7, 4, 5, 6] + + +def test_slice_assign_step_zero(): + a = pm.eval('([1,2,3,4])') + try: + a[0:4:0] = [1] + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "slice step cannot be zero" + + +def test_slice_assign_partial_array_step_2(): + a = pm.eval("([1,2,3,4,5,6])") + a[2:4:2] = [7] + assert a == [1, 2, 7, 4, 5, 6] + + +def test_slice_assign_step_wrong_type(): + a = pm.eval('([1,2,3,4])') + try: + a[2:4:2] = 6 + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "must assign iterable to extended slice" + + +def test_slice_assign_partial_array_negative_start(): + a = pm.eval("([1,2,3,4,5,6])") + a[-5:4:2] = [7, 8] + assert a == [1, 7, 3, 8, 5, 6] + + +def test_slice_assign_partial_array_negative_start_negative_stop(): + a = pm.eval("([1,2,3,4,5,6])") + a[-5:-1:2] = [7, 8] + assert a == [1, 7, 3, 8, 5, 6] + + +def test_slice_assign_own_array_no_match(): + a = pm.eval("([1,2,3,4,5,6])") + try: + a[0:4:2] = a + assert (False) + except Exception as e: + assert str(type(e)) == "" + assert str(e) == "attempt to assign sequence of size 0 to extended slice of size 2" + + +def test_slice_assign_pm_array_step_2(): + a = pm.eval("([1,2,3,4,5,6])") + b = pm.eval("([1,2,3])") + a[0:10:2] = b + assert a == [1, 2, 2, 4, 3, 6] + +# __class__ + + +def test___class__attribute(): + items = pm.eval("([1,2,3,4,5,6])") + assert repr(items.__class__) == "" diff --git a/tests/python/test_objects.py b/tests/python/test_objects.py new file mode 100644 index 00000000..b40f25b1 --- /dev/null +++ b/tests/python/test_objects.py @@ -0,0 +1,188 @@ +import pythonmonkey as pm +import sys + + +def test_eval_pyobjects(): + class MyClass: + pass + + o = MyClass() + proxy_o = pm.eval("(obj) => { return obj; }")(o) + assert o is proxy_o + + +def test_eval_pyobjects_subobjects(): + class InnerClass: + def __init__(self): + self.c = 2 + + class OuterClass: + def __init__(self): + self.a = 1 + self.b = InnerClass() + + o = OuterClass() + + assert pm.eval("(obj) => { return obj.a; }")(o) == 1.0 + assert pm.eval("(obj) => { return obj.b; }")(o) is o.b + assert pm.eval("(obj) => { return obj.b.c; }")(o) == 2.0 + + +def test_eval_pyobjects_cycle(): + class MyClass: + def __init__(self): + self.a = 1 + self.b = 2 + self.recursive = self + + o = MyClass() + + assert pm.eval("(obj) => { return obj.a; }")(o) == 1.0 + assert pm.eval("(obj) => { return obj.b; }")(o) == 2.0 + assert pm.eval("(obj) => { return obj.recursive; }")(o) is o.recursive + + +def test_eval_pyobjects_proxy_get(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + assert pm.eval("(obj) => { return obj.a}")(o) == 42.0 + + +def test_eval_pyobjects_proxy_set(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + pm.eval("(obj) => { obj.b = 43; }")(o) + assert o.b == 43.0 + + +def test_eval_pyobjects_proxy_keys(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + assert pm.eval("(obj) => { return Object.keys(obj)[0]; }")(o) == 'a' + + +def test_eval_pyobjects_proxy_delete(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + pm.eval("(obj) => { delete obj.a; }")(o) + assert not hasattr(o, 'a') + + +def test_eval_pyobjects_proxy_has(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + assert pm.eval("(obj) => { return 'a' in obj; }")(o) + + +def test_eval_pyobjects_proxy_not_extensible(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + assert not pm.eval("(o) => Object.isExtensible(o)")(o) + assert pm.eval("(o) => Object.preventExtensions(o) === o")(o) + + +def test_instanceof_pyobject(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + assert pm.eval("(obj) => { return obj instanceof Object; }")(o) + + +def test_pyobjects_not_instanceof_string(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + + assert not pm.eval("(obj) => { return obj instanceof String; }")(o) + + +def test_pyobjects_valueOf(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + assert o is pm.eval("(obj) => { return obj.valueOf(); }")(o) + + +def test_pyobjects_toString(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + assert '[object Object]' == pm.eval("(obj) => { return obj.toString(); }")(o) + + +def test_pyobjects_toLocaleString(): + class MyClass: + def __init__(self): + self.a = 42 + + o = MyClass() + assert '[object Object]' == pm.eval("(obj) => { return obj.toLocaleString(); }")(o) + + +def test_toPrimitive_iterable(): + iterable = iter([1,2]) + toPrimitive = pm.eval("(obj) => { return obj[Symbol.toPrimitive]; }")(iterable) + assert repr(toPrimitive).__contains__(" { return obj.constructor; }")(iterable) + assert repr(constructor).__contains__(" { return obj[Symbol.toPrimitive]; }")(sys.stdin) + assert repr(toPrimitive).__contains__(" { return obj.constructor; }")(sys.stdin) + assert repr(constructor).__contains__(" x.toString === Object.prototype.toString") + assert is_to_string_correct({}) + + +def test_toString_is_prototype_toLocaleString(): + is_to_locale_string_correct = pm.eval("x => x.toLocaleString === Object.prototype.toLocaleString") + assert is_to_locale_string_correct({}) + + +def test_valueof_is_prototype_valueof(): + is_valueof_correct = pm.eval("x => x.valueOf === Object.prototype.valueOf") + assert is_valueof_correct({}) \ No newline at end of file diff --git a/tests/python/test_pythonmonkey_eval.py b/tests/python/test_pythonmonkey_eval.py index 67bc0c95..cb2366c4 100644 --- a/tests/python/test_pythonmonkey_eval.py +++ b/tests/python/test_pythonmonkey_eval.py @@ -3,286 +3,451 @@ import random from datetime import datetime, timedelta, timezone import math +from io import StringIO +import sys +import asyncio + def test_passes(): - assert True + assert True + def test_eval_numbers_floats(): - for _ in range(10): - py_number = random.uniform(-1000000,1000000) - js_number = pm.eval(repr(py_number)) - assert py_number == js_number + for _ in range(10): + py_number = random.uniform(-1000000, 1000000) + js_number = pm.eval(repr(py_number)) + assert py_number == js_number + def test_eval_numbers_floats_nan(): - jsNaN = pm.eval("NaN") - assert math.isnan(jsNaN) + jsNaN = pm.eval("NaN") + assert math.isnan(jsNaN) + def test_eval_numbers_floats_negative_zero(): - jsNegZero = pm.eval("-0") - assert jsNegZero == 0 - assert jsNegZero == 0.0 # expected that -0.0 == 0.0 == 0 - # https://docs.python.org/3/library/math.html#math.copysign - assert math.copysign(1.0, jsNegZero) == -1.0 + jsNegZero = pm.eval("-0") + assert jsNegZero == 0 + assert jsNegZero == 0.0 # expected that -0.0 == 0.0 == 0 + # https://docs.python.org/3/library/math.html#math.copysign + assert math.copysign(1.0, jsNegZero) == -1.0 + def test_eval_numbers_floats_inf(): - jsPosInf = pm.eval("Infinity") - jsNegInf = pm.eval("-Infinity") - assert jsPosInf == float("+inf") - assert jsNegInf == float("-inf") + jsPosInf = pm.eval("Infinity") + jsNegInf = pm.eval("-Infinity") + assert jsPosInf == float("+inf") + assert jsNegInf == float("-inf") + def test_eval_numbers_integers(): - for _ in range(10): - py_number = random.randint(-1000000,1000000) - js_number = pm.eval(repr(py_number)) - assert py_number == js_number + for _ in range(10): + py_number = random.randint(-1000000, 1000000) + js_number = pm.eval(repr(py_number)) + assert py_number == js_number + def test_eval_booleans(): - py_bool = True - js_bool = pm.eval('true') - assert py_bool == js_bool - py_bool = False - js_bool = pm.eval('false') - assert py_bool == js_bool + py_bool = True + js_bool = pm.eval('true') + assert py_bool == js_bool + py_bool = False + js_bool = pm.eval('false') + assert py_bool == js_bool + def test_eval_dates(): - MIN_YEAR = 1 # https://docs.python.org/3/library/datetime.html#datetime.MINYEAR - MAX_YEAR = 2023 - start = datetime(MIN_YEAR, 1, 1, 00, 00, 00, tzinfo=timezone.utc) - years = MAX_YEAR - MIN_YEAR + 1 - end = start + timedelta(days=365 * years) - for _ in range(10): - py_date = start + (end - start) * random.random() - # round to milliseconds precision because the smallest unit for js Date is 1ms - py_date = py_date.replace(microsecond=min(round(py_date.microsecond, -3), 999000)) # microsecond must be in 0..999999, but it would be rounded to 1000000 if >= 999500 - js_date = pm.eval(f'new Date("{py_date.isoformat()}")') - assert py_date == js_date + MIN_YEAR = 1 # https://docs.python.org/3/library/datetime.html#datetime.MINYEAR + MAX_YEAR = 2023 + start = datetime(MIN_YEAR, 1, 1, 00, 00, 00, tzinfo=timezone.utc) + years = MAX_YEAR - MIN_YEAR + 1 + end = start + timedelta(days=365 * years) + for _ in range(10): + py_date = start + (end - start) * random.random() + # round to milliseconds precision because the smallest unit for js Date is 1ms + # microsecond must be in 0..999999, but it would be rounded to 1000000 if >= 999500 + py_date = py_date.replace(microsecond=min(round(py_date.microsecond, -3), 999000)) + js_date = pm.eval(f'new Date("{py_date.isoformat()}")') + assert py_date == js_date + def test_eval_boxed_booleans(): - py_bool = True - js_bool = pm.eval('new Boolean(true)') - assert py_bool == js_bool - py_bool = False - js_bool = pm.eval('new Boolean(false)') - assert py_bool == js_bool + py_bool = True + js_bool = pm.eval('new Boolean(true)') + assert py_bool == js_bool + py_bool = False + js_bool = pm.eval('new Boolean(false)') + assert py_bool == js_bool + def test_eval_boxed_numbers_floats(): - for _ in range(10): - py_number = random.uniform(-1000000,1000000) - js_number = pm.eval(f'new Number({repr(py_number)})') - assert py_number == js_number + for _ in range(10): + py_number = random.uniform(-1000000, 1000000) + js_number = pm.eval(f'new Number({repr(py_number)})') + assert py_number == js_number + def test_eval_boxed_numbers_integers(): - for _ in range(10): - py_number = random.randint(-1000000,1000000) - js_number = pm.eval(f'new Number({repr(py_number)})') - assert py_number == js_number + for _ in range(10): + py_number = random.randint(-1000000, 1000000) + js_number = pm.eval(f'new Number({repr(py_number)})') + assert py_number == js_number + def test_eval_exceptions(): - # should print out the correct error messages - with pytest.raises(pm.SpiderMonkeyError, match='SyntaxError: "" literal not terminated before end of script'): - pm.eval('"123') - with pytest.raises(pm.SpiderMonkeyError, match="SyntaxError: missing } in compound statement"): - pm.eval('{') - with pytest.raises(pm.SpiderMonkeyError, match="TypeError: can't convert BigInt to number"): - pm.eval('1n + 1') - with pytest.raises(pm.SpiderMonkeyError, match="ReferenceError: RANDOM_VARIABLE is not defined"): - pm.eval('RANDOM_VARIABLE') - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid array length"): - pm.eval('new Array(-1)') - with pytest.raises(pm.SpiderMonkeyError, match="Error: abc"): - # manually by the `throw` statement - pm.eval('throw new Error("abc")') - - # ANYTHING can be thrown in JS - with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: 9007199254740993"): - pm.eval('throw 9007199254740993n') # 2**53+1 - with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: null"): - pm.eval('throw null') - with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: undefined"): - pm.eval('throw undefined') - with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: something from toString"): - # (side effect) calls the `toString` method if an object is thrown - pm.eval('throw { toString() { return "something from toString" } }') - - # convert JS Error object to a Python Exception object for later use (in a `raise` statement) - js_err = pm.eval("new RangeError('to be raised in Python')") - assert isinstance(js_err, BaseException) - assert isinstance(js_err, Exception) - assert type(js_err) == pm.SpiderMonkeyError - with pytest.raises(pm.SpiderMonkeyError, match="RangeError: to be raised in Python"): - raise js_err - - # convert Python Exception object to a JS Error object - get_err_msg = pm.eval("(err) => err.message") - assert "Python BufferError: ttt" == get_err_msg(BufferError("ttt")) - js_rethrow = pm.eval("(err) => { throw err }") - with pytest.raises(pm.SpiderMonkeyError, match="Error: Python BaseException: 123"): - js_rethrow(BaseException("123")) + # should print out the correct error messages + with pytest.raises(pm.SpiderMonkeyError, match='SyntaxError: "" literal not terminated before end of script'): + pm.eval('"123') + with pytest.raises(pm.SpiderMonkeyError, match="SyntaxError: missing } in compound statement"): + pm.eval('{') + with pytest.raises(pm.SpiderMonkeyError, match="TypeError: can't convert BigInt to number"): + pm.eval('1n + 1') + with pytest.raises(pm.SpiderMonkeyError, match="ReferenceError: RANDOM_VARIABLE is not defined"): + pm.eval('RANDOM_VARIABLE') + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid array length"): + pm.eval('new Array(-1)') + with pytest.raises(pm.SpiderMonkeyError, match="Error: abc"): + # manually by the `throw` statement + pm.eval('throw new Error("abc")') + + # ANYTHING can be thrown in JS + with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: 9007199254740993"): + pm.eval('throw 9007199254740993n') # 2**53+1 + with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: null"): + pm.eval('throw null') + with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: undefined"): + pm.eval('throw undefined') + with pytest.raises(pm.SpiderMonkeyError, match="uncaught exception: something from toString"): + # (side effect) calls the `toString` method if an object is thrown + pm.eval('throw { toString() { return "something from toString" } }') + + # convert JS Error object to a Python Exception object for later use (in a `raise` statement) + js_err = pm.eval("new RangeError('to be raised in Python')") + assert isinstance(js_err, BaseException) + assert isinstance(js_err, Exception) + assert type(js_err) is pm.SpiderMonkeyError + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: to be raised in Python"): + raise js_err + + # convert Python Exception object to a JS Error object + get_err_msg = pm.eval("(err) => err.message") + assert "Python BufferError: ttt" == get_err_msg(BufferError("ttt")) + js_rethrow = pm.eval("(err) => { throw err }") + with pytest.raises(pm.SpiderMonkeyError, match="Error: Python BaseException: 123"): + js_rethrow(BaseException("123")) + def test_eval_exceptions_nested_py_js_py(): - def c(): - raise Exception('this is an exception') - b = pm.eval('''(x) => { - try { - x() + def c(): + raise Exception('this is an exception') + err_line_number = sys._getframe().f_lineno - 1 + b = pm.eval('''(x) => { + try { + x() } catch(e) { return "Caught in JS " + e; } }''') - assert b(c) == "Caught in JS Error: Python Exception: this is an exception" + err = b(c) + assert "Caught in JS Error: Python Exception: this is an exception" in str(err) + assert "test_pythonmonkey_eval.py" in str(err) + assert "line {}".format(err_line_number) in str(err) + assert "in c" in str(err) + def test_eval_exceptions_nested_js_py_js(): - c = pm.eval("() => { throw TypeError('this is an exception'); }") + c = pm.eval("() => { throw TypeError('this is an exception'); }") + + def b(x): + try: + x() + return "" + except Exception as e: + return "Caught in Py " + str(e) + ret = b(c) + assert ("Caught in Py Error in" in ret) and ("TypeError: this is an exception" in ret) + + +def test_eval_exceptions_preserve_js_py_js(): + # Tests for https://github.com/Distributive-Network/PythonMonkey/blob/d9a8ebe/src/ExceptionType.cc#L39-L41 + # and https://github.com/Distributive-Network/PythonMonkey/blob/d9a8ebe/src/ExceptionType.cc#L86-L91 + obj = pm.eval("({ err: new TypeError('JS Error') })") + py_err = obj.err + assert type(py_err) is pm.SpiderMonkeyError + assert pm.eval("(e) => e instanceof TypeError")(py_err) + assert pm.eval("(e) => e.message == 'JS Error'")(py_err) + assert pm.eval("(e, obj) => Object.is(e, obj.err)")(py_err, obj) + + +def test_eval_exceptions_preserve_promise_rejection(): + # Tests for https://github.com/Distributive-Network/PythonMonkey/blob/d9a8ebe/src/PromiseType.cc#L46-L48 + async def async_fn(): + try: + await pm.eval("Promise.reject(Number(123))", {'mutedErrors': True}) + # The mutedErrors option is required to avoid sending this to uncaughtExceptionHandler prematurely + except Exception as py_err: + assert type(py_err) is pm.SpiderMonkeyError + assert repr(py_err) == "SpiderMonkeyError(123.0)" + assert pm.eval("(e) => e instanceof Number")(py_err) + assert pm.eval("(e) => e == 123")(py_err) + try: + await pm.eval("Promise.reject(new TypeError('Promise rejection'))", {'mutedErrors': True}) + except Exception as py_err: + assert type(py_err) is pm.SpiderMonkeyError + assert pm.eval("(e) => e instanceof TypeError")(py_err) + assert pm.eval("(e) => e.message == 'Promise rejection'")(py_err) + return True + assert asyncio.run(async_fn()) + + +def test_eval_exceptions_preserve_original_js_error_object(): + # Test for https://github.com/Distributive-Network/PythonMonkey/blob/dc753a0/src/setSpiderMonkeyException.cc#L108-L111 + obj = pm.eval("({ err: new TypeError('JS Error') })") + c = pm.eval("(obj) => { throw obj.err; }") + + def b(fn): + try: + fn(obj) + except Exception as e: + return e + + py_err = b(c) + assert pm.eval("(err) => err instanceof TypeError")(py_err) + assert pm.eval("(e) => e.message == 'JS Error'")(py_err) + assert pm.eval("(e, obj) => Object.is(e, obj.err)")(py_err, obj) - def b(x): - try: - x() - return "" - except Exception as e: - return "Caught in Py " + str(e) - ret = b(c) - assert ("Caught in Py Error in" in ret) and ("TypeError: this is an exception" in ret) def test_eval_undefined(): - x = pm.eval("undefined") - assert x == None + x = pm.eval("undefined") + assert x is None + def test_eval_null(): - x = pm.eval("null") - assert x == pm.null - + x = pm.eval("null") + assert x == pm.null + + def test_eval_functions(): - f = pm.eval("() => { return undefined }") - assert f() == None - - g = pm.eval("() => { return null}") - assert g() == pm.null - - h = pm.eval("(a, b) => {return a + b}") - n = 10 - for _ in range(n): - a = random.randint(-1000, 1000) - b = random.randint(-1000, 1000) - assert h(a, b) == (a + b) - - for _ in range (n): - a = random.uniform(-1000.0, 1000.0) - b = random.uniform(-1000.0, 1000.0) - assert h(a, b) == (a + b) - - assert math.isnan(h(float("nan"), 1)) - assert math.isnan(h(float("+inf"), float("-inf"))) + f = pm.eval("() => { return undefined }") + assert f() is None + + g = pm.eval("() => { return null}") + assert g() == pm.null + + h = pm.eval("(a, b) => {return a + b}") + n = 10 + for _ in range(n): + a = random.randint(-1000, 1000) + b = random.randint(-1000, 1000) + assert h(a, b) == (a + b) + + for _ in range(n): + a = random.uniform(-1000.0, 1000.0) + b = random.uniform(-1000.0, 1000.0) + assert h(a, b) == (a + b) + + assert math.isnan(h(float("nan"), 1)) + assert math.isnan(h(float("+inf"), float("-inf"))) + def test_eval_functions_latin1_string_args(): - concatenate = pm.eval("(a, b) => { return a + b}") - n = 10 - for i in range(n): - length1 = random.randint(0x0000, 0xFFFF) - length2 = random.randint(0x0000, 0xFFFF) - string1 = '' - string2 = '' - - for j in range(length1): - codepoint = random.randint(0x00, 0xFFFF) - string1 += chr(codepoint) # add random chr in ucs2 range - for j in range(length2): - codepoint = random.randint(0x00, 0xFFFF) - string2 += chr(codepoint) - - assert concatenate(string1, string2) == (string1 + string2) + concatenate = pm.eval("(a, b) => { return a + b}") + n = 10 + for i in range(n): + length1 = random.randint(0x0000, 0xFFFF) + length2 = random.randint(0x0000, 0xFFFF) + string1 = '' + string2 = '' + + for j in range(length1): + codepoint = random.randint(0x00, 0xFFFF) + string1 += chr(codepoint) # add random chr in ucs2 range + for j in range(length2): + codepoint = random.randint(0x00, 0xFFFF) + string2 += chr(codepoint) + + assert concatenate(string1, string2) == (string1 + string2) + def test_eval_functions_ucs2_string_args(): - concatenate = pm.eval("(a, b) => { return a + b}") - n = 10 - for i in range(n): - length1 = random.randint(0x0000, 0xFFFF) - length2 = random.randint(0x0000, 0xFFFF) - string1 = '' - string2 = '' - - for j in range(length1): - codepoint = random.randint(0x00, 0xFF) - string1 += chr(codepoint) # add random chr in latin1 range - for j in range(length2): - codepoint = random.randint(0x00, 0xFF) - string2 += chr(codepoint) - - assert concatenate(string1, string2) == (string1 + string2) + concatenate = pm.eval("(a, b) => { return a + b}") + n = 10 + for i in range(n): + length1 = random.randint(0x0000, 0xFFFF) + length2 = random.randint(0x0000, 0xFFFF) + string1 = '' + string2 = '' + + for j in range(length1): + codepoint = random.randint(0x00, 0xFF) + string1 += chr(codepoint) # add random chr in latin1 range + for j in range(length2): + codepoint = random.randint(0x00, 0xFF) + string2 += chr(codepoint) + + assert concatenate(string1, string2) == (string1 + string2) + def test_eval_functions_ucs4_string_args(): - concatenate = pm.eval("(a, b) => { return a + b}") - n = 10 - for i in range(n): - length1 = random.randint(0x0000, 0xFFFF) - length2 = random.randint(0x0000, 0xFFFF) - string1 = '' - string2 = '' - - for j in range(length1): - codepoint = random.randint(0x010000, 0x10FFFF) - string1 += chr(codepoint) # add random chr outside BMP - for j in range(length2): - codepoint = random.randint(0x010000, 0x10FFFF) - string2 += chr(codepoint) - - assert concatenate(string1, string2) == (string1 + string2) + concatenate = pm.eval("(a, b) => { return a + b}") + n = 10 + for i in range(n): + length1 = random.randint(0x0000, 0xFFFF) + length2 = random.randint(0x0000, 0xFFFF) + string1 = '' + string2 = '' + + for j in range(length1): + codepoint = random.randint(0x010000, 0x10FFFF) + string1 += chr(codepoint) # add random chr outside BMP + for j in range(length2): + codepoint = random.randint(0x010000, 0x10FFFF) + string2 += chr(codepoint) + + assert concatenate(string1, string2) == (string1 + string2) + def test_eval_functions_roundtrip(): - # BF-60 https://github.com/Distributive-Network/PythonMonkey/pull/18 - def ident(x): - return x - js_fn_back = pm.eval("(py_fn) => py_fn(()=>{ return 'YYZ' })")(ident) - # pm.collect() # TODO: to be fixed in BF-59 - assert "YYZ" == js_fn_back() + # BF-60 https://github.com/Distributive-Network/PythonMonkey/pull/18 + def ident(x): + return x + js_fn_back = pm.eval("(py_fn) => py_fn(()=>{ return 'YYZ' })")(ident) + # pm.collect() # TODO: to be fixed in BF-59 + assert "YYZ" == js_fn_back() + def test_eval_functions_pyfunction_in_closure(): - # BF-58 https://github.com/Distributive-Network/PythonMonkey/pull/19 - def fn1(): - def fn0(n): - return n + 100 - return fn0 - assert 101.9 == fn1()(1.9) - assert 101.9 == pm.eval("(fn1) => { return fn1 }")(fn1())(1.9) - assert 101.9 == pm.eval("(fn1, x) => { return fn1()(x) }")(fn1, 1.9) - assert 101.9 == pm.eval("(fn1) => { return fn1() }")(fn1)(1.9) + # BF-58 https://github.com/Distributive-Network/PythonMonkey/pull/19 + def fn1(): + def fn0(n): + return n + 100 + return fn0 + assert 101.9 == fn1()(1.9) + assert 101.9 == pm.eval("(fn1) => { return fn1 }")(fn1())(1.9) + assert 101.9 == pm.eval("(fn1, x) => { return fn1()(x) }")(fn1, 1.9) + assert 101.9 == pm.eval("(fn1) => { return fn1() }")(fn1)(1.9) + def test_unwrap_py_function(): - # https://github.com/Distributive-Network/PythonMonkey/issues/65 - def pyFunc(): - pass - unwrappedPyFunc = pm.eval("(wrappedPyFunc) => { return wrappedPyFunc }")(pyFunc) - assert unwrappedPyFunc is pyFunc + # https://github.com/Distributive-Network/PythonMonkey/issues/65 + def pyFunc(): + pass + unwrappedPyFunc = pm.eval("(wrappedPyFunc) => { return wrappedPyFunc }")(pyFunc) + assert unwrappedPyFunc is pyFunc + def test_unwrap_js_function(): - # https://github.com/Distributive-Network/PythonMonkey/issues/65 - wrappedJSFunc = pm.eval("const JSFunc = () => { return 0 }\nJSFunc") - assert pm.eval("(unwrappedJSFunc) => { return unwrappedJSFunc === JSFunc }")(wrappedJSFunc) + # https://github.com/Distributive-Network/PythonMonkey/issues/65 + wrappedJSFunc = pm.eval("const JSFunc = () => { return 0 }\nJSFunc") + assert pm.eval("(unwrappedJSFunc) => { return unwrappedJSFunc === JSFunc }")(wrappedJSFunc) + def test_eval_functions_pyfunctions_ints(): - caller = pm.eval("(func, param1, param2) => { return func(param1, param2) }") - def add(a, b): - return a + b - n = 10 - for i in range(n): - int1 = random.randint(0x0000, 0xFFFF) - int2 = random.randint(0x0000, 0xFFFF) - assert caller(add, int1, int2) == int1 + int2 + caller = pm.eval("(func, param1, param2) => { return func(param1, param2) }") + + def add(a, b): + return a + b + n = 10 + for i in range(n): + int1 = random.randint(0x0000, 0xFFFF) + int2 = random.randint(0x0000, 0xFFFF) + assert caller(add, int1, int2) == int1 + int2 + def test_eval_functions_pyfunctions_strs(): - caller = pm.eval("(func, param1, param2) => { return func(param1, param2) }") - def concatenate(a, b): - return a + b - n = 10 - for i in range(n): - length1 = random.randint(0x0000, 0xFFFF) - length2 = random.randint(0x0000, 0xFFFF) - string1 = '' - string2 = '' - - for j in range(length1): - codepoint = random.randint(0x0000, 0xFFFF) - string1 += chr(codepoint) # add random chr - for j in range(length2): - codepoint = random.randint(0x0000, 0xFFFF) - string2 += chr(codepoint) - assert caller(concatenate, string1, string2) == string1 + string2 + caller = pm.eval("(func, param1, param2) => { return func(param1, param2) }") + + def concatenate(a, b): + return a + b + n = 10 + for i in range(n): + length1 = random.randint(0x0000, 0xFFFF) + length2 = random.randint(0x0000, 0xFFFF) + string1 = '' + string2 = '' + + for j in range(length1): + codepoint = random.randint(0x0000, 0xFFFF) + string1 += chr(codepoint) # add random chr + for j in range(length2): + codepoint = random.randint(0x0000, 0xFFFF) + string2 += chr(codepoint) + assert caller(concatenate, string1, string2) == string1 + string2 + + +def test_py_evaloptions_string_type(): + evalOpts = {'filename': 'GoodFile'} + try: + pm.eval("{throw new Error()}", evalOpts) + except Exception as e: + assert str(e).__contains__("Error in file GoodFile") + + +def test_js_evaloptions_string_type(): + evalOpts = pm.eval("({'filename': 'GoodFile'})") + try: + pm.eval("{throw new Error()}", evalOpts) + except Exception as e: + assert str(e).__contains__("Error in file GoodFile") + + +def test_py_evaloptions_long_type(): + evalOpts = {'lineno': 10} + try: + pm.eval("{throw new Error()}", evalOpts) + except Exception as e: + assert str(e).__contains__("on line 10") + + +def test_js_evaloptions_long_type(): + evalOpts = pm.eval("({'lineno': 10})") + try: + pm.eval("{throw new Error()}", evalOpts) + except Exception as e: + assert str(e).__contains__("on line 10") + + +def test_py_evaloptions_boolean_type(): + evalOpts = {'strict': True} + try: + pm.eval("{a = 9}", evalOpts) + except Exception as e: + assert str(e).__contains__("ReferenceError: assignment to undeclared variable a") + + +def test_js_evaloptions_boolean_type(): + evalOpts = pm.eval("({'strict': true})") + try: + pm.eval("{a = 9}", evalOpts) + except Exception as e: + assert str(e).__contains__("ReferenceError: assignment to undeclared variable a") + + +def test_globalThis(): + obj = pm.eval('globalThis') + assert str(obj).__contains__("{'python': {'pythonMonkey':") + + +def test_console_globalThis(): + temp_out = StringIO() + sys.stdout = temp_out + pm.eval('console.log(globalThis)') + assert temp_out.getvalue().__contains__("{ python: \n { pythonMonkey: \n") + + +def test_console_array(): + temp_out = StringIO() + sys.stdout = temp_out + items = [1, 2, 3] + pm.eval('console.log')(items) + assert temp_out.getvalue() == "[ \x1b[33m1\x1b[39m, \x1b[33m2\x1b[39m, \x1b[33m3\x1b[39m ]\n" + + +def test_iterable_attribute_console_printing(): + temp_out = StringIO() + sys.stdout = temp_out + obj = {} + obj['stdin'] = sys.stdin # sys.stdin is iterable + assert hasattr(sys.stdin, '__iter__') == True + obj['stdin'].isTTY = sys.stdin.isatty() + pm.eval('''(function iife(obj){console.log(obj['stdin'].isTTY);})''')(obj) + assert temp_out.getvalue() == "\x1b[33mfalse\x1b[39m\n" \ No newline at end of file diff --git a/tests/python/test_reentrance_smoke.py b/tests/python/test_reentrance_smoke.py index 623c6bd4..e0ce9cdd 100644 --- a/tests/python/test_reentrance_smoke.py +++ b/tests/python/test_reentrance_smoke.py @@ -4,15 +4,16 @@ # @author Wes Garland, wes@distributive.network # @date June 2023 -import sys, os +import sys +import os import pythonmonkey as pm def test_reentrance(): - globalThis = pm.eval("globalThis;"); - globalThis.pmEval = pm.eval; - globalThis.pyEval = eval; + globalThis = pm.eval("globalThis;") + globalThis.pmEval = pm.eval + globalThis.pyEval = eval - abc=(pm.eval("() => { return {def: pyEval('123')} };"))() - assert(abc['def'] == 123) - print(pm.eval("pmEval(`pyEval(\"'test passed'\")`)")) + abc = (pm.eval("() => { return {def: pyEval('123')} };"))() + assert (abc['def'] == 123) + print(pm.eval("pmEval(`pyEval(\"'test passed'\")`)")) diff --git a/tests/python/test_strings.py b/tests/python/test_strings.py index 914782b3..01a22b97 100644 --- a/tests/python/test_strings.py +++ b/tests/python/test_strings.py @@ -1,256 +1,292 @@ import pythonmonkey as pm import gc import random +import copy + + +def test_identity(): + py_string = "abc" + js_string = pm.eval("(str) => str")(py_string) + assert py_string is js_string + def test_eval_ascii_string_matches_evaluated_string(): - py_ascii_string = "abc" - js_ascii_string = pm.eval(repr(py_ascii_string)) - assert py_ascii_string == js_ascii_string + py_ascii_string = "abc" + js_ascii_string = pm.eval(repr(py_ascii_string)) + assert py_ascii_string == js_ascii_string + def test_eval_latin1_string_matches_evaluated_string(): - py_latin1_string = "a©Ð" - js_latin1_string = pm.eval(repr(py_latin1_string)) - assert py_latin1_string == js_latin1_string + py_latin1_string = "a©Ð" + js_latin1_string = pm.eval(repr(py_latin1_string)) + assert py_latin1_string == js_latin1_string + def test_eval_null_character_string_matches_evaluated_string(): - py_null_character_string = "a\x00©" - js_null_character_string = pm.eval(repr(py_null_character_string)) - assert py_null_character_string == js_null_character_string + py_null_character_string = "a\x00©" + js_null_character_string = pm.eval(repr(py_null_character_string)) + assert py_null_character_string == js_null_character_string + def test_eval_ucs2_string_matches_evaluated_string(): - py_ucs2_string = "ՄԸՋ" - js_ucs2_string = pm.eval(repr(py_ucs2_string)) - assert py_ucs2_string == js_ucs2_string + py_ucs2_string = "ՄԸՋ" + js_ucs2_string = pm.eval(repr(py_ucs2_string)) + assert py_ucs2_string == js_ucs2_string + def test_eval_unpaired_surrogate_string_matches_evaluated_string(): - py_unpaired_surrogate_string = "Ջ©\ud8fe" - js_unpaired_surrogate_string = pm.eval(repr(py_unpaired_surrogate_string)) - assert py_unpaired_surrogate_string == js_unpaired_surrogate_string + py_unpaired_surrogate_string = "Ջ©\ud8fe" + js_unpaired_surrogate_string = pm.eval(repr(py_unpaired_surrogate_string)) + assert py_unpaired_surrogate_string == js_unpaired_surrogate_string + def test_eval_ucs4_string_matches_evaluated_string(): - py_ucs4_string = "🀄🀛🜢" - js_ucs4_string = pm.eval(repr(py_ucs4_string)) - assert py_ucs4_string == js_ucs4_string + py_ucs4_string = "🀄🀛🜢" + js_ucs4_string = pm.eval(repr(py_ucs4_string)) + assert py_ucs4_string == js_ucs4_string + def test_eval_latin1_string_fuzztest(): - n = 10 - for _ in range(n): - length = random.randint(0x0000, 0xFFFF) - string1 = '' - - for _ in range(length): - codepoint = random.randint(0x00, 0xFF) - string1 += chr(codepoint) # add random chr in latin1 range - - - INITIAL_STRING = string1 - m = 10 - for _ in range(m): - string2 = pm.eval(repr(string1)) - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - gc.collect() - pm.collect() - - #garbage collection should not collect variables still in scope - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - string1 = string2 - assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + n = 10 + for _ in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for _ in range(length): + codepoint = random.randint(0x00, 0xFF) + string1 += chr(codepoint) # add random chr in latin1 range + + INITIAL_STRING = string1 + m = 10 + for _ in range(m): + string2 = pm.eval(repr(string1)) + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + # garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 # strings should still match after a bunch of iterations through JS + def test_eval_ucs2_string_fuzztest(): - n = 10 - for _ in range(n): - length = random.randint(0x0000, 0xFFFF) - string1 = '' - - for _i in range(length): - codepoint = random.randint(0x00, 0xFFFF) - string1 += chr(codepoint) # add random chr in ucs2 range - - - INITIAL_STRING = string1 - m = 10 - for _ in range(m): - string2 = pm.eval(repr(string1)) - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - gc.collect() - pm.collect() - - #garbage collection should not collect variables still in scope - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - string1 = string2 - assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + n = 10 + for _ in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for _i in range(length): + codepoint = random.randint(0x00, 0xFFFF) + string1 += chr(codepoint) # add random chr in ucs2 range + + INITIAL_STRING = string1 + m = 10 + for _ in range(m): + string2 = pm.eval(repr(string1)) + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + # garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 # strings should still match after a bunch of iterations through JS + def test_eval_ucs4_string_fuzztest(): - n = 10 - for _ in range(n): - length = random.randint(0x0000, 0xFFFF) - string1 = '' - - for _ in range(length): - codepoint = random.randint(0x010000, 0x10FFFF) - string1 += chr(codepoint) # add random chr outside BMP - - - INITIAL_STRING = string1 - m = 10 - for _ in range(m): - string2 = pm.eval("'" + string1 + "'") - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - gc.collect() - pm.collect() - - #garbage collection should not collect variables still in scope - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - string1 = string2 - assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + n = 10 + for _ in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for _ in range(length): + codepoint = random.randint(0x010000, 0x10FFFF) + string1 += chr(codepoint) # add random chr outside BMP + + INITIAL_STRING = string1 + m = 10 + for _ in range(m): + string2 = pm.eval("'" + string1 + "'") + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + # garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 # strings should still match after a bunch of iterations through JS def test_eval_boxed_ascii_string_matches_evaluated_string(): - py_ascii_string = "abc" - js_ascii_string = pm.eval(f'new String({repr(py_ascii_string)})') - assert py_ascii_string == js_ascii_string + py_ascii_string = "abc" + js_ascii_string = pm.eval(f'new String({repr(py_ascii_string)})') + assert py_ascii_string == js_ascii_string + def test_eval_boxed_latin1_string_matches_evaluated_string(): - py_latin1_string = "a©Ð" - js_latin1_string = pm.eval(f'new String({repr(py_latin1_string)})') - assert py_latin1_string == js_latin1_string + py_latin1_string = "a©Ð" + js_latin1_string = pm.eval(f'new String({repr(py_latin1_string)})') + assert py_latin1_string == js_latin1_string + def test_eval_boxed_null_character_string_matches_evaluated_string(): - py_null_character_string = "a\x00©" - js_null_character_string = pm.eval(f'new String({repr(py_null_character_string)})') - assert py_null_character_string == js_null_character_string + py_null_character_string = "a\x00©" + js_null_character_string = pm.eval(f'new String({repr(py_null_character_string)})') + assert py_null_character_string == js_null_character_string + def test_eval_boxed_ucs2_string_matches_evaluated_string(): - py_ucs2_string = "ՄԸՋ" - js_ucs2_string = pm.eval(f'new String({repr(py_ucs2_string)})') - assert py_ucs2_string == js_ucs2_string + py_ucs2_string = "ՄԸՋ" + js_ucs2_string = pm.eval(f'new String({repr(py_ucs2_string)})') + assert py_ucs2_string == js_ucs2_string + def test_eval_boxed_unpaired_surrogate_string_matches_evaluated_string(): - py_unpaired_surrogate_string = "Ջ©\ud8fe" - js_unpaired_surrogate_string = pm.eval(f'new String({repr(py_unpaired_surrogate_string)})') - assert py_unpaired_surrogate_string == js_unpaired_surrogate_string + py_unpaired_surrogate_string = "Ջ©\ud8fe" + js_unpaired_surrogate_string = pm.eval(f'new String({repr(py_unpaired_surrogate_string)})') + assert py_unpaired_surrogate_string == js_unpaired_surrogate_string + def test_eval_boxed_ucs4_string_matches_evaluated_string(): - py_ucs4_string = "🀄🀛🜢" - js_ucs4_string = pm.eval(f'new String({repr(py_ucs4_string)})') - assert py_ucs4_string == js_ucs4_string + py_ucs4_string = "🀄🀛🜢" + js_ucs4_string = pm.eval(f'new String({repr(py_ucs4_string)})') + assert py_ucs4_string == js_ucs4_string + def test_eval_boxed_latin1_string_fuzztest(): - n = 10 - for _ in range(n): - length = random.randint(0x0000, 0xFFFF) - string1 = '' - - for _ in range(length): - codepoint = random.randint(0x00, 0xFF) - string1 += chr(codepoint) # add random chr in latin1 range - - - INITIAL_STRING = string1 - m = 10 - for _ in range(m): - string2 = pm.eval(f'new String({repr(string1)})') - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - gc.collect() - pm.collect() - - #garbage collection should not collect variables still in scope - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - string1 = string2 - assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + n = 10 + for _ in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for _ in range(length): + codepoint = random.randint(0x00, 0xFF) + string1 += chr(codepoint) # add random chr in latin1 range + + INITIAL_STRING = string1 + m = 10 + for _ in range(m): + string2 = pm.eval(f'new String({repr(string1)})') + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + # garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 # strings should still match after a bunch of iterations through JS + def test_eval_boxed_ucs2_string_fuzztest(): - n = 10 - for _ in range(n): - length = random.randint(0x0000, 0xFFFF) - string1 = '' - - for _ in range(length): - codepoint = random.randint(0x00, 0xFFFF) - string1 += chr(codepoint) # add random chr in ucs2 range - - - INITIAL_STRING = string1 - m = 10 - for _ in range(m): - string2 = pm.eval(f'new String({repr(string1)})') - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - gc.collect() - pm.collect() - - #garbage collection should not collect variables still in scope - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - string1 = string2 - assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + n = 10 + for _ in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for _ in range(length): + codepoint = random.randint(0x00, 0xFFFF) + string1 += chr(codepoint) # add random chr in ucs2 range + + INITIAL_STRING = string1 + m = 10 + for _ in range(m): + string2 = pm.eval(f'new String({repr(string1)})') + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + # garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 # strings should still match after a bunch of iterations through JS + def test_eval_boxed_ucs4_string_fuzztest(): - n = 10 - for _ in range(n): - length = random.randint(0x0000, 0xFFFF) - string1 = '' - - for _ in range(length): - codepoint = random.randint(0x010000, 0x10FFFF) - string1 += chr(codepoint) # add random chr outside BMP - - - INITIAL_STRING = string1 - m = 10 - for _ in range(m): - string2 = pm.eval(f'new String("{string1}")') - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - gc.collect() - pm.collect() - - #garbage collection should not collect variables still in scope - assert len(string1) == length - assert len(string2) == length - assert len(string1) == len(string2) - assert string1 == string2 - - string1 = string2 - assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + n = 10 + for _ in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for _ in range(length): + codepoint = random.randint(0x010000, 0x10FFFF) + string1 += chr(codepoint) # add random chr outside BMP + + INITIAL_STRING = string1 + m = 10 + for _ in range(m): + string2 = pm.eval(f'new String("{string1}")') + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + # garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 # strings should still match after a bunch of iterations through JS + + +def test_string_proxy_copy(): + world = pm.eval('(function() {return "World"})') + say = pm.eval('(function(who) { return `Hello ${who}`})') + who = world() + hello_world = say(copy.copy(who)) + assert hello_world == "Hello World" + assert hello_world is not who + +def test_string_proxy_deepcopy(): + world = pm.eval('(function() {return "World"})') + say = pm.eval('(function(who) { return `Hello ${who}`})') + who = world() + hello_world = say(copy.deepcopy(who)) + assert hello_world == "Hello World" + assert hello_world is not who + \ No newline at end of file diff --git a/tests/python/test_xhr.py b/tests/python/test_xhr.py new file mode 100644 index 00000000..990193eb --- /dev/null +++ b/tests/python/test_xhr.py @@ -0,0 +1,97 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler +import pythonmonkey as pm +import threading +import asyncio +import json + +def test_xhr(): + class TestHTTPRequestHandler(BaseHTTPRequestHandler): + def log_request(self, *args) -> None: + return + + def do_GET(self): + self.send_response(200) + self.end_headers() + self.wfile.write(b"get response") + + def do_POST(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + length = int(self.headers.get('Content-Length')) + json_string = self.rfile.read(length).decode("utf-8") + parameter_dict = json.loads(json_string) + parameter_dict["User-Agent"] = self.headers['User-Agent'] + data = json.dumps(parameter_dict).encode("utf-8") + self.wfile.write(data) + + httpd = HTTPServer(('localhost', 4001), TestHTTPRequestHandler) + thread = threading.Thread(target = httpd.serve_forever) + thread.daemon = True + thread.start() + + async def async_fn(): + assert "get response" == await pm.eval(""" + new Promise(function (resolve, reject) { + let xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://localhost:4001'); + + xhr.onload = function () + { + if (this.status >= 200 && this.status < 300) + { + resolve(this.response); + } + else + { + reject(new Error(JSON.stringify({ + status: this.status, + statusText: this.statusText + }))); + } + }; + + xhr.onerror = function (ev) + { + reject(ev.error); + }; + xhr.send(); + }); + """) + + post_result = await pm.eval(""" + new Promise(function (resolve, reject) + { + let xhr = new XMLHttpRequest(); + xhr.open('POST', 'http://localhost:4001'); + + xhr.onload = function () + { + if (this.status >= 200 && this.status < 300) + { + resolve(this.response); + } + else + { + reject(new Error(JSON.stringify({ + status: this.status, + statusText: this.statusText + }))); + } + }; + + xhr.onerror = function (ev) + { + console.log(ev) + reject(ev.error); + }; + + xhr.send(JSON.stringify({fromPM: "snakesandmonkeys"})); + }) + """) + + result_json = json.loads(post_result) + assert result_json["fromPM"] == "snakesandmonkeys" + assert result_json["User-Agent"].startswith("Python/") + httpd.shutdown() + asyncio.run(async_fn()) \ No newline at end of file diff --git a/cmake/format/uncrustify.cfg b/uncrustify.cfg similarity index 99% rename from cmake/format/uncrustify.cfg rename to uncrustify.cfg index 56d94610..0c7b4793 100644 --- a/cmake/format/uncrustify.cfg +++ b/uncrustify.cfg @@ -29,7 +29,7 @@ indent_namespace = true indent_off_after_assign = true indent_paren_open_brace = true indent_paren_close = indent_columns -indent_shift = true +indent_shift = 1 indent_template_param = true indent_var_def_cont = true indent_with_tabs = 0 @@ -85,7 +85,7 @@ sp_before_semi_for_empty = remove sp_before_sparen = force sp_before_square = remove sp_before_squares = remove -sp_before_tr_emb_cmt = add +sp_before_tr_cmt = add sp_before_type_brace_init_lst_close = remove sp_between_new_paren = remove sp_between_ptr_star = remove