├── .dockerignore
├── .editorconfig
├── .envrc
├── .github
└── workflows
│ ├── main.yml
│ ├── publish-dev-images.yml
│ ├── publish-docs.yml
│ └── publish.yml
├── .gitignore
├── .prettierrc
├── .vscode-python.env
├── CHANGELOG.md
├── Dockerfile
├── LICENSE.md
├── Makefile
├── README.md
├── docker-bake.hcl
├── docker-compose.devcontainer.yml
├── docker-compose.yml
├── docs
├── .gitignore
├── _extensions
│ ├── machow
│ │ └── interlinks
│ │ │ ├── .gitignore
│ │ │ ├── _extension.yml
│ │ │ └── interlinks.lua
│ └── mcanouil
│ │ └── iconify
│ │ ├── LICENSE
│ │ ├── _extension.yml
│ │ ├── iconify-icon.min.js
│ │ └── iconify.lua
├── _quarto.yml
├── explanation
│ └── v8_serialization_format.qmd
├── howto
│ ├── install.qmd
│ └── use_at_a_glance.qmd
├── index.qmd
├── styles.css
├── tutorials
│ └── js_to_py.qmd
├── v8serialize_logo.svg
└── v8serialize_logo_auto.svg
├── poetry.lock
├── pyproject.toml
├── scripts
├── ci-build-docs.sh
├── ci-publish-docs.sh
└── list-echoservers.sh
├── src
└── v8serialize
│ ├── __init__.py
│ ├── _decorators.py
│ ├── _enums.py
│ ├── _errors.py
│ ├── _pycompat
│ ├── __init__.py
│ ├── builtins.py
│ ├── dataclasses.py
│ ├── enum.py
│ ├── exceptions.py
│ ├── inspect.py
│ ├── re.py
│ ├── types.py
│ └── typing.py
│ ├── _recursive_eq.py
│ ├── _references.py
│ ├── _typing.py
│ ├── _values.py
│ ├── _versions.py
│ ├── constants.py
│ ├── decode.py
│ ├── encode.py
│ ├── extensions.py
│ ├── jstypes
│ ├── __init__.py
│ ├── _equality.py
│ ├── _normalise_property_key.py
│ ├── _repr.py
│ ├── _v8.py
│ ├── _v8traceback.py
│ ├── jsarray.py
│ ├── jsarrayproperties.py
│ ├── jsbigint.py
│ ├── jsbuffers.py
│ ├── jserror.py
│ ├── jsmap.py
│ ├── jsobject.py
│ ├── jsprimitiveobject.py
│ ├── jsregexp.py
│ ├── jsset.py
│ └── jsundefined.py
│ └── py.typed
├── test
├── __init__.py
├── conftest.py
├── jstypes
│ ├── __init__.py
│ ├── _v8_traceback_error_fixtures.py
│ ├── snapshots
│ │ ├── repr__jsarray_maxjsarray__0.txt
│ │ ├── repr__jsarray_maxjsarray__1.txt
│ │ ├── repr__jsarray_maxjsarray__2.txt
│ │ ├── repr__jsarray_maxjsarray__3.txt
│ │ ├── repr__jsarray_maxjsarray__4.txt
│ │ ├── repr__jsarray_maxjsarray__5.txt
│ │ ├── repr__jsarray_maxjsarray__6.txt
│ │ ├── repr__jsarray_maxjsarray__7.txt
│ │ ├── repr__jsarray_maxjsarray_indented__0.txt
│ │ ├── repr__jsarray_maxjsarray_indented__1.txt
│ │ ├── repr__jsarray_maxjsarray_indented__2.txt
│ │ ├── repr__jsarray_maxjsarray_indented__3.txt
│ │ ├── repr__jsarray_maxjsarray_indented__4.txt
│ │ ├── repr__jsarray_maxjsarray_indented__5.txt
│ │ ├── repr__jsarray_maxjsarray_indented__6.txt
│ │ ├── repr__jsarray_maxjsarray_indented__7.txt
│ │ ├── repr__jsarray_maxlevel__0.txt
│ │ ├── repr__jsarray_repr__0.txt
│ │ ├── repr__jsarray_repr__1.txt
│ │ ├── repr__jsarray_repr__10.txt
│ │ ├── repr__jsarray_repr__11.txt
│ │ ├── repr__jsarray_repr__2.txt
│ │ ├── repr__jsarray_repr__3.txt
│ │ ├── repr__jsarray_repr__4.txt
│ │ ├── repr__jsarray_repr__5.txt
│ │ ├── repr__jsarray_repr__6.txt
│ │ ├── repr__jsarray_repr__7.txt
│ │ ├── repr__jsarray_repr__8.txt
│ │ ├── repr__jsarray_repr__9.txt
│ │ ├── repr__jsobject_maxjsobject__0.txt
│ │ ├── repr__jsobject_maxjsobject__1.txt
│ │ ├── repr__jsobject_maxjsobject__2.txt
│ │ ├── repr__jsobject_maxjsobject_indented__0.txt
│ │ ├── repr__jsobject_maxjsobject_indented__1.txt
│ │ ├── repr__jsobject_maxjsobject_indented__2.txt
│ │ ├── repr__jsobject_maxjsobject_indented__3.txt
│ │ ├── repr__jsobject_repr__0.txt
│ │ ├── repr__jsobject_repr__1.txt
│ │ ├── repr__jsobject_repr__2.txt
│ │ ├── repr__jsobject_repr__3.txt
│ │ ├── repr__jsobject_repr__4.txt
│ │ ├── repr__jsobject_repr__5.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_ExceptionGroup_py__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_ExceptionGroup_v8__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_ExceptionGroup_with_context_py__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_ExceptionGroup_with_context_v8__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_exception_with_context_py__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_exception_with_context_v8__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_nested_ExceptionGroups_with_context_py__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_nested_ExceptionGroups_with_context_v8__0.exc.txt
│ │ ├── v8_traceback__format_exception_for_v8__represents_simple_exception_py__0.exc.txt
│ │ └── v8_traceback__format_exception_for_v8__represents_simple_exception_v8__0.exc.txt
│ ├── test__equality.py
│ ├── test__repr.py
│ ├── test__v8_traceback.py
│ ├── test_jsarray.py
│ ├── test_jsarrayproperties.py
│ ├── test_jsbigint.py
│ ├── test_jsbuffers.py
│ ├── test_jserror.py
│ ├── test_jsmap.py
│ ├── test_jsobject.py
│ ├── test_jsobject__statemachine.py
│ ├── test_jsprimitiveobject.py
│ ├── test_jsregexp.py
│ ├── test_jsset.py
│ └── test_normalise_property_key.py
├── pycompat
│ ├── __init__.py
│ ├── test_dataclasses.py
│ └── test_typing.py
├── strategies.py
├── test__recursive_eq.py
├── test_codec_round_trip.py
├── test_constants.py
├── test_decode.py
├── test_encode.py
├── test_errors.py
├── test_extensions.py
├── test_protocol_abc_inheritance.py
├── test_protocol_dataclass_interaction.py
├── test_references.py
├── test_round_trip_with_v8.py
└── utils.py
└── testing
├── smoketest
├── README.md
├── pyproject.toml
└── smoketest.py
└── v8serialize-echo
├── .dockerignore
├── .envrc
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── deno.json
├── deno.lock
├── docker-bake.hcl
├── main.ts
└── scripts
└── build_npm.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | .*
2 | **/__pycache__
3 | **/dist
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://editorconfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_size = 2
10 |
11 | [*.py]
12 | indent_size = 4
13 |
14 | # Trailing whitespace is significant in .diff files
15 | [*.diff]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | source_env_if_exists .local.env
2 | source_env testing/v8serialize-echo
3 |
4 | # Defines the echoservers tested in test/test_round_trip_with_v8.py
5 | V8SERIALIZE_ECHOSERVERS=$(./scripts/list-echoservers.sh)
6 | export V8SERIALIZE_ECHOSERVERS
7 |
8 | # Get the name of the devcontainer volume if we are running in a devcontainer
9 | # with a named volume as the workspace.
10 | function workspace_volume_name() {
11 | local CONTAINER_ID
12 | CONTAINER_ID="$(hostname)"
13 | # shellcheck disable=SC2016
14 | local WORKSPACE_MOUNT_SOURCE_FMT='
15 | {{- $source := "" }}
16 | {{- range .HostConfig.Mounts }}
17 | {{- if (and (eq .Type "volume") (eq .Target "/workspaces")) }}
18 | {{- $source = .Source }}
19 | {{- end }}
20 | {{- end }}
21 | {{- $source }}'
22 | docker container inspect "$CONTAINER_ID" \
23 | --format="$WORKSPACE_MOUNT_SOURCE_FMT" 2>/dev/null
24 | }
25 |
26 | # Enable the devcontainer compose config if we're in a devcontainer with a
27 | # volume-mounted workspace.
28 | V8SERIALIZE_DEVCONTAINER_VOLUME=$(workspace_volume_name)
29 | if [[ "${V8SERIALIZE_DEVCONTAINER_VOLUME?}" ]]; then
30 | export V8SERIALIZE_DEVCONTAINER_VOLUME
31 | export WORKSPACE_MOUNT_PATH=/workspaces
32 | export "COMPOSE_FILE=$(pwd)/docker-compose.yml:$(pwd)/docker-compose.devcontainer.yml"
33 | fi
34 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: "Main"
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | env:
9 | ECHOSERVER_VERSION: 0.3.0
10 |
11 | jobs:
12 | prepare_checks:
13 | runs-on: ubuntu-latest
14 | outputs:
15 | targets: ${{ steps.generate.outputs.targets }}
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: List targets
21 | id: generate
22 | uses: docker/bake-action/subaction/list-targets@v4
23 | with:
24 | target: default
25 | files: docker-bake.hcl
26 |
27 | run_check:
28 | runs-on: ubuntu-latest
29 | needs:
30 | - prepare_checks
31 | strategy:
32 | fail-fast: false
33 | matrix:
34 | target: ${{ fromJson(needs.prepare_checks.outputs.targets) }}
35 | steps:
36 | - name: Checkout
37 | uses: actions/checkout@v4
38 |
39 | - name: Check
40 | uses: docker/bake-action@v5
41 | with:
42 | targets: ${{ matrix.target }}
43 | files: docker-bake.hcl
44 |
45 | prepare_integration_checks:
46 | runs-on: ubuntu-latest
47 | outputs:
48 | targets: ${{ steps.generate.outputs.targets }}
49 | steps:
50 | - name: Checkout
51 | uses: actions/checkout@v4
52 |
53 | - name: List targets
54 | id: generate
55 | uses: docker/bake-action/subaction/list-targets@v4
56 | with:
57 | target: dev
58 | files: docker-bake.hcl
59 |
60 | run_integration_check:
61 | runs-on: ubuntu-latest
62 | needs:
63 | - prepare_integration_checks
64 | strategy:
65 | fail-fast: false
66 | matrix:
67 | target:
68 | ${{ fromJson(needs.prepare_integration_checks.outputs.targets) }}
69 | steps:
70 | - name: Checkout
71 | uses: actions/checkout@v4
72 |
73 | - name: Build dev environment image
74 | id: dev_image
75 | uses: docker/bake-action@v5
76 | with:
77 | targets: ${{ matrix.target }}
78 | files: docker-bake.hcl
79 | push: false
80 | load: true
81 |
82 | - name: Run integration test
83 | env:
84 | BAKE_META: ${{ steps.dev_image.outputs.metadata }}
85 | DEV_IMAGE_TARGET: ${{ matrix.target }}
86 | run: |
87 | set -x
88 | # Find the image name built by the preceding step for the Python
89 | # version being tested.
90 | V8SERIALIZE_DEV_IMAGE=$(jq <<<"${BAKE_META:?}" -re \
91 | '.[$ENV.DEV_IMAGE_TARGET]["image.name"] | split(",") | first'
92 | )
93 |
94 | # Start the V8 echoservers that the integration test communicates with
95 | docker compose up -d
96 |
97 | # Discover the running echoservers
98 | V8SERIALIZE_ECHOSERVERS=$(./scripts/list-echoservers.sh)
99 | export V8SERIALIZE_DEV_IMAGE V8SERIALIZE_ECHOSERVERS
100 |
101 | docker compose run integration_test
102 |
--------------------------------------------------------------------------------
/.github/workflows/publish-dev-images.yml:
--------------------------------------------------------------------------------
1 | name: "Publish Dev/CI Container Images"
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - testing/v8serialize-echo/**/*
8 | schedule:
9 | # weekly, Wednesday @ 04:51
10 | - cron: 51 4 * * 3
11 | workflow_dispatch:
12 |
13 | env:
14 | REGISTRY: ghcr.io
15 | TAG_PREFIX: ghcr.io/${{ github.repository }}
16 |
17 | jobs:
18 | v8serialize-echo:
19 | name: Build & Publish v8serialize/echoserver Container Images
20 | runs-on: ubuntu-latest
21 | permissions:
22 | packages: write
23 | id-token: write # needed for signing the images with GitHub OIDC Token
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v4
28 |
29 | - name: Install Cosign
30 | uses: sigstore/cosign-installer@v3.6.0
31 |
32 | - name: Set up QEMU
33 | uses: docker/setup-qemu-action@v3
34 |
35 | - name: Set up Docker Buildx
36 | uses: docker/setup-buildx-action@v3
37 |
38 | - name: Log in to the Container registry
39 | uses: docker/login-action@v3
40 | with:
41 | registry: ${{ env.REGISTRY }}
42 | username: ${{ github.actor }}
43 | password: ${{ secrets.GITHUB_TOKEN }}
44 |
45 | - name: Set Environment Variables
46 | run: |
47 | set -x
48 | cd testing/v8serialize-echo
49 | source .envrc
50 | echo "ECHOSERVER_VERSION=${ECHOSERVER_VERSION:?}" >> "${GITHUB_ENV:?}"
51 |
52 | - name: Build & Publish CI Container Images
53 | id: bake
54 | uses: docker/bake-action@v5
55 | with:
56 | workdir: testing/v8serialize-echo
57 | provenance: true
58 | sbom: true
59 | push: true
60 | set: |
61 | *.cache-from=type=gha
62 | *.cache-to=type=gha,mode=max
63 |
64 | - name: Sign the images with GitHub OIDC Token
65 | env:
66 | BAKE_META: ${{ steps.bake.outputs.metadata }}
67 | run: |
68 | readarray -t image_refs < <(
69 | jq <<<"${BAKE_META:?}" -r '
70 | .[]
71 | | select(.["image.name"]? and .["containerimage.digest"]?)
72 | | (.["containerimage.digest"]) as $containerimage_digest
73 | | (.["image.name"] | split(",")) as $image_names
74 | | $image_names[] | "\(.)@\($containerimage_digest)"
75 | '
76 | )
77 |
78 | echo "Images to sign:"
79 | printf ' - %s\n' "${image_refs[@]}"
80 |
81 | cosign sign --yes ${image_refs[@]:?}
82 |
--------------------------------------------------------------------------------
/.github/workflows/publish-docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish docs to GitHub Pages
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | build-deploy:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | env:
14 | GIT_AUTHOR_NAME: "GitHub Actions"
15 | GIT_AUTHOR_EMAIL:
16 | "${{ github.repository_owner }}@users.noreply.github.com"
17 | steps:
18 | - name: Check out repository
19 | uses: actions/checkout@v4
20 |
21 | - name: "Fetch gh-pages"
22 | run: git fetch origin gh-pages
23 |
24 | - name: Set up Quarto
25 | uses: quarto-dev/quarto-actions/setup@v2
26 |
27 | - uses: actions/setup-python@v5
28 | with:
29 | python-version: "3.12"
30 |
31 | - name: "Install project dependencies"
32 | run: |
33 | pip install poetry
34 | poetry install
35 |
36 | - name: "Build & Publish"
37 | run: |
38 | poetry run scripts/ci-publish-docs.sh
39 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 | on:
3 | push:
4 | tags:
5 | - v**
6 | jobs:
7 | pypi-publish:
8 | name: Upload to PyPI
9 | environment: release
10 | runs-on: ubuntu-latest
11 | permissions:
12 | # IMPORTANT: this permission is mandatory for trusted publishing
13 | id-token: write
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: actions/setup-python@v4
17 | with:
18 | python-version: "3.12"
19 | - name: Install tools
20 | run: pip install poetry
21 | - name: Build the release packages
22 | run: |
23 | poetry build
24 | sha256sum dist/*
25 |
26 | # only publish tagged commits to PyPI
27 | - name: Publish distribution 📦 to PyPI
28 | if: startsWith(github.ref, 'refs/tags/v')
29 | uses: pypa/gh-action-pypi-publish@release/v1
30 | with:
31 | print-hash: true
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | !.devcontainer
3 | !.dockerignore
4 | !.editorconfig
5 | !.env.local.example
6 | !.envrc
7 | !.git*
8 | !.prettierrc
9 | !.vscode-python.env
10 |
11 | **/__pycache__
12 | /htmlcov
13 | **/dist
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "proseWrap": "always"
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode-python.env:
--------------------------------------------------------------------------------
1 | # To have VSCode use this file, configure settings.json with:
2 | # "python.envFile": "${workspaceFolder}/.vscode-python.env"
3 |
4 | # Dynamically list running echoservers when running test/test_round_trip_with_v8.py
5 | V8SERIALIZE_ECHOSERVERS=scripts/list-echoservers.sh
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to v8serialize will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 | and this project adheres to
7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8 |
9 | ## [Unreleased]
10 |
11 | ### Added
12 |
13 | - Marked SerializationFeature.Float16Array as released from V8 13.1.201 (was
14 | marked as unreleased). ([#3](https://github.com/h4l/v8serialize/pull/3))
15 | - `JSBigInt` type — a Python `int` subclass that always encodes to BigInt.
16 | ([#7](https://github.com/h4l/v8serialize/pull/7))
17 |
18 | ### Changed
19 |
20 | - The API docs now include type annotations in the textual function signatures.
21 | Previously types were only shown in the table of parameters. Overloaded
22 | functions with multiple signatures are not shown, only the catch-all signature
23 | is (this is a limitation of quartodoc).
24 | ([#6](https://github.com/h4l/v8serialize/pull/6))
25 | - Adjust number encoding rules.
26 |
27 | - The JSBigInt type added in this release always encodes to JavaScript bigint,
28 | and serialized bigints always decode to JSBigInt.
29 | - Previously bigint decoded to plain `int`, which meant that bigints in the
30 | safe float range would not round-trip as bigint, they'd be encoded as one
31 | of the int types, like Int32, or Double.
32 | - JavaScript Numbers encoded as Double (float64) now decode to Python `int` if
33 | they are exact integers in the range of integers that float64 can represent
34 | exactly.
35 | - Previously JavaScript Numbers that weren't serialized as one of the int
36 | types (like Int32) were always decoded as Python `float`.
37 |
38 | The encoding rules now are:
39 |
40 | - Python `float` always encodes to Double/float64.
41 | - Python `int` encodes to Double/float64 if it's within the float-safe range
42 | but too large for the int types, like Int32.
43 | - Python `int` encodes to JavaScript bigint if it's outside than the
44 | float-safe range.
45 | - Python `JSBigInt` always encodes to JavaScript bigint.
46 |
47 | The decoding rules now are:
48 |
49 | - JavaScript Double/float64 decodes to Python `float`, unless it's an exact
50 | integer within the float-safe integer range, in which case it decodes as
51 | Python `int`.
52 | - Small int values like Int32 decode as Python `int`.
53 | - JavaScript bigint values decode as Python `JSBigInt`.
54 |
55 | So values round-trip with the same types consistently, with the exceptions of:
56 |
57 | - large Python `int` which becomes `JSBigInt` after a round-trip.
58 | - exact integer Python `float` round-trip to Python `int`
59 | - This is compatible with Python's numeric type system as `int` types are
60 | accepted by types requiring `float`, and the `/` and `//` operators work
61 | equivalently with `int` and `float` representations of the same value.
62 |
63 | ([#7](https://github.com/h4l/v8serialize/pull/7))
64 |
65 | ## [0.1.0] - 2024-09-24
66 |
67 | ### Added
68 |
69 | - The first stable release.
70 |
71 | [unreleased]: https://github.com/h4l/v8serialize/compare/v0.1.0...HEAD
72 | [0.1.0]: https://github.com/h4l/v8serialize/releases/tag/v0.1.0
73 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG PYTHON_VER
2 |
3 | FROM python:${PYTHON_VER:?} AS python-base
4 | ENV PIP_DISABLE_PIP_VERSION_CHECK=1
5 |
6 |
7 | FROM python-base AS poetry
8 | RUN --mount=type=cache,target=/root/.cache pip install poetry
9 | RUN python -m venv /venv
10 | ENV VIRTUAL_ENV=/venv \
11 | PATH="/venv/bin:$PATH"
12 | RUN poetry config virtualenvs.create false
13 | WORKDIR /workspace
14 | COPY pyproject.toml poetry.lock /workspace/
15 |
16 | # Poetry needs these to exist to setup the editable install
17 | RUN mkdir -p src/v8serialize && touch src/v8serialize/__init__.py README.md
18 | RUN --mount=type=cache,target=/root/.cache poetry install
19 |
20 |
21 | FROM poetry AS test
22 | RUN --mount=source=.,target=/workspace,rw \
23 | --mount=type=cache,uid=1000,target=.pytest_cache \
24 | --mount=type=cache,uid=1000,target=.hypothesis \
25 | pytest
26 |
27 |
28 | FROM poetry AS lint-setup
29 | # invalidate cache so that the lint tasks run. We use no-cache-filter here but
30 | # not on the lint-* tasks so that the tasks can mount cache dirs themselves.
31 | RUN touch .now
32 |
33 |
34 | FROM lint-setup AS lint-check
35 | RUN --mount=source=.,target=/workspace,rw \
36 | ruff check src test
37 |
38 |
39 | FROM lint-setup AS lint-format
40 | RUN --mount=source=.,target=/workspace,rw \
41 | poetry run ruff format --check --diff .
42 |
43 |
44 | FROM lint-setup AS lint-mypy
45 | RUN --mount=source=.,target=/workspace,rw \
46 | --mount=type=cache,target=.mypy_cache \
47 | poetry run mypy .
48 |
49 |
50 | FROM poetry AS smoketest-pkg-build
51 | RUN --mount=source=testing/smoketest,target=.,rw \
52 | mkdir /dist && poetry build -o /dist
53 |
54 |
55 | FROM scratch AS smoketest-pkg
56 | COPY --from=smoketest-pkg-build /dist/* .
57 |
58 |
59 | FROM poetry AS v8serialize-pkg-build
60 | RUN --mount=source=.,target=/workspace,rw \
61 | mkdir /dist && poetry build -o /dist
62 |
63 |
64 | FROM scratch AS v8serialize-pkg
65 | COPY --from=v8serialize-pkg-build /dist/* .
66 |
67 |
68 | FROM python-base AS test-package
69 | RUN python -m venv /env
70 | ENV PATH=/env/bin:$PATH
71 | RUN --mount=from=smoketest-pkg,target=/pkg/smoketest \
72 | --mount=from=v8serialize-pkg,target=/pkg/v8serialize \
73 | pip install /pkg/smoketest/*.whl /pkg/v8serialize/*.whl
74 | RUN pip list
75 | RUN python -m smoketest
76 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright 2024 The v8serialize Authors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the “Software”), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := bash
2 | .ONESHELL:
3 | .SHELLFLAGS := -eu -o pipefail -c
4 | .RECIPEPREFIX = >
5 | .DELETE_ON_ERROR:
6 | MAKEFLAGS += --warn-undefined-variables
7 | MAKEFLAGS += --no-builtin-rules
8 |
9 | # This should be the first rule so that it runs by default when running `$ make`
10 | # without arguments.
11 | help:
12 | > @echo "Targets:"
13 | > grep -P '^([\w-]+)(?=:)' --only-matching Makefile | sort
14 | .PHONY: default
15 |
16 | clean:
17 | > rm -rf dist out
18 | .PHONY: clean
19 |
20 | install:
21 | > poetry install
22 | .PHONY: install
23 |
24 | test:
25 | > pytest
26 | .PHONY: test
27 |
28 | test-cov:
29 | > pytest --cov=v8serialize --cov-report=html
30 | .PHONY: test
31 |
32 | out/:
33 | > mkdir out
34 |
35 | typecheck:
36 | > @if dmypy status; then
37 | > dmypy run src test
38 | > else
39 | > mypy src test
40 | > fi
41 | .PHONY: typecheck
42 |
43 | lint: check-code-issues check-code-import-order check-code-format check-misc-file-formatting
44 | .PHONY: lint
45 |
46 | check-code-issues:
47 | > ruff check src test
48 | .PHONY: check-code-issues
49 |
50 | check-code-format:
51 | > ruff format --check src test
52 | .PHONY: check-code-format
53 |
54 | check-misc-file-formatting:
55 | > npx prettier --check .
56 | .PHONY: check-misc-file-formatting
57 |
58 | reformat-code:
59 | > @if [[ "$$(git status --porcelain)" != "" ]]; then
60 | > echo "Refusing to reformat code: files have uncommitted changes" >&2 ; exit 1
61 | > fi
62 | > ruff format src test
63 | .PHONY: reformat-code
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Imagine having postMessage()
between JavaScript and Python.
6 |
7 |
8 |
9 | pip install v8serialize |
10 |
11 |
12 |
13 | Documentation
14 |
15 | ---
16 |
17 | # `v8serialize`
18 |
19 | A Python library to read & write JavaScript values in [V8 serialization format]
20 | with Python.
21 |
22 | [V8 serialization format]:
23 | https://h4l.github.io/v8serialize/en/latest/explanation/v8_serialization_format.html
24 |
25 | ## Examples
26 |
27 | These examples demonstrate serializing and deserializing the same selection of
28 | JavaScript values in Python and JavaScript. JavaScript types supported by
29 | JavaScript's [`structuredClone()` algorithm] can be serialized.
30 |
31 | [`structuredClone()` algorithm]:
32 | https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
33 |
34 | ### Serialize with Python
35 |
36 | ```python
37 | from base64 import b64encode
38 | from datetime import datetime, UTC
39 | import re
40 |
41 | from v8serialize import dumps
42 | from v8serialize.jstypes import JSObject, JSArray, JSUndefined
43 |
44 | serialized = dumps(
45 | [
46 | "strings 🧵🧶🪡",
47 | 123,
48 | None,
49 | JSUndefined,
50 | JSArray({0: 'a', 1: 'b', 123456789: 'sparse'}),
51 | JSObject({"msg": "Hi"}),
52 | b"\xc0\xff\xee",
53 | 2**128,
54 | {"maps": True},
55 | {"sets", "yes"},
56 | re.compile(r"^\w+$"),
57 | datetime(2024, 1, 1, tzinfo=UTC),
58 | ]
59 | )
60 |
61 | print(b64encode(serialized).decode())
62 | ```
63 |
64 | **Output**
65 |
66 | ```
67 | /w9BDFMUc3RyaW5ncyDwn6e18J+ntvCfqqFVezBfYZaa7zpVAFMBYVUBUwFiVZWa7zpTBnNwYXJzZUADlprvOm9TA21zZ1MCSGl7AUIDwP/uWiIAAAAAAAAAAAAAAAAAAAAAATtTBG1hcHNUOgInUwRzZXRzUwN5ZXMsAlJTBV5cdyskgAJEAABAHyXMeEIkAAw=
68 | ```
69 |
70 | ### Deserialize with Python
71 |
72 | ```python
73 | from base64 import b64decode
74 | from v8serialize import loads
75 |
76 | # The output of the JavaScript example
77 | serialized = b64decode(
78 | "/w9BDGMccwB0AHIAaQBuAGcAcwAgAD7Y9d0+2PbdPtih3kn2ATBfYZaa7zpJACIBYUkCIgFiSaq03nUiBnNwYXJzZUADlprvOm8iA21zZyICSGl7AUIDwP/uWjAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA7IgRtYXBzVDoCJyIDeWVzIgRzZXRzLAJSIgVeXHcrJIACRAAAQB8lzHhCJAAM"
79 | )
80 | print(loads(serialized))
81 | ```
82 |
83 | **Output**
84 |
85 | ```python
86 | JSArray([
87 | 'strings 🧵🧶🪡',
88 | 123,
89 | None,
90 | JSUndefined,
91 | JSArray({
92 | 0: 'a',
93 | 1: 'b',
94 | 123456789: 'sparse',
95 | }),
96 | JSObject(msg='Hi'),
97 | JSArrayBuffer(b'\xc0\xff\xee'),
98 | 340282366920938463463374607431768211456,
99 | JSMap({
100 | 'maps': True,
101 | }),
102 | JSSet([
103 | 'yes',
104 | 'sets',
105 | ]),
106 | JSRegExp(source='^\\w+$', flags=),
107 | datetime.datetime(2024, 1, 1, 0, 0),
108 | ])
109 | ```
110 |
111 | ### Serialize with Node.js / Deno
112 |
113 | ```javascript
114 | import * as v8 from "node:v8";
115 |
116 | const sparseArray = ["a", "b"];
117 | sparseArray[123456789] = "sparse";
118 |
119 | const buffer = v8.serialize([
120 | "strings 🧵🧶🪡",
121 | 123,
122 | null,
123 | undefined,
124 | sparseArray,
125 | { msg: "Hi" },
126 | Uint8Array.from([0xc0, 0xff, 0xee]).buffer,
127 | 2n ** 128n,
128 | new Map([["maps", true]]),
129 | new Set(["yes", "sets"]),
130 | /^\w+$/v,
131 | new Date(Date.UTC(2024, 0, 1)),
132 | ]);
133 |
134 | console.log(buffer.toString("base64"));
135 | ```
136 |
137 | **Output**
138 |
139 | ```
140 | /w9BDGMccwB0AHIAaQBuAGcAcwAgAD7Y9d0+2PbdPtih3kn2ATBfYZaa7zpJACIBYUkCIgFiSaq03nUiBnNwYXJzZUADlprvOm8iA21zZyICSGl7AUIDwP/uWjAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA7IgRtYXBzVDoCJyIDeWVzIgRzZXRzLAJSIgVeXHcrJIACRAAAQB8lzHhCJAAM
141 | ```
142 |
143 | ## Deserialize with Node.js / Deno
144 |
145 | ```javascript
146 | import * as v8 from "node:v8";
147 |
148 | // The output of the Python example
149 | const buffer = Buffer.from(
150 | "/w9BDFMUc3RyaW5ncyDwn6e18J+ntvCfqqFVezBfYZaa7zpVAFMBYVUBUwFiVZWa7zpTBnNwYXJzZUADlprvOm9TA21zZ1MCSGl7AUIDwP/uWiIAAAAAAAAAAAAAAAAAAAAAATtTBG1hcHNUOgInUwN5ZXNTBHNldHMsAlJTBV5cdyskgAJEAABAHyXMeEIkAAw=",
151 | "base64"
152 | );
153 | console.log(v8.deserialize(buffer));
154 | ```
155 |
156 | **Output**
157 |
158 | ```javascript
159 | [
160 | 'strings 🧵🧶🪡',
161 | 123,
162 | null,
163 | undefined,
164 | [ 'a', 'b', <123456787 empty items>, 'sparse' ],
165 | { msg: 'Hi' },
166 | ArrayBuffer { [Uint8Contents]: , byteLength: 3 },
167 | 340282366920938463463374607431768211456n,
168 | Map(1) { 'maps' => true },
169 | Set(2) { 'yes', 'sets' },
170 | /^\w+$/v,
171 | 2024-01-01T00:00:00.000Z
172 | ]
173 | ```
174 |
--------------------------------------------------------------------------------
/docker-bake.hcl:
--------------------------------------------------------------------------------
1 | group "default" {
2 | targets = ["test", "test_package", "lint"]
3 | }
4 |
5 | // TODO: integration
6 |
7 | py_versions = ["3.9", "3.10", "3.11", "3.12", "3.13"]
8 |
9 | target "test" {
10 | name = "test_py${replace(py, ".", "")}"
11 | matrix = {
12 | py = py_versions,
13 | }
14 | args = {
15 | PYTHON_VER = py == "latest" ? "slim" : "${py}-slim"
16 | }
17 | target = "test"
18 | no-cache-filter = ["test"]
19 | output = ["type=cacheonly"]
20 | }
21 |
22 | target "test_package" {
23 | name = "test_package_py${replace(py, ".", "")}"
24 | matrix = {
25 | py = py_versions,
26 | }
27 | args = {
28 | PYTHON_VER = py == "latest" ? "slim" : "${py}-slim"
29 | }
30 | target = "test-package"
31 | no-cache-filter = ["test-package"]
32 | output = ["type=cacheonly"]
33 | }
34 |
35 | target "lint" {
36 | name = "lint-${lint_type}"
37 | matrix = {
38 | lint_type = ["check", "format", "mypy"],
39 | }
40 | args = {
41 | PYTHON_VER = "slim"
42 | }
43 | target = "lint-${lint_type}"
44 | no-cache-filter = ["lint-setup"]
45 | output = ["type=cacheonly"]
46 | }
47 |
48 | target "dev" {
49 | name = "dev_py${replace(py, ".", "")}"
50 | matrix = {
51 | py = py_versions,
52 | }
53 | inherits = ["test_py${replace(py, ".", "")}"]
54 | no-cache-filter = []
55 | output = []
56 | target = "poetry"
57 | tags = ["v8serialize-dev:py${replace(py, ".", "")}"]
58 | }
59 |
--------------------------------------------------------------------------------
/docker-compose.devcontainer.yml:
--------------------------------------------------------------------------------
1 | services:
2 | integration_test:
3 | volumes:
4 | - devcontainer_volume:${WORKSPACE_MOUNT_PATH:?}
5 | working_dir: $PWD
6 |
7 | volumes:
8 | devcontainer_volume:
9 | external: true
10 | name: ${V8SERIALIZE_DEVCONTAINER_VOLUME:?}
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | name: v8serialize
2 | services:
3 | echoserver-node-22:
4 | container_name: ${COMPOSE_PROJECT_NAME:?}-echoserver-node-22
5 | image: ghcr.io/h4l/v8serialize/echoserver:${ECHOSERVER_VERSION:?}-node-22
6 | networks: [v8serialize]
7 | labels:
8 | com.github.h4l.v8serialize.echoserver: "true"
9 |
10 | echoserver-node-18:
11 | container_name: ${COMPOSE_PROJECT_NAME:?}-echoserver-node-18
12 | image: ghcr.io/h4l/v8serialize/echoserver:${ECHOSERVER_VERSION:?}-node-18
13 | networks: [v8serialize]
14 | labels:
15 | com.github.h4l.v8serialize.echoserver: "true"
16 |
17 | echoserver-deno:
18 | container_name: ${COMPOSE_PROJECT_NAME:?}-echoserver-deno
19 | image: ghcr.io/h4l/v8serialize/echoserver:${ECHOSERVER_VERSION:?}-deno-1.46.1
20 | networks: [v8serialize]
21 | labels:
22 | com.github.h4l.v8serialize.echoserver: "true"
23 |
24 | integration_test:
25 | profiles: [integration]
26 | image: ${V8SERIALIZE_DEV_IMAGE:-v8serialize-dev:py312}
27 | networks: [v8serialize]
28 | environment:
29 | V8SERIALIZE_ECHOSERVERS: "${V8SERIALIZE_ECHOSERVERS:-__required_but_not_set__}"
30 | depends_on:
31 | echoserver-node-22:
32 | condition: service_healthy
33 | echoserver-node-18:
34 | condition: service_healthy
35 | echoserver-deno:
36 | condition: service_healthy
37 | command: pytest -m integration -vv
38 | volumes:
39 | - .:${WORKSPACE_MOUNT_PATH:-/workspace}
40 | working_dir: /workspace
41 |
42 | networks:
43 | v8serialize:
44 | external: ${V8SERIALIZE_NETWORK_EXTERNAL:-false}
45 | name: ${V8SERIALIZE_NETWORK:-v8serialize}
46 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /.quarto/
2 |
3 | # Generated by quarto
4 | /_site
5 |
6 | # Generated by quartodoc
7 | /reference
8 | /_inv
9 | /objects.json
10 | /_sidebar.yml
11 |
--------------------------------------------------------------------------------
/docs/_extensions/machow/interlinks/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 | *.pdf
3 | *_files/
4 |
--------------------------------------------------------------------------------
/docs/_extensions/machow/interlinks/_extension.yml:
--------------------------------------------------------------------------------
1 | title: Interlinks
2 | author: Michael Chow
3 | version: 1.1.0
4 | quarto-required: ">=1.2.0"
5 | contributes:
6 | filters:
7 | - interlinks.lua
8 |
--------------------------------------------------------------------------------
/docs/_extensions/mcanouil/iconify/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mickaël Canouil
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/_extensions/mcanouil/iconify/_extension.yml:
--------------------------------------------------------------------------------
1 | title: Iconify support
2 | author: Mickaël Canouil
3 | version: 2.1.2
4 | quarto-required: ">=1.2.280"
5 | contributes:
6 | shortcodes:
7 | - iconify.lua
8 |
--------------------------------------------------------------------------------
/docs/_extensions/mcanouil/iconify/iconify.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | # MIT License
3 | #
4 | # Copyright (c) Mickaël Canouil
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 |
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 |
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 | ]]
24 |
25 | local function ensure_html_deps()
26 | quarto.doc.add_html_dependency({
27 | name = 'iconify',
28 | version = '2.1.0',
29 | scripts = {"iconify-icon.min.js"}
30 | })
31 | end
32 |
33 | local function is_empty(s)
34 | return s == nil or s == ''
35 | end
36 |
37 | local function is_valid_size(size)
38 | if is_empty(size) then
39 | return ''
40 | end
41 | local size_table = {
42 | ["tiny"] = "0.5em",
43 | ["scriptsize"] = "0.7em",
44 | ["footnotesize"] = "0.8em",
45 | ["small"] = "0.9em",
46 | ["normalsize"] = "1em",
47 | ["large"] = "1.2em",
48 | ["Large"] = "1.5em",
49 | ["LARGE"] = "1.75em",
50 | ["huge"] = "2em",
51 | ["Huge"] = "2.5em",
52 | ["1x"] = "1em",
53 | ["2x"] = "2em",
54 | ["3x"] = "3em",
55 | ["4x"] = "4em",
56 | ["5x"] = "5em",
57 | ["6x"] = "6em",
58 | ["7x"] = "7em",
59 | ["8x"] = "8em",
60 | ["9x"] = "9em",
61 | ["10x"] = "10em",
62 | ["2xs"] = "0.625em",
63 | ["xs"] = "0.75em",
64 | ["sm"] = "0.875em",
65 | ["lg"] = "1.25em",
66 | ["xl"] = "1.5em",
67 | ["2xl"] = "2em"
68 | }
69 | for key, value in pairs(size_table) do
70 | if key == size then
71 | return 'font-size: ' .. value .. ';'
72 | end
73 | end
74 | return 'font-size: ' .. size .. ';'
75 | end
76 |
77 | return {
78 | ["iconify"] = function(args, kwargs)
79 | -- detect html (excluding epub which won't handle fa)
80 | if quarto.doc.is_format("html:js") then
81 | ensure_html_deps()
82 | local icon = pandoc.utils.stringify(args[1])
83 | local set = "fluent-emoji"
84 |
85 | if #args > 1 and string.find(pandoc.utils.stringify(args[2]), ":") then
86 | quarto.log.warning(
87 | 'Use "set:icon" or "set icon" syntax, not both! ' ..
88 | 'Using "set:icon" syntax and discarding first argument!'
89 | )
90 | icon = pandoc.utils.stringify(args[2])
91 | end
92 |
93 | if string.find(icon, ":") then
94 | set = string.sub(icon, 1, string.find(icon, ":") - 1)
95 | icon = string.sub(icon, string.find(icon, ":") + 1)
96 | elseif #args > 1 then
97 | set = icon
98 | icon = pandoc.utils.stringify(args[2])
99 | end
100 |
101 | local attributes = ' icon="' .. set .. ':' .. icon .. '"'
102 | local default_label = 'Icon ' .. icon .. ' from ' .. set .. ' Iconify.design set.'
103 |
104 | local size = is_valid_size(pandoc.utils.stringify(kwargs["size"]))
105 | if not is_empty(size) then
106 | attributes = attributes .. ' style="' .. size .. '"'
107 | end
108 |
109 | local aria_label = pandoc.utils.stringify(kwargs["label"])
110 | if is_empty(aria_label) then
111 | aria_label = ' aria-label="' .. default_label .. '"'
112 | else
113 | aria_label = ' aria-label="' .. aria_label .. '"'
114 | end
115 |
116 | local title = pandoc.utils.stringify(kwargs["title"])
117 | if is_empty(title) then
118 | title = ' title="' .. default_label .. '"'
119 | else
120 | title = ' title="' .. title .. '"'
121 | end
122 |
123 | attributes = attributes .. aria_label .. title
124 |
125 | local width = pandoc.utils.stringify(kwargs["width"])
126 | if not is_empty(width) and is_empty(size) then
127 | attributes = attributes .. ' width="' .. width .. '"'
128 | end
129 | local height = pandoc.utils.stringify(kwargs["height"])
130 | if not is_empty(height) and is_empty(size) then
131 | attributes = attributes .. ' height="' .. height .. '"'
132 | end
133 | local flip = pandoc.utils.stringify(kwargs["flip"])
134 | if not is_empty(flip) then
135 | attributes = attributes .. ' flip="' .. flip.. '"'
136 | end
137 | local rotate = pandoc.utils.stringify(kwargs["rotate"])
138 | if not is_empty(rotate) then
139 | attributes = attributes .. ' rotate="' .. rotate .. '"'
140 | end
141 |
142 | local inline = pandoc.utils.stringify(kwargs["inline"])
143 | if is_empty(inline) or inline ~= "false" then
144 | attributes = ' inline ' .. attributes
145 | end
146 |
147 |
148 | return pandoc.RawInline(
149 | 'html',
150 | ''
151 | )
152 | else
153 | return pandoc.Null()
154 | end
155 | end
156 | }
157 |
--------------------------------------------------------------------------------
/docs/explanation/v8_serialization_format.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Introduction to the V8 serialization format"
3 | ---
4 |
5 | The V8 JavaScript Engine is used by several well-known JavaScript platforms, like Node.js and Deno. V8 has built-in support for serializing JavaScript values to binary data, and deserializing them back to values.
6 |
7 | The capabilities of the serialization format are compatible with the Web Platform's [Structured Clone] algorithm, which is used to send JavaScript values between contexts, such as [`postMessage()`] to send values to background workers, and to persistently store JavaScript values as data in [IndexedDB].
8 |
9 | [Structured Clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
10 | [`postMessage()`]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
11 | [IndexedDB]: https://developer.mozilla.org/en-US/docs/Glossary/IndexedDB
12 |
13 | The appealing thing about this from a JavaScript developer's point of view is that many common JavaScript types can be transparently moved between contexts without needing to manually serialize and deserialize them. For example, `Date`, `Map` and `Set`, as well as primitives like `undefined` are transparently handled, whereas with JSON these types need to be explicitly converted to plain objects, `null`, etc.
14 |
15 | V8's code base states that its value serialization format is used to persist data, and requires that changes to its own code [maintain backwards compatability][backwards compatability], meaning that newer V8 versions must be able to read values serialized by older versions, but values serialized by newer versions are not required to be readable by older versions.
16 |
17 | [backwards compatability]: https://github.com/v8/v8/blob/d49151b/src/objects/value-serializer.cc#L51
18 |
19 | ## Users of the format
20 |
21 | * Node.js [exposes V8 value serialization to users via its `v8` module][node-v8] as `serialize()`/`deserialize()` and `v8.Serializer`/`v8.Deserializer`.
22 | * Deno [also implements the `node:v8` module] as part of its Node.js compatibility.
23 | * Deno uses the format to store JavaScript values in its [Deno KV database](https://deno.com/kv).
24 | * A few projects on GitHub aim to implement the format in plain JavaScript, to allow it to be used in browsers or other non-V8 runtimes. For example, [worker-tools/v8-value-serializer](https://github.com/worker-tools/v8-value-serializer)
25 |
26 | [node-v8]: https://nodejs.org/docs/latest/api/v8.html#serialization-api
27 | [deno-v8]: https://docs.deno.com/api/node/v8/
28 |
29 | ## Capabilities
30 |
31 | The format is able to represent the JavaScript types that cover typical data used in programs:
32 |
33 | - Array
34 | - ArrayBuffer
35 | - Boolean
36 | - DataView
37 | - Date
38 | - Error types (with a fixed set of error names).
39 | - Map
40 | - Number
41 | - Object (plain objects only — prototypes, functions and get/set properties are stripped)
42 | - Primitive (including BigInt, but not Symbol)
43 | - RegExp
44 | - Set
45 | - String
46 | - TypedArray
47 |
48 | The format supports reference cycles, so complex object structures with inter-linked objects are not a problem. It supports multiple references to the same value, so strings can be de-duplicated. It also supports JavaScript's sparse arrays.
49 |
50 | These features make it largely transparant for JavaScript to send and receive serialized values.
51 |
52 | ## Considerations
53 |
54 | ### JavaScript details
55 |
56 | Although the format is very easy to use from JavaScript, using it outside JavaScript is complicated by the format's close ties to the JavaScript data types. Implementations must deal with JavaScript features like sparse arrays, support for mixing integer and string properties in arrays and objects, the object-identity equality used by Map and Set, and the wide variety of binary data representations (ArrayBuffer, DataView and all the TypedArray subclases).
57 |
58 | These aspects complicate interoperability with non-JavaScript languages, and mean that the format makes sense in a context where interoperability with JavaScript is an important requirement. For example, when maximising ease of use for the JavaScript side of an application is a priority. For general purpose data interchange, a simpler format like JSON, or a format designed for cross-language support like Protocol Buffers would be more suitable.
59 |
60 | ### Stability
61 |
62 | Although the V8 source code now clearly describes its backwards compatibility requirement, in the past the format has mistakenly broken backwards compatibility. An example of this was [support for Error objects that self-reference in their cause][CircularErrorCause]. Previously V8 failed to deserialize Error objects that self-referenced, but in fixing this the the Error deserialization logic was changed in such a way as to not support reading errors serialized by the previous implementation.
63 |
64 | Judging by the history of changes made to V8's serialization code, the backwards compatability requirement became more clearly and strongly emphasised since this occured.
65 |
66 | [CircularErrorCause]: `v8serialize.SerializationFeature.CircularErrorCause`
67 |
68 |
69 | ### Endianness
70 |
71 | V8 defines the format as using the native byte order of the computer V8 runs on. In theory this could pose interoperability problems. In practice serialized data is always little-endian, as big-endian devices are not common.
72 |
--------------------------------------------------------------------------------
/docs/howto/install.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "How to install v8serialize"
3 | ---
4 |
5 | v8serialize is [published on PyPI as `v8serialize`][v8serialize-pypi] and supports Python 3.9 and above.
6 |
7 | To install with pip, run:
8 |
9 | ```bash
10 | pip install v8serialize
11 | ```
12 |
13 | [v8serialize-pypi]: https://pypi.org/project/v8serialize/
14 |
--------------------------------------------------------------------------------
/docs/howto/use_at_a_glance.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "How to use at a glance"
3 | number-sections: true
4 | ---
5 |
6 | For more details, see [the tutorials](../tutorials/js_to_py.qmd).
7 |
8 | ## Serialize with Python
9 | ```{python}
10 | from base64 import b64encode
11 | from datetime import datetime, UTC
12 | import re
13 |
14 | from v8serialize import dumps
15 | from v8serialize.jstypes import JSObject, JSArray, JSUndefined
16 |
17 | serialized = dumps(
18 | [
19 | "strings 🧵🧶🪡",
20 | 123,
21 | None,
22 | JSUndefined,
23 | JSArray([(0, "a"), (1, "b"), (123456789, "sparse")]),
24 | JSObject({"msg": "Hi"}),
25 | b"\xc0\xff\xee",
26 | 2**128,
27 | {"maps": True},
28 | {"sets", "yes"},
29 | re.compile(r"^\w+$"),
30 | datetime(2024, 1, 1, tzinfo=UTC),
31 | ]
32 | )
33 |
34 | print(b64encode(serialized).decode())
35 | ```
36 |
37 | ## Deserialize with Python
38 | ```{python}
39 | from base64 import b64decode
40 | from v8serialize import loads
41 |
42 | # The output of the JavaScript example
43 | serialized = b64decode(
44 | "/w9BDGMccwB0AHIAaQBuAGcAcwAgAD7Y9d0+2PbdPtih3kn2ATBfYZaa7zpJACIBYUkCIgFiSaq03nUiBnNwYXJzZUADlprvOm8iA21zZyICSGl7AUIDwP/uWjAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA7IgRtYXBzVDoCJyIDeWVzIgRzZXRzLAJSIgVeXHcrJIACRAAAQB8lzHhCJAAM"
45 | )
46 | print(loads(serialized))
47 | ```
48 | Output:
49 | ```
50 | JSArray([
51 | 'strings 🧵🧶🪡',
52 | 123,
53 | None,
54 | JSUndefined,
55 | JSArray({
56 | 0: 'a',
57 | 1: 'b',
58 | 123456789: 'sparse',
59 | }),
60 | JSObject(msg='Hi'),
61 | JSArrayBuffer(b'\xc0\xff\xee'),
62 | 340282366920938463463374607431768211456,
63 | JSMap({
64 | 'maps': True,
65 | }),
66 | JSSet([
67 | 'yes',
68 | 'sets',
69 | ]),
70 | JSRegExp(source='^\\w+$', flags=),
71 | datetime.datetime(2024, 1, 1, 0, 0),
72 | ])
73 | ```
74 |
75 |
76 | ## Serialize with Node.js / Deno
77 |
78 | ```javascript
79 | import * as v8 from 'node:v8';
80 |
81 | const sparseArray = ['a', 'b'];
82 | sparseArray[123456789] = 'sparse';
83 |
84 | const buffer = v8.serialize([
85 | 'strings 🧵🧶🪡',
86 | 123,
87 | null,
88 | undefined,
89 | sparseArray,
90 | {msg: 'Hi'},
91 | Uint8Array.from([0xc0, 0xff, 0xee]).buffer,
92 | 2n**128n,
93 | new Map([['maps', true]]),
94 | new Set(['yes', 'sets']),
95 | /^\w+$/v,
96 | new Date(Date.UTC(2024, 0, 1)),
97 | ]);
98 |
99 | console.log(buffer.toString('base64'));
100 | ```
101 | ```
102 | /w9BDGMccwB0AHIAaQBuAGcAcwAgAD7Y9d0+2PbdPtih3kn2ATBfYZaa7zpJACIBYUkCIgFiSaq03nUiBnNwYXJzZUADlprvOm8iA21zZyICSGl7AUIDwP/uWjAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA7IgRtYXBzVDoCJyIDeWVzIgRzZXRzLAJSIgVeXHcrJIACRAAAQB8lzHhCJAAM
103 | ```
104 |
105 | ## Deserialize with Node.js / Deno
106 |
107 | ```javascript
108 | import * as v8 from 'node:v8';
109 |
110 | // The output of the Python example
111 | const buffer = Buffer.from('/w9BDFMUc3RyaW5ncyDwn6e18J+ntvCfqqFVezBfYZaa7zpVAFMBYVUBUwFiVZWa7zpTBnNwYXJzZUADlprvOm9TA21zZ1MCSGl7AUIDwP/uWiIAAAAAAAAAAAAAAAAAAAAAATtTBG1hcHNUOgInUwN5ZXNTBHNldHMsAlJTBV5cdyskgAJEAABAHyXMeEIkAAw=', 'base64');
112 | console.log(v8.deserialize(buffer));
113 | ```
114 | ```
115 | [
116 | 'strings 🧵🧶🪡',
117 | 123,
118 | null,
119 | undefined,
120 | [ 'a', 'b', <123456787 empty items>, 'sparse' ],
121 | { msg: 'Hi' },
122 | ArrayBuffer { [Uint8Contents]: , byteLength: 3 },
123 | 340282366920938463463374607431768211456n,
124 | Map(1) { 'maps' => true },
125 | Set(2) { 'yes', 'sets' },
126 | /^\w+$/v,
127 | 2024-01-01T00:00:00.000Z
128 | ]
129 | ```
130 |
--------------------------------------------------------------------------------
/docs/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: v8serialize
3 | description: "Read & write JavaScript values in [V8 serialization format](./explanation/v8_serialization_format.qmd) with Python."
4 | image: v8serialize_logo.svg
5 | image-alt: The v8serialize logo. Monochome. Large "V8" and smaller "serialize" in a handwritten style, with the 8 stylized to look like a snake.
6 | about:
7 | template: jolla
8 | links:
9 | - icon: github
10 | text: GitHub
11 | href: https://github.com/h4l/v8serialize/
12 | - text: "{{< iconify file-icons:pypi >}} PyPI"
13 | href: https://pypi.org/project/v8serialize/
14 | ---
15 |
16 | ```bash
17 | pip install v8serialize
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/docs/styles.css:
--------------------------------------------------------------------------------
1 | /* Get rid of the circle clip on the about template image used on the homepage. */
2 | div.quarto-about-jolla img.round {
3 | border-radius: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "v8serialize"
3 | version = "0.1.0"
4 | description = "Read & write JavaScript values from Python with the V8 serialization format."
5 | authors = ["Hal Blackburn "]
6 | license = "MIT"
7 | readme = "README.md"
8 |
9 | [tool.poetry.dependencies]
10 | python = "^3.9"
11 | packaging = ">=14.5" # this version introduces the VERSION_PATTERN constant
12 |
13 | [tool.poetry.group.dev.dependencies]
14 | exceptiongroup = { version = "^1", python = "<3.11" }
15 | hypothesis = "^6.108.8"
16 | # pytest must be ^7 because pytest-insta 0.2 requires pytest <8
17 | pytest = "^7"
18 | mypy = "^1.11.1"
19 | frozendict = "^2.4.4"
20 | pytest-cov = "^6.0.0"
21 | # pytest-insta must be ^0.2 because 0.3 requires python 3.10
22 | pytest-insta = "^0.2.0"
23 | httpx = "^0.28.1"
24 | typing-extensions = "^4.12.2"
25 | pytest-xdist = "^3.6.1"
26 | ruff = "^0.8.4"
27 |
28 |
29 | [tool.poetry.group.docs.dependencies]
30 | quartodoc = "^0.9.1"
31 | jupyter = "^1.1.1"
32 |
33 |
34 | [build-system]
35 | requires = ["poetry-core"]
36 | build-backend = "poetry.core.masonry.api"
37 |
38 | [tool.mypy]
39 | strict = true
40 |
41 | [tool.coverage.report]
42 | exclude_also = ["if TYPE_CHECKING:"]
43 |
44 | [tool.pytest.ini_options]
45 | addopts = [
46 | "-n",
47 | "auto",
48 | "--strict-markers",
49 | "-m",
50 | "not integration",
51 | "--doctest-modules",
52 | ]
53 | markers = ["integration: Integration tests"]
54 |
55 | [tool.ruff.lint]
56 | select = [
57 | "B", # flake8-bugbear
58 | "D", # pydocstyle
59 | "E",
60 | "F",
61 | "FA", # flake8-future-annotations
62 | "PYI", # flake8-pyi
63 | "I", # isort
64 | ]
65 | ignore = [
66 | "PYI041", # prefer "float" over "int | float". They are not the same!
67 | "D100", # "Missing docstring in public module" — not everything is documented yet
68 | "D101", # "Missing docstring in public class" — not everything is documented yet
69 | "D102", # "Missing docstring in public method" — not everything is documented yet
70 | "D103", # "Missing docstring in public function" — not everything is documented yet
71 | "D104", # "Missing docstring in public package" — not everything is documented yet
72 | "D105", # "Missing docstring in magic method" — not everything is documented yet
73 | ]
74 |
75 | [tool.ruff.lint.pydocstyle]
76 | convention = "numpy"
77 |
78 | [tool.ruff.lint.isort]
79 | extra-standard-library = ["typing_extensions", "_typeshed"]
80 |
--------------------------------------------------------------------------------
/scripts/ci-build-docs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | cd "$(dirname "${BASH_SOURCE[0]:?}")/../docs"
5 |
6 | rm -rf _site
7 | quartodoc interlinks
8 | quartodoc build
9 | quarto render
10 |
--------------------------------------------------------------------------------
/scripts/ci-publish-docs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xeuo pipefail
3 |
4 | cd "$(dirname "${BASH_SOURCE[0]:?}")/.."
5 |
6 | built_site_dir="$(pwd)/docs/_site"
7 | gh_pages_dir=$(mktemp -d)
8 | git_head_sha=$(git rev-parse --short HEAD)
9 | publish_prefix_dir=en/latest
10 |
11 | if git rev-parse --verify gh-pages >/dev/null 2>&1; then
12 | git worktree add "${gh_pages_dir:?}" gh-pages
13 | else
14 | git worktree add "${gh_pages_dir:?}" origin/gh-pages -b gh-pages
15 | fi
16 |
17 | scripts/ci-build-docs.sh
18 | cd "${gh_pages_dir:?}"
19 | git rm -r "${publish_prefix_dir:?}"
20 | mkdir -p "$(dirname "${publish_prefix_dir:?}")"
21 | cp -a "${built_site_dir:?}" "${publish_prefix_dir:?}"
22 | git add "${publish_prefix_dir:?}"
23 |
24 | if [[ "$(git status --porcelain)" != "" ]]; then
25 | if ! git config user.email >/dev/null; then
26 | git config user.email "${GIT_AUTHOR_EMAIL:?}"
27 | fi
28 | if ! git config user.name >/dev/null; then
29 | git config user.name "${GIT_AUTHOR_NAME:?}"
30 | fi
31 |
32 | git commit -m "docs: update ${publish_prefix_dir@Q} docs built at ${git_head_sha:?}"
33 | git push origin gh-pages
34 | else
35 | echo "No changes in ${publish_prefix_dir@Q} after rebuilding docs at ${git_head_sha:?}"
36 | fi
37 |
38 | git worktree remove "${gh_pages_dir:?}"
39 |
--------------------------------------------------------------------------------
/scripts/list-echoservers.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | # Output a JSON object listing URLs of running echoserver containers
5 | #
6 | # e.g. '{"echoserver-node-18":"http://v8serialize-echoserver-node-18:8000/"}'
7 | #
8 | # Containers must be labelled as indicated by --filter.
9 | docker container ls \
10 | --filter=label=com.github.h4l.v8serialize.echoserver=true \
11 | --format=json \
12 | | jq -sce '[
13 | .[]
14 | | (.Names | (capture("^v8serialize-(?.*)$") | .name) // .) as $name
15 | | "http://\(.Names):8000/" as $url
16 | | { key: $name, value: $url }
17 | ] | from_entries'
18 |
--------------------------------------------------------------------------------
/src/v8serialize/__init__.py:
--------------------------------------------------------------------------------
1 | """The main public API of v8serialize."""
2 |
3 | from __future__ import annotations
4 |
5 | from v8serialize._errors import DecodeV8SerializeError as DecodeV8SerializeError
6 | from v8serialize._errors import JSRegExpV8SerializeError as JSRegExpV8SerializeError
7 | from v8serialize._errors import (
8 | UnhandledTagDecodeV8SerializeError as UnhandledTagDecodeV8SerializeError,
9 | )
10 | from v8serialize._errors import V8SerializeError as V8SerializeError
11 | from v8serialize._pycompat.typing import Buffer as Buffer
12 | from v8serialize._pycompat.typing import BufferSequence as BufferSequence
13 | from v8serialize._pycompat.typing import ReadableBinary as ReadableBinary
14 | from v8serialize._references import (
15 | IllegalCyclicReferenceV8SerializeError as IllegalCyclicReferenceV8SerializeError,
16 | )
17 | from v8serialize._typing import SparseMutableSequence as SparseMutableSequence
18 | from v8serialize._typing import SparseSequence as SparseSequence
19 | from v8serialize.constants import JSErrorName as JSErrorName
20 | from v8serialize.constants import JSRegExpFlag as JSRegExpFlag
21 | from v8serialize.constants import SerializationFeature as SerializationFeature
22 | from v8serialize.constants import SymbolicVersion as SymbolicVersion
23 | from v8serialize.decode import Decoder as Decoder
24 | from v8serialize.decode import DecodeStep as DecodeStep
25 | from v8serialize.decode import DecodeStepFn as DecodeStepFn
26 | from v8serialize.decode import DecodeStepObject as DecodeStepObject
27 | from v8serialize.decode import TagReader as TagReader
28 | from v8serialize.decode import default_decode_steps as default_decode_steps
29 | from v8serialize.decode import loads as loads
30 | from v8serialize.encode import Encoder as Encoder
31 | from v8serialize.encode import EncodeV8SerializeError as EncodeV8SerializeError
32 | from v8serialize.encode import (
33 | FeatureNotEnabledEncodeV8SerializeError as FeatureNotEnabledEncodeV8SerializeError,
34 | )
35 | from v8serialize.encode import (
36 | UnhandledValueEncodeV8SerializeError as UnhandledValueEncodeV8SerializeError,
37 | )
38 | from v8serialize.encode import default_encode_steps as default_encode_steps
39 | from v8serialize.encode import dumps as dumps
40 |
--------------------------------------------------------------------------------
/src/v8serialize/_decorators.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from functools import singledispatchmethod as _singledispatchmethod
4 | from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, overload
5 |
6 | if TYPE_CHECKING:
7 | from functools import _SingleDispatchCallable
8 | from typing_extensions import Concatenate, ParamSpec
9 |
10 | T = TypeVar("T")
11 | F = TypeVar("F", bound=Callable[..., Any])
12 |
13 |
14 | # typeshed's singledispatchmethod type annotations don't keep the function's
15 | # argument types. We re-define its types to fix that.
16 | if TYPE_CHECKING:
17 | P = ParamSpec("P")
18 | S = TypeVar("S") # self / class
19 | D = TypeVar("D") # default dispatch type
20 | D1 = TypeVar("D1") # overload dispatch type
21 |
22 | class singledispatchmethod(Generic[S, D, P, T]):
23 | dispatcher: _SingleDispatchCallable[T]
24 | func: Callable[Concatenate[S, D, P], T]
25 |
26 | def __init__(self, func: Callable[Concatenate[S, D, P], T]) -> None: ...
27 |
28 | @property
29 | def __isabstractmethod__(self) -> bool: ...
30 |
31 | # The register decorator can be used like @register to use the 1st arg's
32 | # type annotation as the dispatch type.
33 | @overload
34 | def register(
35 | self, cls: Callable[Concatenate[S, D1, P], T]
36 | ) -> Callable[Concatenate[S, D1, P], T]: ...
37 |
38 | # ... or with an explicit type, like @register(bool)
39 | @overload
40 | def register(
41 | self,
42 | cls: type,
43 | ) -> Callable[
44 | [Callable[Concatenate[S, D1, P], T]], Callable[Concatenate[S, D1, P], T]
45 | ]: ...
46 |
47 | @overload
48 | def register(
49 | self, cls: type[Any], method: Callable[Concatenate[D1, P], T]
50 | ) -> Callable[Concatenate[D1, P], T]: ...
51 |
52 | def register(*args: Any, **kwargs: Any) -> Any: ...
53 |
54 | def __call__(self, value: D, *args: P.args, **kwargs: P.kwargs) -> T: ...
55 |
56 | else:
57 | singledispatchmethod = _singledispatchmethod
58 |
--------------------------------------------------------------------------------
/src/v8serialize/_enums.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import FrozenInstanceError
4 | from typing import TypeVar
5 |
6 | TypeT = TypeVar("TypeT", bound=type)
7 |
8 |
9 | def frozen_setattr(cls: type, name: str, value: object) -> None:
10 | raise FrozenInstanceError(f"Cannot assign to field {name!r}")
11 |
12 |
13 | def frozen(cls: TypeT) -> TypeT:
14 | """Disable `__setattr__`, much like @dataclass(frozen=True)."""
15 | cls.__setattr__ = frozen_setattr # type: ignore[method-assign,assignment]
16 | return cls
17 |
--------------------------------------------------------------------------------
/src/v8serialize/_errors.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass, fields
4 | from typing import TYPE_CHECKING, cast
5 |
6 | from v8serialize._pycompat.dataclasses import slots_if310
7 | from v8serialize._pycompat.typing import ReadableBinary
8 |
9 | if TYPE_CHECKING:
10 | from v8serialize.constants import SerializationTag
11 |
12 |
13 | @dataclass(init=False)
14 | class V8SerializeError(Exception):
15 | """The base class that all v8serialize errors are subclasses of."""
16 |
17 | if not TYPE_CHECKING:
18 | message: str # needed to have dataclass include message in the repr, etc
19 |
20 | def __init__(self, message: str, *args: object) -> None:
21 | super().__init__(message, *args)
22 |
23 | @property
24 | def message(self) -> str:
25 | return cast(str, self.args[0])
26 |
27 | def __str__(self) -> str:
28 | field_values = [
29 | (f.name, getattr(self, f.name)) for f in fields(self) if f.name != "message"
30 | ]
31 | values_fmt = ", ".join(f"{f}={v!r}" for (f, v) in field_values)
32 |
33 | if values_fmt:
34 | return f"{self.message}: {values_fmt}"
35 | return self.message
36 |
37 |
38 | # TODO: str/repr needs customising to abbreviate the data field
39 | @dataclass(init=False)
40 | class DecodeV8SerializeError(V8SerializeError, ValueError):
41 | position: int
42 | data: ReadableBinary
43 |
44 | def __init__(
45 | self, message: str, *args: object, position: int, data: ReadableBinary
46 | ) -> None:
47 | super().__init__(message, *args)
48 | self.position = position
49 | self.data = data
50 |
51 |
52 | @dataclass(init=False)
53 | class UnhandledTagDecodeV8SerializeError(DecodeV8SerializeError):
54 | """
55 | No `TagReader` is able to handle a `SerializationTag`.
56 |
57 | Raised when attempting to deserialize a tag that no `TagReader` is able to
58 | handle (by reading the tag's data from the stream and representing the data
59 | as a Python object).
60 | """
61 |
62 | if not TYPE_CHECKING:
63 | tag: SerializationTag
64 |
65 | def __init__(
66 | self,
67 | message: str,
68 | *args: object,
69 | tag: SerializationTag,
70 | position: int,
71 | data: ReadableBinary,
72 | ) -> None:
73 | super().__init__(message, tag, *args, position=position, data=data)
74 |
75 | @property
76 | def tag(self) -> SerializationTag:
77 | return cast("SerializationTag", self.args[1])
78 |
79 |
80 | @dataclass(init=False, **slots_if310())
81 | class NormalizedKeyError(KeyError):
82 | """A JSObject does not contain a property for the requested key.
83 |
84 | JSObjects store and look up integer keys differently from non-integer keys,
85 | so the actual key used in the lookup may not be the same as the same as the
86 | original, raw key. The `normalized_key` and `raw_key` properties hold both
87 | versions of the key.
88 | """
89 |
90 | normalized_key: object
91 | raw_key: object
92 |
93 | def __init__(self, normalized_key: object, raw_key: object) -> None:
94 | self.normalized_key = normalized_key
95 | self.raw_key = raw_key
96 | super(NormalizedKeyError, self).__init__(
97 | f"{self.normalized_key!r} (normalized from {self.raw_key!r})"
98 | )
99 |
100 |
101 | class JSRegExpV8SerializeError(V8SerializeError):
102 | pass
103 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/builtins.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import TYPE_CHECKING, TypeVar
5 |
6 | if TYPE_CHECKING:
7 | from typing_extensions import ParamSpec
8 |
9 | _P = ParamSpec("_P")
10 | _R_co = TypeVar("_R_co", covariant=True)
11 |
12 | # @staticmethod decorator is not callable in py3.9, you must reference it via
13 | # the class it's used in. We call it from within the class definition to define
14 | # hypothesis strategies, so we need to call it directly.
15 | if sys.version_info >= (3, 10):
16 | callable_staticmethod = staticmethod
17 | else:
18 | if TYPE_CHECKING:
19 |
20 | class callable_staticmethod(staticmethod[_P, _R_co]):
21 | def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ...
22 |
23 | else:
24 |
25 | class callable_staticmethod(staticmethod):
26 | def __call__(self, *args, **kwargs):
27 | return self.__func__(*args, **kwargs)
28 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/dataclasses.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from dataclasses import FrozenInstanceError, dataclass
5 | from dataclasses import fields as dataclass_fields
6 | from typing import Literal, TypedDict
7 |
8 |
9 | class NoArg(TypedDict):
10 | pass
11 |
12 |
13 | class SlotsTrue(TypedDict):
14 | slots: Literal[True]
15 |
16 |
17 | if sys.version_info < (3, 10):
18 |
19 | def slots_if310() -> NoArg:
20 | return NoArg()
21 |
22 | else:
23 |
24 | def slots_if310() -> SlotsTrue:
25 | return SlotsTrue(slots=True)
26 |
27 |
28 | @dataclass
29 | class FrozenAfterInitDataclass:
30 | """A mixin for dataclasses that disallows changing fields after init.
31 |
32 | Fields can be set once and not again. This is an alternative to
33 | `@dataclass(frozen=True)` — it only freezes dataclass-managed fields, so it
34 | doesn't affect non-dataclass fields, such as typing.Generic's dunder fields.
35 | """
36 |
37 | def __delattr__(self, name: str) -> None:
38 | if name in (f.name for f in dataclass_fields(self)):
39 | raise FrozenInstanceError(f"cannot delete field {name}")
40 | super(FrozenAfterInitDataclass, self).__delattr__(name)
41 |
42 | def __setattr__(self, name: str, value: object) -> None:
43 | if name in (f.name for f in dataclass_fields(self)):
44 | if hasattr(self, name):
45 | raise FrozenInstanceError(f"cannot set {name!r}")
46 | super(FrozenAfterInitDataclass, self).__setattr__(name, value)
47 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/enum.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from enum import EnumMeta, Flag, IntFlag
5 | from typing import TYPE_CHECKING, Iterator
6 |
7 | if TYPE_CHECKING:
8 | from typing_extensions import Self
9 |
10 |
11 | class ContainsValueEnumMeta(EnumMeta):
12 | def __contains__(cls, value: object) -> bool:
13 | if value in cls._value2member_map_:
14 | return True
15 | return False
16 |
17 |
18 | if sys.version_info < (3, 12):
19 | from enum import Enum
20 |
21 | class StrEnum(str, Enum, metaclass=ContainsValueEnumMeta):
22 | def __str__(self) -> str:
23 | assert self._value_ is not self
24 | return str(self._value_)
25 |
26 | else:
27 | from enum import StrEnum as StrEnum # noqa: F401 # re-export
28 |
29 |
30 | if sys.version_info < (3, 11):
31 | # Even though IntFlag is just (int, Flag), MyPy errors if we subclass
32 | # (IterableFlag, IntFlag), so we have to redundantly define this for both
33 | # types.
34 | class IterableFlag(Flag):
35 | def __iter__(self) -> Iterator[Self]:
36 | for flag in type(self):
37 | if self & flag:
38 | yield flag
39 |
40 | class IterableIntFlag(IntFlag):
41 | def __iter__(self) -> Iterator[Self]:
42 | for flag in type(self):
43 | if self & flag:
44 | yield flag
45 |
46 | else:
47 |
48 | class IterableFlag(Flag):
49 | pass
50 |
51 | class IterableIntFlag(IntFlag):
52 | pass
53 |
54 |
55 | # In py3.12 you can do things like `42 in SomeIntEnum`, returning True/False.
56 | # In previous versions you get a TypeError.
57 | if sys.version_info < (3, 12):
58 | from enum import IntEnum as _IntEnum
59 |
60 | class IntEnum(_IntEnum, metaclass=ContainsValueEnumMeta):
61 | def __str__(self) -> str:
62 | assert self._value_ is not self
63 | return str(self._value_)
64 |
65 | else:
66 | from enum import IntEnum as IntEnum # noqa: F401 # re-export
67 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/exceptions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Protocol, cast
4 |
5 | if TYPE_CHECKING:
6 | from typing_extensions import TypeGuard
7 |
8 |
9 | class Notes(Protocol):
10 | __notes__: list[str]
11 |
12 |
13 | def has_notes(exc: BaseException) -> TypeGuard[Notes]:
14 | return isinstance(getattr(exc, "__notes__", None), list)
15 |
16 |
17 | def add_note(exc: BaseException, note: str) -> None:
18 | if not isinstance(note, str):
19 | raise TypeError("note must be a str")
20 | if not has_notes(exc):
21 | exc_with_notes = cast(Notes, exc)
22 | exc_with_notes.__notes__ = notes = []
23 | else:
24 | notes = exc.__notes__
25 | notes.append(note)
26 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/inspect.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import enum
4 | import sys
5 |
6 | # BufferFlags does not exist until 3.12
7 | if sys.version_info >= (3, 12):
8 | from inspect import BufferFlags as BufferFlags
9 | else:
10 |
11 | class BufferFlags(enum.IntFlag):
12 | SIMPLE = 0
13 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/re.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from re import ASCII as ASCII
4 | from re import DOTALL as DOTALL
5 | from re import IGNORECASE as IGNORECASE
6 | from re import LOCALE as LOCALE
7 | from re import MULTILINE as MULTILINE
8 | from re import UNICODE as UNICODE
9 | from re import VERBOSE as VERBOSE
10 |
11 | from v8serialize._pycompat.enum import IterableIntFlag
12 |
13 |
14 | # 3.9 doesn't define NOFLAG, and default IntFlag is not iterable in 3.9.
15 | class RegexFlag(IterableIntFlag):
16 | NOFLAG = 0
17 | ASCII = ASCII
18 | DOTALL = DOTALL
19 | IGNORECASE = IGNORECASE
20 | LOCALE = LOCALE
21 | MULTILINE = MULTILINE
22 | UNICODE = UNICODE
23 | VERBOSE = VERBOSE
24 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 |
5 | if sys.version_info >= (3, 10):
6 | from types import NoneType as NoneType
7 | else:
8 | NoneType = type(None)
9 |
--------------------------------------------------------------------------------
/src/v8serialize/_pycompat/typing.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from array import array
4 | from typing import TYPE_CHECKING, Sequence, Union, overload
5 |
6 | from v8serialize._pycompat.inspect import BufferFlags
7 |
8 | if TYPE_CHECKING:
9 | from typing_extensions import Buffer as Buffer
10 | from typing_extensions import TypeAlias, TypeGuard
11 |
12 | class BufferSequence(Sequence[int], Buffer):
13 | """
14 | Binary data, like `bytes`, `bytearray`, `array.array` and `memoryview`.
15 |
16 | Essentially a `Sequence[int]` that's also a `Buffer`.
17 | """
18 |
19 | @overload # type: ignore[override]
20 | def __getitem__(self, index: slice, /) -> BufferSequence: ...
21 |
22 | @overload
23 | def __getitem__(self, index: int, /) -> int: ...
24 |
25 | def __getitem__(self, index: int | slice, /) -> int | BufferSequence: ...
26 |
27 | else:
28 | try:
29 | from collections.abc import Buffer
30 | except ImportError:
31 | from abc import ABC, abstractmethod
32 |
33 | class Buffer(ABC):
34 | """An alias of [`collections.abc.Buffer`](`collections.abc.Buffer`)."""
35 |
36 | @abstractmethod
37 | def __buffer__(self, flags: int) -> memoryview: ...
38 |
39 | class BufferSequence(Sequence, Buffer):
40 | """
41 | A `Sequence[int]` that's also a `Buffer`.
42 |
43 | Essentially binary data, like `bytes`, `bytearray`, `array.array` and
44 | `memoryview`.
45 | """
46 |
47 |
48 | ReadableBinary: TypeAlias = Union[
49 | "bytes | bytearray | memoryview | array[int] | BufferSequence"
50 | ]
51 | """
52 | Binary data such as `bytes`, `bytearray`, `array.array` and `memoryview`.
53 |
54 | Can also be any [`BufferSequence`](`v8serialize.BufferSequence`).
55 | """
56 |
57 |
58 | def is_readable_binary(buffer: Buffer) -> TypeGuard[ReadableBinary]:
59 | """Return True if a binary value can be read without wrapping in a `memoryview`."""
60 | return isinstance(buffer, (bytes, bytearray, memoryview, array, Sequence))
61 |
62 |
63 | def get_buffer(
64 | buffer: Buffer, flags: int | BufferFlags = BufferFlags.SIMPLE
65 | ) -> memoryview:
66 | """Get a bytes-format memoryview of a value supporting the Buffer protocol.
67 |
68 | Returns
69 | -------
70 | :
71 | A memoryview with itemsize 1, 1 dimension and `B` (uint8) format.
72 | """
73 | # Python buffer protocol API only available from Python 3.12
74 | if hasattr(buffer, "__buffer__"):
75 | buf = buffer.__buffer__(flags)
76 | else:
77 | buf = memoryview(buffer)
78 | if not (buf.format == "B" and buf.ndim == 1 and buf.itemsize == 1):
79 | buf = buf.cast("B")
80 | return buf
81 |
--------------------------------------------------------------------------------
/src/v8serialize/_recursive_eq.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from _thread import get_ident
4 | from functools import wraps
5 | from typing import Final, TypeVar
6 |
7 | _RUNNING_EQ_KEYS: Final[set[tuple[int, int]]] = set()
8 |
9 | _T = TypeVar("_T")
10 |
11 |
12 | def recursive_eq(cls: type[_T]) -> type[_T]:
13 | """Allow `==` of classes with self-referencing fields.
14 |
15 | This class decorator wraps `__eq__` to detect and short-circuit `__eq__`
16 | being called on an object as a result of itself checking its own equality.
17 | This allows objects that contain direct or indirect references to themself
18 | to compare with `==`, without infinite recursive calls until stack overflow.
19 |
20 | Self-referential objects are equal to others if both sides use the same
21 | object-identity structure (according to `is` / `id()`) and their own
22 | `__eq__` is `True`.
23 |
24 | For example `a -> b -> a` can be equal to `a' -> b' -> a'`, but will not be
25 | equal to `a' -> b' -> A' -> B' -> a'` (assuming `a` would be equal to `A` in
26 | isolation). In the first case, the identity structure has two nodes
27 | repeating, in the second there are four nodes, so the object-identity
28 | structure is different.
29 | """
30 | wrapped_eq = cls.__eq__
31 |
32 | @wraps(wrapped_eq)
33 | def decorator(self: object, other: object) -> bool:
34 | # We must stop if self is other, otherwise we'd try to add ourself
35 | # twice, which shouldn't be allowed — it'd fail when removing.
36 | # Could parametrize a default result here for some kind of weird
37 | # recursive type that doesn't eq itself, but that seems unlikely.
38 | if self is other:
39 | return True
40 |
41 | # Keys are different across threads so that concurrent eq calls are
42 | # independent.
43 | thread_id = get_ident()
44 | self_key = id(self), thread_id
45 | other_key = id(other), thread_id
46 | self_running = self_key in _RUNNING_EQ_KEYS
47 | other_running = other_key in _RUNNING_EQ_KEYS
48 |
49 | # If either side already has __eq__ running in the call stack, we must
50 | # stop recursing, otherwise we'll infinitely loop.
51 | if self_running or other_running:
52 | # If both sides were already seen, we might be equal (caller decides)
53 | # If only one side was running, we can't be equal, as there's a
54 | # different structure referencing this point.
55 | return self_running and other_running
56 |
57 | # Register these objects as running so that we won't descend into them
58 | # if they are referenced again.
59 | _RUNNING_EQ_KEYS.add(self_key)
60 | _RUNNING_EQ_KEYS.add(other_key)
61 |
62 | # Allow the type to do its own equality check as normal, descending into
63 | # children if required.
64 | try:
65 | eq = wrapped_eq(self, other)
66 | finally:
67 | _RUNNING_EQ_KEYS.remove(self_key)
68 | _RUNNING_EQ_KEYS.remove(other_key)
69 | return eq
70 |
71 | cls.__eq__ = decorator # type: ignore[method-assign]
72 | return cls
73 |
--------------------------------------------------------------------------------
/src/v8serialize/_values.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import (
4 | TYPE_CHECKING,
5 | Literal,
6 | NewType,
7 | Protocol,
8 | TypeVar,
9 | overload,
10 | runtime_checkable,
11 | )
12 |
13 | from v8serialize._pycompat.typing import ReadableBinary
14 | from v8serialize.constants import ArrayBufferViewTag, JSErrorName
15 |
16 | if TYPE_CHECKING:
17 | from typing_extensions import TypeAlias
18 |
19 | T_co = TypeVar("T_co", covariant=True)
20 |
21 | SharedArrayBufferId = NewType("SharedArrayBufferId", int)
22 | TransferId = NewType("TransferId", int)
23 |
24 |
25 | @runtime_checkable
26 | class AnyArrayBuffer(Protocol):
27 | """
28 | A Protocol matching [JSArrayBuffer].
29 |
30 | [JSArrayBuffer]: `v8serialize.jstypes.JSArrayBuffer`
31 | """
32 |
33 | if TYPE_CHECKING:
34 |
35 | @property
36 | def data(self) -> ReadableBinary: ...
37 | @property
38 | def max_byte_length(self) -> int: ...
39 | @property
40 | def resizable(self) -> bool: ...
41 |
42 | else:
43 | # test/test_protocol_dataclass_interaction.py
44 | data = ...
45 | max_byte_length = ...
46 | resizable = ...
47 |
48 |
49 | @runtime_checkable
50 | class AnySharedArrayBuffer(Protocol):
51 | """
52 | A Protocol matching [JSSharedArrayBuffer].
53 |
54 | [JSSharedArrayBuffer]: `v8serialize.jstypes.JSSharedArrayBuffer`
55 | """
56 |
57 | if TYPE_CHECKING:
58 |
59 | @property
60 | def buffer_id(self) -> SharedArrayBufferId: ...
61 |
62 | else:
63 | # test/test_protocol_dataclass_interaction.py
64 | buffer_id = ...
65 |
66 |
67 | @runtime_checkable
68 | class AnyArrayBufferTransfer(Protocol):
69 | """
70 | A Protocol matching [JSArrayBufferTransfer].
71 |
72 | [JSArrayBufferTransfer]: `v8serialize.jstypes.JSArrayBufferTransfer`
73 | """
74 |
75 | if TYPE_CHECKING:
76 |
77 | @property
78 | def transfer_id(self) -> TransferId: ...
79 |
80 | else:
81 | # test/test_protocol_dataclass_interaction.py
82 | transfer_id = ...
83 |
84 |
85 | @runtime_checkable
86 | class AnyArrayBufferView(Protocol):
87 | """
88 | A Protocol matching [JSJSTypedArray] and [JSJSDataView].
89 |
90 | [JSJSTypedArray]: `v8serialize.jstypes.JSJSTypedArray`
91 | [JSJSDataView]: `v8serialize.jstypes.JSJSDataView`
92 | """
93 |
94 | if TYPE_CHECKING:
95 |
96 | @property
97 | def view_tag(self) -> ArrayBufferViewTag: ...
98 | @property
99 | def byte_offset(self) -> int: ...
100 | @property
101 | def byte_length(self) -> int: ...
102 | @property
103 | def is_length_tracking(self) -> bool: ...
104 | @property
105 | def is_backing_buffer_resizable(self) -> bool: ...
106 |
107 | else:
108 | # test/test_protocol_dataclass_interaction.py
109 | view_tag = ...
110 | byte_offset = ...
111 | byte_length = ...
112 | is_length_tracking = ...
113 | is_backing_buffer_resizable = ...
114 |
115 |
116 | AnyArrayBufferData: TypeAlias = (
117 | "AnyArrayBuffer | AnySharedArrayBuffer | AnyArrayBufferTransfer"
118 | )
119 | """Any of the 3 ArrayBuffer types."""
120 |
121 | BufferT = TypeVar("BufferT")
122 | BufferT_co = TypeVar("BufferT_co", covariant=True)
123 | BufferT_con = TypeVar("BufferT_con", contravariant=True)
124 | ViewT = TypeVar("ViewT")
125 | ViewT_co = TypeVar("ViewT_co", covariant=True)
126 |
127 |
128 | class ArrayBufferConstructor(Protocol[BufferT_co]):
129 | """A function that creates a representation of a serialized ArrayBuffer."""
130 |
131 | @overload
132 | def __call__(
133 | self, data: memoryview, *, max_byte_length: None, resizable: Literal[False]
134 | ) -> BufferT_co: ...
135 |
136 | @overload
137 | def __call__(
138 | self, data: memoryview, *, max_byte_length: int, resizable: Literal[True]
139 | ) -> BufferT_co: ...
140 |
141 |
142 | class SharedArrayBufferConstructor(Protocol[BufferT_co]):
143 | """A function that creates a representation of a serialized SharedArrayBuffer."""
144 |
145 | def __call__(self, buffer_id: SharedArrayBufferId) -> BufferT_co: ...
146 |
147 |
148 | class ArrayBufferTransferConstructor(Protocol[BufferT_co]):
149 | """A function that creates a representation of a serialized ArrayBufferTransfer."""
150 |
151 | def __call__(self, transfer_id: TransferId) -> BufferT_co: ...
152 |
153 |
154 | class ArrayBufferViewConstructor(Protocol[BufferT_con, ViewT_co]):
155 | """A function that creates a representation of a serialized ArrayBuffer view."""
156 |
157 | def __call__(
158 | self,
159 | buffer: BufferT_con,
160 | format: ArrayBufferViewTag,
161 | *,
162 | byte_offset: int,
163 | byte_length: int | None,
164 | ) -> ViewT_co: ...
165 |
166 |
167 | class AnyJSError(Protocol):
168 | """
169 | A Protocol matching [JSError] and [JSErrorData].
170 |
171 | [JSError]: `v8serialize.jstypes.JSError`
172 | [JSErrorData]: `v8serialize.jstypes.JSErrorData`
173 | """
174 |
175 | # properties from protocols mess up concrete classes if they exist at runtime
176 | if TYPE_CHECKING:
177 |
178 | @property
179 | def name(self) -> str | JSErrorName: ...
180 | @name.setter
181 | def name(self, name: JSErrorName) -> None: ...
182 |
183 | message: str | None
184 | stack: str | None
185 | cause: object | None
186 |
187 |
188 | class JSErrorBuilder(Protocol[T_co]):
189 | """A function that creates a representation of a serialized Error."""
190 |
191 | def __call__(self, partial: AnyJSError, /) -> tuple[T_co, AnyJSError]: ...
192 |
--------------------------------------------------------------------------------
/src/v8serialize/_versions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 |
5 | from packaging.version import VERSION_PATTERN, InvalidVersion, Version
6 |
7 | LENIENT_VERSION_PATTERN = re.compile(
8 | f"^(?P{VERSION_PATTERN})(?:-(?P\\S+))?$", re.VERBOSE
9 | )
10 |
11 |
12 | def parse_lenient_version(version: str) -> Version:
13 | """
14 | Parse a version number less strictly than `packaging.version.parse()`.
15 |
16 | Versions can have arbitrary suffixes after a `-`, like `12.9.202.2-rusty`.
17 | These suffixes become the local part of a Python Version.
18 |
19 | >>> version = parse_lenient_version("12.9.202.2-rusty")
20 | >>> version
21 |
22 | >>> version.local
23 | 'rusty'
24 |
25 | >>> parse_lenient_version("12.9.202.2")
26 |
27 |
28 | >>> parse_lenient_version("afsdf") # doctest: +IGNORE_EXCEPTION_DETAIL
29 | Traceback (most recent call last):
30 | packaging.version.InvalidVersion: afsdf
31 | """
32 | match = LENIENT_VERSION_PATTERN.match(version)
33 | if not match:
34 | raise InvalidVersion(version)
35 |
36 | # We allow trailing local part starting with a dash. The Python version
37 | # expects a local part to start with a + and contain anything, but doesn't
38 | # allow a - suffix. Note that if the default pattern's local part matches,
39 | # we must have no suffix, as local will have consumed it all.
40 | lenient_suffix = match.group("suffix")
41 | if lenient_suffix:
42 | assert not match.group("local")
43 | return Version(f"{match.group('version')}+{lenient_suffix}")
44 | return Version(version)
45 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/__init__.py:
--------------------------------------------------------------------------------
1 | """Python representations of the JavaScript types in the V8 Serialization format."""
2 |
3 | from __future__ import annotations
4 |
5 | from v8serialize.constants import JSRegExpFlag as JSRegExpFlag
6 | from v8serialize.jstypes._equality import JSSameValueZero as JSSameValueZero
7 | from v8serialize.jstypes._equality import same_value_zero as same_value_zero
8 | from v8serialize.jstypes._repr import JSRepr as JSRepr
9 | from v8serialize.jstypes._repr import (
10 | JSReprSettingsNotRestored as JSReprSettingsNotRestored,
11 | )
12 | from v8serialize.jstypes._repr import default_js_repr as default_js_repr
13 | from v8serialize.jstypes._repr import js_repr_settings as js_repr_settings
14 | from v8serialize.jstypes.jsarray import JSArray as JSArray
15 | from v8serialize.jstypes.jsarrayproperties import JSHole as JSHole
16 | from v8serialize.jstypes.jsarrayproperties import JSHoleType as JSHoleType
17 | from v8serialize.jstypes.jsbigint import JSBigInt as JSBigInt
18 | from v8serialize.jstypes.jsbuffers import (
19 | ArrayBufferViewStructFormat as ArrayBufferViewStructFormat,
20 | )
21 | from v8serialize.jstypes.jsbuffers import (
22 | BoundsJSArrayBufferError as BoundsJSArrayBufferError,
23 | )
24 | from v8serialize.jstypes.jsbuffers import (
25 | ByteLengthJSArrayBufferError as ByteLengthJSArrayBufferError,
26 | )
27 | from v8serialize.jstypes.jsbuffers import DataFormat as DataFormat
28 | from v8serialize.jstypes.jsbuffers import DataType as DataType
29 | from v8serialize.jstypes.jsbuffers import DataViewBuffer as DataViewBuffer
30 | from v8serialize.jstypes.jsbuffers import (
31 | ItemSizeJSArrayBufferError as ItemSizeJSArrayBufferError,
32 | )
33 | from v8serialize.jstypes.jsbuffers import JSArrayBuffer as JSArrayBuffer
34 | from v8serialize.jstypes.jsbuffers import JSArrayBufferError as JSArrayBufferError
35 | from v8serialize.jstypes.jsbuffers import JSArrayBufferTransfer as JSArrayBufferTransfer
36 | from v8serialize.jstypes.jsbuffers import JSBigInt64Array as JSBigInt64Array
37 | from v8serialize.jstypes.jsbuffers import JSBigUint64Array as JSBigUint64Array
38 | from v8serialize.jstypes.jsbuffers import JSDataView as JSDataView
39 | from v8serialize.jstypes.jsbuffers import JSFloat16Array as JSFloat16Array
40 | from v8serialize.jstypes.jsbuffers import JSFloat32Array as JSFloat32Array
41 | from v8serialize.jstypes.jsbuffers import JSFloat64Array as JSFloat64Array
42 | from v8serialize.jstypes.jsbuffers import JSInt8Array as JSInt8Array
43 | from v8serialize.jstypes.jsbuffers import JSInt16Array as JSInt16Array
44 | from v8serialize.jstypes.jsbuffers import JSInt32Array as JSInt32Array
45 | from v8serialize.jstypes.jsbuffers import JSSharedArrayBuffer as JSSharedArrayBuffer
46 | from v8serialize.jstypes.jsbuffers import JSTypedArray as JSTypedArray
47 | from v8serialize.jstypes.jsbuffers import JSUint8Array as JSUint8Array
48 | from v8serialize.jstypes.jsbuffers import JSUint8ClampedArray as JSUint8ClampedArray
49 | from v8serialize.jstypes.jsbuffers import JSUint16Array as JSUint16Array
50 | from v8serialize.jstypes.jsbuffers import JSUint32Array as JSUint32Array
51 | from v8serialize.jstypes.jsbuffers import create_view as create_view
52 | from v8serialize.jstypes.jserror import JSError as JSError
53 | from v8serialize.jstypes.jserror import JSErrorData as JSErrorData
54 | from v8serialize.jstypes.jsmap import JSMap as JSMap
55 | from v8serialize.jstypes.jsobject import JSObject as JSObject
56 | from v8serialize.jstypes.jsprimitiveobject import (
57 | FalseJSPrimitiveObject as FalseJSPrimitiveObject,
58 | )
59 | from v8serialize.jstypes.jsprimitiveobject import JSPrimitiveObject as JSPrimitiveObject
60 | from v8serialize.jstypes.jsprimitiveobject import (
61 | NumberJSPrimitiveObject as NumberJSPrimitiveObject,
62 | )
63 | from v8serialize.jstypes.jsprimitiveobject import (
64 | PrimitiveObjectValue as PrimitiveObjectValue,
65 | )
66 | from v8serialize.jstypes.jsprimitiveobject import (
67 | StringJSPrimitiveObject as StringJSPrimitiveObject,
68 | )
69 | from v8serialize.jstypes.jsprimitiveobject import (
70 | TrueJSPrimitiveObject as TrueJSPrimitiveObject,
71 | )
72 | from v8serialize.jstypes.jsprimitiveobject import (
73 | UnknownJSPrimitiveObject as UnknownJSPrimitiveObject,
74 | )
75 | from v8serialize.jstypes.jsregexp import JSRegExp as JSRegExp
76 | from v8serialize.jstypes.jsset import JSSet as JSSet
77 | from v8serialize.jstypes.jsundefined import JSUndefined as JSUndefined
78 | from v8serialize.jstypes.jsundefined import JSUndefinedType as JSUndefinedType
79 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/_equality.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import isnan
4 | from typing import Final, NewType
5 |
6 | from v8serialize.jstypes.jsundefined import JSUndefinedEnum
7 |
8 | _nankey: Final = (float("nan"),) # equal thanks to tuple identity
9 |
10 | JSSameValueZero = NewType("JSSameValueZero", object)
11 | """
12 | The type of the opaque values returned by [`same_value_zero`].
13 |
14 | [`same_value_zero`]: `v8serialize.jstypes.same_value_zero`
15 | """
16 |
17 |
18 | def same_value_zero(value: object) -> JSSameValueZero:
19 | """
20 | Get a surrogate value that follows [JavaScript same-value-zero equality rules][samevaluezero].
21 |
22 | Python values can be compared according to same-value-zero by using `==`,
23 | `hash()` on the result of calling this function on the values, rather than
24 | on the values them directly. Like a key function when sorting.
25 |
26 | `same_value_zero(x) == same_value_zero(y)` is `True` if `x` and `y` are
27 | equal under [JavaScript's same-value-zero rule][samevaluezero].
28 |
29 | [samevaluezero]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/\
30 | Equality_comparisons_and_sameness#same-value-zero_equality
31 |
32 | Parameters
33 | ----------
34 | value
35 | Any Python object
36 |
37 | Returns
38 | -------
39 | :
40 | An opaque value that follows the same-value-zero rules when compared
41 | with `==` or passed to `hash()`.
42 |
43 |
44 | Examples
45 | --------
46 | >>> NaN = float('nan')
47 | >>> NaN == NaN
48 | False
49 | >>> same_value_zero(NaN) == same_value_zero(NaN)
50 | True
51 |
52 | >>> True == 1
53 | True
54 | >>> same_value_zero(True) == same_value_zero(1)
55 | False
56 |
57 | >>> l1, l2 = [0], [0]
58 | >>> l1 is l2
59 | False
60 | >>> l1 == l2
61 | True
62 | >>> same_value_zero(l1) == same_value_zero(l2)
63 | False
64 | >>> same_value_zero(l1) == same_value_zero(l1)
65 | True
66 |
67 | Strings and numbers are equal by value.
68 |
69 | >>> s1, s2 = str([ord('a')]), str([ord('a')])
70 | >>> s1 is s2
71 | False
72 | >>> s1 == s2
73 | True
74 | >>> same_value_zero(s1) == same_value_zero(s2)
75 | True
76 | >>> same_value_zero(1.0) == same_value_zero(1)
77 | True
78 | """ # noqa: E501
79 | # These values are equal by value.
80 | if isinstance(value, bool):
81 | # bools are equal to 0 and 1 by default
82 | return (bool, value) # type: ignore[return-value]
83 | elif isinstance(value, (int, float)):
84 | if isnan(value):
85 | # Represent nan as a value equal to itself.
86 | return _nankey # type: ignore[return-value]
87 | return value # type: ignore[return-value]
88 |
89 | # bytes is included here despite not existing in JS. I don't think it makes
90 | # sense to consider bytes equal by identity; it's a type that would be
91 | # equal under these rules if it did exist in JS.
92 | elif isinstance(value, (JSUndefinedEnum, type(None), str, bytes)):
93 | return value # type: ignore[return-value]
94 | # Everything else is equal by object identity. Wrap id in a tuple to avoid
95 | # clashing with int.
96 | return (id(value),) # type: ignore[return-value]
97 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/_normalise_property_key.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from v8serialize.jstypes.jsarrayproperties import MAX_ARRAY_LENGTH
4 |
5 |
6 | def canonical_numeric_index_string(value: str) -> int | None:
7 | """Get the int representation of value or None.
8 |
9 | The result is None unless interpreting value as a base10 int and back to a
10 | string is equal to value.
11 |
12 | This is very similar to the ECMA spec's function, except that negative
13 | values are returned as None.
14 | https://tc39.es/ecma262/#sec-canonicalnumericindexstring
15 | """
16 | # isdecimal includes non-ascii decimal numbers.
17 | if value.isdecimal() and value.isascii():
18 | int_value = int(value)
19 | # numbers with unnecessary leading zeros are not canonical
20 | return None if value[0] == "0" and value != "0" else int_value
21 | return None
22 |
23 |
24 | def normalise_property_key(key: str | int | float) -> str | int:
25 | """Get the canonical representation of a JavaScript property key as int or str.
26 |
27 | A key is an int if the str value is the base10 representation of the same
28 | integer and falls in the inclusive range 0..2**32-2 (which is the max
29 | JavaScript array index).
30 |
31 | We support floats as input as well as ints, because V8-serialized JSObject
32 | data can store keys as floating point values. Handling these in the same way
33 | as JavaScript requires some care, so by doing it here we can remove the need
34 | for users or other parts of the API to know about the differences.
35 |
36 | >>> normalise_property_key('3')
37 | 3
38 | >>> normalise_property_key('A')
39 | 'A'
40 | >>> normalise_property_key('-3')
41 | '-3'
42 | >>> normalise_property_key(1.0)
43 | 1
44 | >>> normalise_property_key("-0")
45 | '-0'
46 | >>> normalise_property_key(-0.0)
47 | 0
48 | >>> normalise_property_key(-1.0)
49 | '-1'
50 | >>> normalise_property_key(-1.5)
51 | '-1.5'
52 |
53 | This reflects the behaviour defined in: https://tc39.es/ecma262/#integer-index
54 | """
55 | if isinstance(key, str):
56 | int_value = canonical_numeric_index_string(key)
57 | if int_value is None:
58 | return key
59 | key = int_value
60 |
61 | if isinstance(key, int) or key.is_integer():
62 | if 0 <= key < MAX_ARRAY_LENGTH:
63 | return int(key)
64 | # Format out-of-range integer floats without decimal point, as JS does
65 | # not use them for integer numbers.
66 | key = int(key)
67 | return str(key)
68 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/_v8.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from abc import ABC
4 | from dataclasses import dataclass
5 | from typing import NewType
6 |
7 | from v8serialize._pycompat.dataclasses import slots_if310
8 |
9 | V8SharedValueId = NewType("V8SharedValueId", int)
10 |
11 |
12 | @dataclass(frozen=True, **slots_if310())
13 | class V8SharedObjectReference(ABC):
14 | """Represents an inaccessible shared object in a V8 process.
15 |
16 | This can only be used to round-trip a value back to V8. It should resolve to
17 | an actual JavaScript value. There's no real use-case for this type, it only
18 | really exists to avoid being unable to deserialize a a larger object graph
19 | that happens to contain a shared object that is of no consequence.
20 | """
21 |
22 | shared_value_id: V8SharedValueId
23 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/_v8traceback.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Iterable, Sequence
4 | from itertools import zip_longest
5 | from traceback import FrameSummary, TracebackException
6 | from typing import Generator, cast
7 |
8 |
9 | def format_exception_for_v8(
10 | tbe: TracebackException, group_path: Sequence[int] = ()
11 | ) -> Generator[str]:
12 | r"""Render a Python exception in the style of a V8 Error stack trace.
13 |
14 | Returns an iterable of strings ending with `"\n"`. (Join the lines to get a
15 | complete stack trace.)
16 |
17 | This aims to follow the layout described in https://v8.dev/docs/stack-trace-api
18 |
19 | V8 stack traces are in the reverse order of Python — most recent call first.
20 | They also omit details of of the the source code line that each stack entry
21 | corresponds to. For example, in V8 format:
22 |
23 | ```
24 | ValueError: Unable to do the thing
25 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:66:12)
26 | at raise_simple (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:11:8)
27 | at call_and_capture_tbe (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:82:8)
28 | ```
29 |
30 | vs Python:
31 |
32 | ```
33 | Traceback (most recent call last):
34 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 82, in call_and_capture_tbe
35 | fn()
36 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 11, in raise_simple
37 | self.failing_operation()
38 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 66, in failing_operation
39 | raise ValueError("Unable to do the thing")
40 | ValueError: Unable to do the thing
41 | ```
42 |
43 | V8's JavaScript Errors don't support the __context__ feature of Python
44 | exceptions, and V8 serialization doesn't support JavaScript's
45 | AggregateError, but it does support linking exceptions via `cause`. With this
46 | in mind, this format encodes details of context exceptions and
47 | sub-exceptions of exception groups in the string of the root exception. It
48 | does not include cause exceptions, because causes can be represented
49 | natively as `cause`.
50 | """ # noqa: E501
51 | yield from format_v8_stack(tbe, group_path=group_path)
52 |
53 | for related in walk_related(tbe):
54 | yield from [
55 | "\n",
56 | "The above exception occurred while handling another exception:\n",
57 | "\n",
58 | ]
59 | yield from format_v8_stack(related, group_path=())
60 |
61 |
62 | def walk_related(tbe: TracebackException) -> Generator[TracebackException]:
63 | while tbe.__context__ and not tbe.__suppress_context__:
64 | yield tbe.__context__
65 | tbe = tbe.__context__
66 |
67 |
68 | def format_v8_stack(
69 | tbe: TracebackException, *, group_path: Sequence[int]
70 | ) -> Generator[str]:
71 | # e.g. "ValueError: foo must be positive"
72 | yield from tbe.format_exception_only()
73 |
74 | if tbe.stack:
75 | # e.g. " at main (/foo/bar.py:8:4)"
76 | yield from (format_v8_frame(fs) for fs in reversed(tbe.stack))
77 |
78 | sub_exceptions: list[TracebackException] | None = getattr(
79 | tbe, "exceptions", None
80 | ) # 3.11+
81 | if sub_exceptions:
82 | for i, sub_tbe in enumerate(
83 | cast(Sequence[TracebackException], sub_exceptions), start=1
84 | ):
85 | sub_group_path = [*group_path, i]
86 | yield "\n"
87 | prefixes = [f" ↳ {format_group_path(sub_group_path)}: ", " "]
88 | yield from prefix_lines(
89 | format_exception_for_v8(sub_tbe, group_path=sub_group_path), prefixes
90 | )
91 |
92 |
93 | def format_group_path(group_path: Sequence[int]) -> str:
94 | return ".".join(map(str, group_path))
95 |
96 |
97 | def prefix_lines(lines: Iterable[str], prefixes: Iterable[str]) -> Generator[str]:
98 | last_prefix = ""
99 | for line, prefix in zip_longest(lines, prefixes):
100 | if line is None:
101 | return
102 | if prefix is None:
103 | prefix = last_prefix
104 | else:
105 | last_prefix = prefix
106 | result = f"{prefix}{line}"
107 | yield result.lstrip(" ") if result.isspace() else result
108 |
109 |
110 | def format_v8_frame(fs: FrameSummary) -> str:
111 | return f" at {fs.name} {format_v8_frame_location(fs)}\n"
112 |
113 |
114 | def format_v8_frame_location(fs: FrameSummary) -> str:
115 | if fs.filename is None and fs.lineno is None and fs.colno is None:
116 | return "(unknown location)"
117 |
118 | filename = sub_none(fs.filename, "")
119 | lineno = sub_none(fs.lineno, "")
120 | # colno available from 3.11
121 | colno = sub_none(getattr(fs, "colno", None), "")
122 |
123 | return f"({filename}:{lineno}:{colno})"
124 |
125 |
126 | def sub_none(value: object, none_substitute: str) -> str:
127 | return none_substitute if value is None else str(value)
128 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/jsregexp.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 | from dataclasses import dataclass, field
5 | from re import Pattern, compile
6 | from typing import AnyStr, Literal, overload
7 |
8 | from v8serialize._errors import JSRegExpV8SerializeError
9 | from v8serialize._pycompat.dataclasses import slots_if310
10 | from v8serialize._pycompat.re import RegexFlag
11 | from v8serialize.constants import JSRegExpFlag
12 |
13 |
14 | @dataclass(frozen=True, order=True, **slots_if310())
15 | class JSRegExp:
16 | """The data represented by a [JavaScript RegExp].
17 |
18 | Note that while Python and JavaScript Regular Expressions are similar, they
19 | each have features and syntax not supported by the other. Simple expressions
20 | will work the same in both languages, but this is not the case in general.
21 |
22 | **`JSRegExp` does not support matching text with the RegExp**, but
23 | [`as_python_pattern()`] can work for patterns that use compatible syntax and
24 | flags.
25 |
26 | [`as_python_pattern()`]: `v8serialize.jstypes.JSRegExp.as_python_pattern`
27 | [JavaScript RegExp]: https://developer.mozilla.org/en-US/docs/Web/\
28 | JavaScript/Reference/Global_Objects/RegExp
29 | """
30 |
31 | source: str
32 | flags: JSRegExpFlag = field(default=JSRegExpFlag.NoFlag)
33 |
34 | def __post_init__(self) -> None:
35 | if self.source == "":
36 | # JavaScript regexes cannot be empty, because the slash-delimited
37 | # literal syntax would be the same as a comment. Empty regexes are
38 | # represented as an empty non-capturing group.
39 | object.__setattr__(self, "source", "(?:)")
40 | if self.flags & JSRegExpFlag.Unicode and self.flags & JSRegExpFlag.UnicodeSets:
41 | raise ValueError(
42 | "The Unicode and UnicodeSets flags cannot be set together: "
43 | "Setting both is a syntax error in JavaScript because they "
44 | "enable incompatible interpretations of the RegExp source."
45 | )
46 |
47 | @overload
48 | @staticmethod
49 | def from_python_pattern(
50 | pattern: re.Pattern[AnyStr], throw: Literal[False]
51 | ) -> JSRegExp | None: ...
52 |
53 | @overload
54 | @staticmethod
55 | def from_python_pattern(
56 | pattern: re.Pattern[AnyStr], throw: Literal[True] = True
57 | ) -> JSRegExp: ...
58 |
59 | @staticmethod
60 | def from_python_pattern(
61 | pattern: re.Pattern[AnyStr], throw: bool = True
62 | ) -> JSRegExp | None:
63 | """Naively create a JSRegExp with an un-translated Python re.Pattern.
64 |
65 | As with `as_python_pattern()` this can result in JSRegExp objects that
66 | won't behave on the JavaScript side in the same way as in Python.
67 |
68 | This can fail if the Python pattern has `re.VERBOSE` set, as there's
69 | no equivalent JavaScript flag.
70 | """
71 | if isinstance(pattern.pattern, bytes):
72 | source = pattern.pattern.decode()
73 | else:
74 | source = pattern.pattern
75 | try:
76 | flags = JSRegExpFlag.from_python_flags(RegexFlag(pattern.flags))
77 | except JSRegExpV8SerializeError as e:
78 | if throw:
79 | raise JSRegExpV8SerializeError(
80 | f"Python re.Pattern flags cannot be represented by "
81 | f"JavaScript RegExp: {e}"
82 | ) from e
83 | return None
84 | return JSRegExp(source, flags=flags)
85 |
86 | @overload
87 | def as_python_pattern(self, throw: Literal[False]) -> Pattern[str] | None: ...
88 |
89 | @overload
90 | def as_python_pattern(self, throw: Literal[True] = True) -> Pattern[str]: ...
91 |
92 | def as_python_pattern(self, throw: bool = True) -> Pattern[str] | None:
93 | """Naively compile the JavaScript RegExp as a Python re.Pattern.
94 |
95 | The pattern may fail to compile due to syntax incompatibility, or may
96 | compile but behave incorrectly due to differences between Python and
97 | JavaScript's regular expression support.
98 | """
99 | # There is https://github.com/Zac-HD/js-regex but it seems to be a proof
100 | # of concept in that it doesn't fully parse and translate the regex AST,
101 | # so some patterns still don't work.
102 | try:
103 | return compile(self.source, self.flags.as_python_flags())
104 | except (JSRegExpV8SerializeError, re.error) as e:
105 | if throw:
106 | raise JSRegExpV8SerializeError(
107 | f"JSRegExp is not a valid Python re.Pattern: {e}"
108 | ) from e
109 | return None
110 |
--------------------------------------------------------------------------------
/src/v8serialize/jstypes/jsundefined.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from enum import Enum
4 | from typing import TYPE_CHECKING, Final, Literal
5 |
6 | if TYPE_CHECKING:
7 | from typing_extensions import TypeAlias
8 |
9 |
10 | class JSUndefinedEnum(Enum):
11 | """Defines the JSUndefined enum value."""
12 |
13 | JSUndefined = "JSUndefined"
14 | """Represents the JavaScript value `undefined`."""
15 |
16 | def __repr__(self) -> str:
17 | return self.name
18 |
19 | def __str__(self) -> str:
20 | return self.name
21 |
22 |
23 | JSUndefinedType: TypeAlias = Literal[JSUndefinedEnum.JSUndefined]
24 | JSUndefined: Final = JSUndefinedEnum.JSUndefined
25 | """Represents the JavaScript value `undefined`."""
26 |
--------------------------------------------------------------------------------
/src/v8serialize/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h4l/v8serialize/ad6b5782fb24396b3e7b1a8d7d51130913390dab/src/v8serialize/py.typed
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/test/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 | import sys
5 | from enum import Enum, auto
6 | from pathlib import Path
7 |
8 | from pytest_insta import Fmt
9 |
10 |
11 | class ExceptionColno(Enum):
12 | Supported = auto()
13 | Unsupported = auto()
14 |
15 | @staticmethod
16 | def get_current() -> ExceptionColno:
17 | return (
18 | ExceptionColno.Supported
19 | if sys.version_info >= (3, 11)
20 | else ExceptionColno.Unsupported
21 | )
22 |
23 | @property
24 | def is_current(self) -> bool:
25 | return self == ExceptionColno.get_current()
26 |
27 |
28 | class FmtException(Fmt[str]): # type: ignore[no-untyped-call] # __init_subclass__
29 | extension = ".exc.txt"
30 | project_dir: Path
31 | colno: ExceptionColno
32 |
33 | def __init__(self) -> None:
34 | self.project_dir = Path(__file__, "../..").resolve()
35 | self.project_dir_pattern = re.escape(str(Path(__file__, "../..").resolve()))
36 | self.colno = ExceptionColno.get_current()
37 |
38 | def with_absolute_paths(self, trace: str) -> str:
39 | return re.sub(r'(?<=[("])/…/v8serialize/', f"{self.project_dir}/", trace)
40 |
41 | def with_relative_paths(self, trace: str) -> str:
42 | return re.sub(f'(?<=[("]){self.project_dir_pattern}/', "/…/v8serialize/", trace)
43 |
44 | def with_unknown_colno(self, trace: str) -> str:
45 | return re.sub(
46 | r"^(\s+at .* \(.*:\d+):(\d+)\)$",
47 | r"\1:)",
48 | trace,
49 | flags=re.MULTILINE,
50 | )
51 |
52 | def load(self, path: Path) -> str:
53 | exception_trace = path.read_text()
54 | exception_trace = self.with_absolute_paths(exception_trace)
55 | if self.colno is ExceptionColno.Unsupported:
56 | exception_trace = self.with_unknown_colno(exception_trace)
57 | return exception_trace
58 |
59 | def dump(self, path: Path, value: str) -> None:
60 | exception_trace = self.with_relative_paths(value)
61 | if self.colno is ExceptionColno.Unsupported:
62 | raise AssertionError(
63 | f"""\
64 | Cannot dump a modified exception trace snapshot on a platform with \
65 | {self.colno}. You must (re)generate the snapshot on a platform that supports \
66 | column numbers in tracebacks (Python 3.11+) and then run tests on platforms \
67 | that lack column number support to verify the pre-generated snapshot \
68 | matches their behaviour (column numbers are ignored when comparing snapshots
69 | on platforms without column number support)."""
70 | )
71 | path.write_text(exception_trace)
72 |
--------------------------------------------------------------------------------
/test/jstypes/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/test/jstypes/_v8_traceback_error_fixtures.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from traceback import TracebackException
5 | from typing import Callable
6 | from typing_extensions import Never
7 |
8 | if sys.version_info < (3, 11):
9 | from exceptiongroup import ExceptionGroup
10 |
11 |
12 | class ErrorScenario:
13 | """
14 | Functions that raise errors with various attached context/exception groups.
15 |
16 | Used to generate well-known exceptions to test traceback formatting
17 | behaviour.
18 | """
19 |
20 | def raise_simple(self) -> Never:
21 | self.failing_operation()
22 |
23 | def raise_group(self) -> Never:
24 | try:
25 | self.failing_operation()
26 | except Exception as e:
27 | fail1 = e
28 |
29 | try:
30 | self.failing_operation2()
31 | except Exception as e:
32 | fail2 = e
33 |
34 | raise ExceptionGroup("Everything went wrong", [fail1, fail2])
35 |
36 | def raise_context(self) -> Never:
37 | try:
38 | self.failing_operation()
39 | except Exception as e:
40 | e / 0 # type: ignore[operator]
41 | raise AssertionError("unreachable") from e
42 |
43 | def raise_context_group(self) -> Never:
44 | try:
45 | self.raise_group()
46 | except Exception as e:
47 | e / 0 # type: ignore[operator]
48 | raise AssertionError("unreachable") from e
49 |
50 | def raise_group_with_context(self) -> Never:
51 | try:
52 | self.failing_operation_with_context()
53 | except Exception as e:
54 | fail1 = e
55 |
56 | try:
57 | self.failing_operation2()
58 | except Exception as e:
59 | fail2 = e
60 |
61 | raise ExceptionGroup("Everything went wrong", [fail1, fail2])
62 |
63 | def raise_sub_group_with_context(self) -> Never:
64 | try:
65 | self.raise_group_with_context()
66 | except Exception as e:
67 | fail1 = e
68 |
69 | try:
70 | self.raise_group()
71 | except Exception as e:
72 | fail2 = e
73 |
74 | raise ExceptionGroup("This is fine", [fail1, fail2])
75 |
76 | def failing_operation(self) -> Never:
77 | if True:
78 | raise ValueError("Unable to do the thing")
79 |
80 | def failing_operation2(self) -> Never:
81 | if True:
82 | raise TypeError("Expected an Apple but received an Orange")
83 |
84 | def failing_operation_with_context(self) -> Never:
85 | try:
86 | self.failing_operation()
87 | except Exception as e:
88 | e.missing_attribute.foo() # type: ignore[attr-defined]
89 | raise AssertionError("unreachable") from e
90 |
91 |
92 | def call_and_capture_tbe(fn: Callable[[], Never]) -> TracebackException:
93 | try:
94 | fn()
95 | except Exception as e:
96 | return TracebackException.from_exception(e)
97 | raise AssertionError("fn did not raise")
98 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__0.txt:
--------------------------------------------------------------------------------
1 | JSArray(c='C')
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__1.txt:
--------------------------------------------------------------------------------
1 | JSArray(c='C', ...)
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__2.txt:
--------------------------------------------------------------------------------
1 | JSArray({'!': 'C'})
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__3.txt:
--------------------------------------------------------------------------------
1 | JSArray({'!': 'C', ...})
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__4.txt:
--------------------------------------------------------------------------------
1 | JSArray(['a'])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__5.txt:
--------------------------------------------------------------------------------
1 | JSArray(['a', ...])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__6.txt:
--------------------------------------------------------------------------------
1 | JSArray(['a', ...], ...)
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray__7.txt:
--------------------------------------------------------------------------------
1 | JSArray(['a', ...], **{...})
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__0.txt:
--------------------------------------------------------------------------------
1 | JSArray(c='C')
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__1.txt:
--------------------------------------------------------------------------------
1 | JSArray(
2 | c='C',
3 | ...,
4 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__2.txt:
--------------------------------------------------------------------------------
1 | JSArray({'!': 'C'})
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__3.txt:
--------------------------------------------------------------------------------
1 | JSArray({
2 | '!': 'C',
3 | ...,
4 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__4.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | ])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__5.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | ...,
4 | ])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__6.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | ...,
4 | ],
5 | ...,
6 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxjsarray_indented__7.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | ...,
4 | ], **{
5 | ...,
6 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_maxlevel__0.txt:
--------------------------------------------------------------------------------
1 | ...
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__0.txt:
--------------------------------------------------------------------------------
1 | JSArray()
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__1.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | 'b',
4 | ])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__10.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | ...,
4 | ])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__11.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | JSObject(
3 | names=JSArray([
4 | 'Bill',
5 | 'Bob',
6 | ]),
7 | ),
8 | ])
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__2.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | 'b',
4 | ],
5 | x='y',
6 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__3.txt:
--------------------------------------------------------------------------------
1 | JSArray({
2 | 1000: 'a',
3 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__4.txt:
--------------------------------------------------------------------------------
1 | JSArray({
2 | 1000: 'a',
3 | },
4 | x='y',
5 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__5.txt:
--------------------------------------------------------------------------------
1 | JSArray(x=1)
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__6.txt:
--------------------------------------------------------------------------------
1 | JSArray(
2 | x=1,
3 | y=2,
4 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__7.txt:
--------------------------------------------------------------------------------
1 | JSArray({'!': 1})
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__8.txt:
--------------------------------------------------------------------------------
1 | JSArray({
2 | '!': 1,
3 | '!!': 2,
4 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsarray_repr__9.txt:
--------------------------------------------------------------------------------
1 | JSArray([
2 | 'a',
3 | ], **{
4 | '!': 1,
5 | '!!': 2,
6 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject__0.txt:
--------------------------------------------------------------------------------
1 | JSObject(a=1, ...)
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject__1.txt:
--------------------------------------------------------------------------------
1 | JSObject({'!': 1, ...})
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject__2.txt:
--------------------------------------------------------------------------------
1 | JSObject({0: 'a'}, ...)
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject_indented__0.txt:
--------------------------------------------------------------------------------
1 | JSObject(
2 | a=1,
3 | ...,
4 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject_indented__1.txt:
--------------------------------------------------------------------------------
1 | JSObject({
2 | '!': 1,
3 | ...,
4 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject_indented__2.txt:
--------------------------------------------------------------------------------
1 | JSObject({
2 | 0: 'a',
3 | },
4 | ...,
5 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_maxjsobject_indented__3.txt:
--------------------------------------------------------------------------------
1 | JSObject({
2 | 0: 'a',
3 | ...,
4 | },
5 | ...,
6 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_repr__0.txt:
--------------------------------------------------------------------------------
1 | JSObject()
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_repr__1.txt:
--------------------------------------------------------------------------------
1 | JSObject(
2 | z=1,
3 | b=2,
4 | c=3,
5 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_repr__2.txt:
--------------------------------------------------------------------------------
1 | JSObject({
2 | 1000: 'a',
3 | 1001: 'b',
4 | },
5 | z='other',
6 | x='X',
7 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_repr__3.txt:
--------------------------------------------------------------------------------
1 | JSObject({
2 | 'foo bar': 1,
3 | 'b': 2,
4 | 'c': 3,
5 | })
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_repr__4.txt:
--------------------------------------------------------------------------------
1 | JSObject(
2 | a=1,
3 | b=...,
4 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/repr__jsobject_repr__5.txt:
--------------------------------------------------------------------------------
1 | JSObject(
2 | a=JSArray([
3 | JSObject(name='Bob'),
4 | JSObject(
5 | name='Alice',
6 | id=2,
7 | ),
8 | ]),
9 | )
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_ExceptionGroup_py__0.exc.txt:
--------------------------------------------------------------------------------
1 | + Exception Group Traceback (most recent call last):
2 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 94, in call_and_capture_tbe
3 | | fn()
4 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 34, in raise_group
5 | | raise ExceptionGroup("Everything went wrong", [fail1, fail2])
6 | | ExceptionGroup: Everything went wrong (2 sub-exceptions)
7 | +-+---------------- 1 ----------------
8 | | Traceback (most recent call last):
9 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 25, in raise_group
10 | | self.failing_operation()
11 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 78, in failing_operation
12 | | raise ValueError("Unable to do the thing")
13 | | ValueError: Unable to do the thing
14 | +---------------- 2 ----------------
15 | | Traceback (most recent call last):
16 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 30, in raise_group
17 | | self.failing_operation2()
18 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 82, in failing_operation2
19 | | raise TypeError("Expected an Apple but received an Orange")
20 | | TypeError: Expected an Apple but received an Orange
21 | +------------------------------------
22 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_ExceptionGroup_v8__0.exc.txt:
--------------------------------------------------------------------------------
1 | ExceptionGroup: Everything went wrong (2 sub-exceptions)
2 | at raise_group (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:34:8)
3 | at call_and_capture_tbe (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:94:8)
4 |
5 | ↳ 1: ValueError: Unable to do the thing
6 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:78:12)
7 | at raise_group (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:25:12)
8 |
9 | ↳ 2: TypeError: Expected an Apple but received an Orange
10 | at failing_operation2 (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:82:12)
11 | at raise_group (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:30:12)
12 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_ExceptionGroup_with_context_py__0.exc.txt:
--------------------------------------------------------------------------------
1 | + Exception Group Traceback (most recent call last):
2 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 94, in call_and_capture_tbe
3 | | fn()
4 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 61, in raise_group_with_context
5 | | raise ExceptionGroup("Everything went wrong", [fail1, fail2])
6 | | ExceptionGroup: Everything went wrong (2 sub-exceptions)
7 | +-+---------------- 1 ----------------
8 | | Traceback (most recent call last):
9 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 86, in failing_operation_with_context
10 | | self.failing_operation()
11 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 78, in failing_operation
12 | | raise ValueError("Unable to do the thing")
13 | | ValueError: Unable to do the thing
14 | |
15 | | During handling of the above exception, another exception occurred:
16 | |
17 | | Traceback (most recent call last):
18 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 52, in raise_group_with_context
19 | | self.failing_operation_with_context()
20 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 88, in failing_operation_with_context
21 | | e.missing_attribute.foo() # type: ignore[attr-defined]
22 | | ^^^^^^^^^^^^^^^^^^^
23 | | AttributeError: 'ValueError' object has no attribute 'missing_attribute'
24 | +---------------- 2 ----------------
25 | | Traceback (most recent call last):
26 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 57, in raise_group_with_context
27 | | self.failing_operation2()
28 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 82, in failing_operation2
29 | | raise TypeError("Expected an Apple but received an Orange")
30 | | TypeError: Expected an Apple but received an Orange
31 | +------------------------------------
32 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_ExceptionGroup_with_context_v8__0.exc.txt:
--------------------------------------------------------------------------------
1 | ExceptionGroup: Everything went wrong (2 sub-exceptions)
2 | at raise_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:61:8)
3 | at call_and_capture_tbe (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:94:8)
4 |
5 | ↳ 1: AttributeError: 'ValueError' object has no attribute 'missing_attribute'
6 | at failing_operation_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:88:12)
7 | at raise_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:52:12)
8 |
9 | The above exception occurred while handling another exception:
10 |
11 | ValueError: Unable to do the thing
12 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:78:12)
13 | at failing_operation_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:86:12)
14 |
15 | ↳ 2: TypeError: Expected an Apple but received an Orange
16 | at failing_operation2 (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:82:12)
17 | at raise_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:57:12)
18 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_exception_with_context_py__0.exc.txt:
--------------------------------------------------------------------------------
1 | Traceback (most recent call last):
2 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 38, in raise_context
3 | self.failing_operation()
4 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 78, in failing_operation
5 | raise ValueError("Unable to do the thing")
6 | ValueError: Unable to do the thing
7 |
8 | During handling of the above exception, another exception occurred:
9 |
10 | Traceback (most recent call last):
11 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 94, in call_and_capture_tbe
12 | fn()
13 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 40, in raise_context
14 | e / 0 # type: ignore[operator]
15 | ~~^~~
16 | TypeError: unsupported operand type(s) for /: 'ValueError' and 'int'
17 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_exception_with_context_v8__0.exc.txt:
--------------------------------------------------------------------------------
1 | TypeError: unsupported operand type(s) for /: 'ValueError' and 'int'
2 | at raise_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:40:12)
3 | at call_and_capture_tbe (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:94:8)
4 |
5 | The above exception occurred while handling another exception:
6 |
7 | ValueError: Unable to do the thing
8 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:78:12)
9 | at raise_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:38:12)
10 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_nested_ExceptionGroups_with_context_py__0.exc.txt:
--------------------------------------------------------------------------------
1 | + Exception Group Traceback (most recent call last):
2 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 94, in call_and_capture_tbe
3 | | fn()
4 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 74, in raise_sub_group_with_context
5 | | raise ExceptionGroup("This is fine", [fail1, fail2])
6 | | ExceptionGroup: This is fine (2 sub-exceptions)
7 | +-+---------------- 1 ----------------
8 | | Exception Group Traceback (most recent call last):
9 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 65, in raise_sub_group_with_context
10 | | self.raise_group_with_context()
11 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 61, in raise_group_with_context
12 | | raise ExceptionGroup("Everything went wrong", [fail1, fail2])
13 | | ExceptionGroup: Everything went wrong (2 sub-exceptions)
14 | +-+---------------- 1 ----------------
15 | | Traceback (most recent call last):
16 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 86, in failing_operation_with_context
17 | | self.failing_operation()
18 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 78, in failing_operation
19 | | raise ValueError("Unable to do the thing")
20 | | ValueError: Unable to do the thing
21 | |
22 | | During handling of the above exception, another exception occurred:
23 | |
24 | | Traceback (most recent call last):
25 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 52, in raise_group_with_context
26 | | self.failing_operation_with_context()
27 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 88, in failing_operation_with_context
28 | | e.missing_attribute.foo() # type: ignore[attr-defined]
29 | | ^^^^^^^^^^^^^^^^^^^
30 | | AttributeError: 'ValueError' object has no attribute 'missing_attribute'
31 | +---------------- 2 ----------------
32 | | Traceback (most recent call last):
33 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 57, in raise_group_with_context
34 | | self.failing_operation2()
35 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 82, in failing_operation2
36 | | raise TypeError("Expected an Apple but received an Orange")
37 | | TypeError: Expected an Apple but received an Orange
38 | +------------------------------------
39 | +---------------- 2 ----------------
40 | | Exception Group Traceback (most recent call last):
41 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 70, in raise_sub_group_with_context
42 | | self.raise_group()
43 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 34, in raise_group
44 | | raise ExceptionGroup("Everything went wrong", [fail1, fail2])
45 | | ExceptionGroup: Everything went wrong (2 sub-exceptions)
46 | +-+---------------- 1 ----------------
47 | | Traceback (most recent call last):
48 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 25, in raise_group
49 | | self.failing_operation()
50 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 78, in failing_operation
51 | | raise ValueError("Unable to do the thing")
52 | | ValueError: Unable to do the thing
53 | +---------------- 2 ----------------
54 | | Traceback (most recent call last):
55 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 30, in raise_group
56 | | self.failing_operation2()
57 | | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 82, in failing_operation2
58 | | raise TypeError("Expected an Apple but received an Orange")
59 | | TypeError: Expected an Apple but received an Orange
60 | +------------------------------------
61 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_nested_ExceptionGroups_with_context_v8__0.exc.txt:
--------------------------------------------------------------------------------
1 | ExceptionGroup: This is fine (2 sub-exceptions)
2 | at raise_sub_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:74:8)
3 | at call_and_capture_tbe (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:94:8)
4 |
5 | ↳ 1: ExceptionGroup: Everything went wrong (2 sub-exceptions)
6 | at raise_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:61:8)
7 | at raise_sub_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:65:12)
8 |
9 | ↳ 1.1: AttributeError: 'ValueError' object has no attribute 'missing_attribute'
10 | at failing_operation_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:88:12)
11 | at raise_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:52:12)
12 |
13 | The above exception occurred while handling another exception:
14 |
15 | ValueError: Unable to do the thing
16 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:78:12)
17 | at failing_operation_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:86:12)
18 |
19 | ↳ 1.2: TypeError: Expected an Apple but received an Orange
20 | at failing_operation2 (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:82:12)
21 | at raise_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:57:12)
22 |
23 | ↳ 2: ExceptionGroup: Everything went wrong (2 sub-exceptions)
24 | at raise_group (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:34:8)
25 | at raise_sub_group_with_context (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:70:12)
26 |
27 | ↳ 2.1: ValueError: Unable to do the thing
28 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:78:12)
29 | at raise_group (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:25:12)
30 |
31 | ↳ 2.2: TypeError: Expected an Apple but received an Orange
32 | at failing_operation2 (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:82:12)
33 | at raise_group (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:30:12)
34 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_simple_exception_py__0.exc.txt:
--------------------------------------------------------------------------------
1 | Traceback (most recent call last):
2 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 94, in call_and_capture_tbe
3 | fn()
4 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 21, in raise_simple
5 | self.failing_operation()
6 | File "/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py", line 78, in failing_operation
7 | raise ValueError("Unable to do the thing")
8 | ValueError: Unable to do the thing
9 |
--------------------------------------------------------------------------------
/test/jstypes/snapshots/v8_traceback__format_exception_for_v8__represents_simple_exception_v8__0.exc.txt:
--------------------------------------------------------------------------------
1 | ValueError: Unable to do the thing
2 | at failing_operation (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:78:12)
3 | at raise_simple (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:21:8)
4 | at call_and_capture_tbe (/…/v8serialize/test/jstypes/_v8_traceback_error_fixtures.py:94:8)
5 |
--------------------------------------------------------------------------------
/test/jstypes/test__equality.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import isnan
4 |
5 | import pytest
6 | from hypothesis import given
7 |
8 | from test.strategies import values_and_objects as mk_values_and_objects
9 | from v8serialize.jstypes._equality import same_value_zero
10 | from v8serialize.jstypes.jsobject import JSObject
11 | from v8serialize.jstypes.jsprimitiveobject import JSPrimitiveObject
12 | from v8serialize.jstypes.jsundefined import JSUndefined
13 |
14 | values_and_objects = mk_values_and_objects(allow_nan=True, only_hashable=False)
15 |
16 |
17 | # Examples from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#comparing_equality_methods # noqa: E501
18 | @pytest.mark.parametrize(
19 | "x, y, equal",
20 | [
21 | (JSUndefined, JSUndefined, True),
22 | (None, None, True),
23 | (True, True, True),
24 | (False, False, True),
25 | ("foo", "foo", True),
26 | (b"foo", b"foo", True),
27 | (0, 0, True),
28 | (0.0, 0.0, True),
29 | (-0.0, -0.0, True),
30 | (0, False, False),
31 | (0.0, False, False),
32 | ("", False, False),
33 | (b"", False, False),
34 | ("", 0, False),
35 | (b"", 0, False),
36 | ("0", 0, False),
37 | ("17", 17, False),
38 | ([1, 2], "1,2", False),
39 | (JSPrimitiveObject("foo"), "foo", False),
40 | (None, JSUndefined, False),
41 | (None, False, False),
42 | (JSUndefined, False, False),
43 | (JSObject(foo="bar"), JSObject(foo="bar"), False),
44 | (JSPrimitiveObject("foo"), JSPrimitiveObject("foo"), False),
45 | (0, None, False),
46 | (0, float("nan"), False),
47 | (float("nan"), float("nan"), True),
48 | ],
49 | )
50 | def test_same_value_zero__mdn_examples(x: object, y: object, equal: bool) -> None:
51 | assert (same_value_zero(x) == same_value_zero(y)) is equal
52 |
53 |
54 | @given(x=values_and_objects, y=values_and_objects)
55 | def test_same_value_zero(x: object, y: object) -> None:
56 | both_same_constant = any(
57 | x is _ and y is _ for _ in [JSUndefined, None, True, False]
58 | )
59 | both_same_string = isinstance(x, str) and isinstance(y, str) and x == y
60 | both_same_bytes = isinstance(x, bytes) and isinstance(y, bytes) and x == y
61 | both_nan = isinstance(x, float) and isinstance(y, float) and isnan(x) and isnan(y)
62 | # note: -0.0 == 0.0 by default in Python
63 | both_equal_numbers = (
64 | isinstance(x, (int, float)) and isinstance(y, (int, float)) and x == y
65 | )
66 | both_same_object = x is y
67 | should_be_equal = (
68 | both_same_constant
69 | or both_same_string
70 | or both_same_bytes
71 | or both_nan
72 | or both_equal_numbers
73 | or both_same_object
74 | )
75 |
76 | assert (same_value_zero(x) == same_value_zero(y)) is should_be_equal
77 |
--------------------------------------------------------------------------------
/test/jstypes/test__v8_traceback.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 | import sys
5 | from typing import Callable, cast
6 | from typing_extensions import Never
7 |
8 | import pytest
9 | from pytest_insta import SnapshotFixture
10 |
11 | from test.jstypes._v8_traceback_error_fixtures import (
12 | ErrorScenario,
13 | call_and_capture_tbe,
14 | )
15 | from v8serialize.jstypes._v8traceback import format_exception_for_v8
16 |
17 | FmtExc = Callable[[Callable[[], Never]], str]
18 |
19 |
20 | def call_and_format_exception_for_v8(fn: Callable[[], Never]) -> str:
21 | result = "".join(format_exception_for_v8(call_and_capture_tbe(fn)))
22 |
23 | if sys.version_info < (3, 11):
24 | # we use the exceptiongroup backport before 3.11, and its ExceptionGroup
25 | # is prefixed with its module name in tracebacks. We remove the prefix
26 | # to ignore this difference.
27 | result = re.sub(r"\bexceptiongroup\.ExceptionGroup\b", "ExceptionGroup", result)
28 |
29 | print(result)
30 | return result
31 |
32 |
33 | def call_and_format_exception_for_python(fn: Callable[[], Never]) -> str:
34 | result = "".join(call_and_capture_tbe(fn).format())
35 | print(result)
36 | return result
37 |
38 |
39 | @pytest.fixture(
40 | params=[
41 | pytest.param(call_and_format_exception_for_v8, id="v8"),
42 | pytest.param(
43 | call_and_format_exception_for_python,
44 | id="py",
45 | marks=pytest.mark.skipif(
46 | sys.version_info[:2] != (3, 12),
47 | reason="py traceback generated on py3.12 only as a reference "
48 | 'for "correct" behaviour',
49 | ),
50 | ),
51 | ]
52 | )
53 | def fmt_exc(request: pytest.FixtureRequest) -> FmtExc:
54 | return cast(FmtExc, request.param)
55 |
56 |
57 | @pytest.fixture
58 | def errors() -> ErrorScenario:
59 | return ErrorScenario()
60 |
61 |
62 | def test_format_exception_for_v8__represents_simple_exception(
63 | snapshot: SnapshotFixture, fmt_exc: FmtExc, errors: ErrorScenario
64 | ) -> None:
65 | assert snapshot(".exc.txt") == fmt_exc(errors.raise_simple)
66 |
67 |
68 | def test_format_exception_for_v8__represents_exception_with_context(
69 | snapshot: SnapshotFixture, fmt_exc: FmtExc, errors: ErrorScenario
70 | ) -> None:
71 | assert snapshot(".exc.txt") == fmt_exc(errors.raise_context)
72 |
73 |
74 | def test_format_exception_for_v8__represents_ExceptionGroup(
75 | snapshot: SnapshotFixture, fmt_exc: FmtExc, errors: ErrorScenario
76 | ) -> None:
77 | assert snapshot(".exc.txt") == fmt_exc(errors.raise_group)
78 |
79 |
80 | def test_format_exception_for_v8__represents_ExceptionGroup_with_context(
81 | snapshot: SnapshotFixture, fmt_exc: FmtExc, errors: ErrorScenario
82 | ) -> None:
83 | assert snapshot(".exc.txt") == fmt_exc(errors.raise_group_with_context)
84 |
85 |
86 | def test_format_exception_for_v8__represents_nested_ExceptionGroups_with_context(
87 | snapshot: SnapshotFixture, fmt_exc: FmtExc, errors: ErrorScenario
88 | ) -> None:
89 | assert snapshot(".exc.txt") == fmt_exc(errors.raise_sub_group_with_context)
90 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsarray.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Sequence
4 |
5 | from v8serialize.jstypes.jsarray import JSArray
6 | from v8serialize.jstypes.jsarrayproperties import JSHole, JSHoleType
7 | from v8serialize.jstypes.jsobject import JSObject
8 |
9 |
10 | def test_init__array() -> None:
11 | assert dict(JSArray([])) == {}
12 |
13 | items1: list[str] = ["a", "b"]
14 | assert dict(JSArray[str](items1)) == {0: "a", 1: "b"}
15 |
16 | items2: list[str | JSHoleType] = ["a", "b", JSHole, "d"]
17 | assert dict(JSArray[str](items2)) == {0: "a", 1: "b", 3: "d"}
18 |
19 | assert dict(JSArray[str]([JSHole, "b"], x="X")) == {1: "b", "x": "X"}
20 | assert dict(JSArray[str](["a", "b"], x="X", **{"0": "override"})) == (
21 | {0: "override", 1: "b", "x": "X"}
22 | )
23 |
24 |
25 | def test_init__mapping() -> None:
26 | assert dict(JSArray({})) == {}
27 |
28 | items1: dict[str | int, str] = {0: "a", "1": "b"}
29 | assert dict(JSArray[str](items1)) == {0: "a", 1: "b"}
30 |
31 | items2: dict[str | int, str] = {0: "a", 1: "b", 3: "d"}
32 | assert dict(JSArray[str](items2)) == {0: "a", 1: "b", 3: "d"}
33 |
34 | assert dict(JSArray[str]({1: "b"}, x="X")) == {1: "b", "x": "X"}
35 | assert dict(JSArray[str]({0: "a", 1: "b"}, x="X", **{"0": "override"})) == (
36 | {0: "override", 1: "b", "x": "X"}
37 | )
38 |
39 |
40 | def test_jsarray_is_not_sequence() -> None:
41 | assert not isinstance(JSArray(), Sequence)
42 | # Its array properties are though
43 | assert isinstance(JSArray().array, Sequence)
44 |
45 |
46 | def test_abc_registration() -> None:
47 | class Example:
48 | pass
49 |
50 | assert not isinstance(Example(), JSArray)
51 | assert not isinstance(Example(), JSObject)
52 | JSArray.register(Example)
53 | assert isinstance(Example(), JSArray)
54 | # Registered objects also become subtypes of JSObject, because JSArray is.
55 | assert isinstance(Example(), JSObject)
56 |
57 |
58 | def test_eq() -> None:
59 | assert JSArray() == JSArray()
60 | assert JSArray(**{"0": 1}, a=1, b=2) == JSArray(**{"0": 1}, a=1, b=2)
61 | assert JSArray(a=1, b=2) == JSArray(a=1, b=2)
62 |
63 | assert JSArray() != []
64 | assert JSArray([1, 2]) != [1, 2]
65 | assert JSArray([1, JSHole, 2]) != [1, 2]
66 | assert JSArray([], a=1) != []
67 | assert JSArray([], a=1) != {"a": 1}
68 | assert JSArray([], a=1) != JSObject({"a": 1})
69 |
70 | # Objects are not equal to equivalent arrays, like dicts are not equal to lists
71 | assert JSObject() != JSArray()
72 | assert JSObject(**{"0": 1}) != JSArray([1])
73 | assert JSObject(a=1) != JSArray(a=1)
74 |
75 |
76 | def test_eq__cycle_direct() -> None:
77 | # Objects that contain reference cycles with the same identity structure are
78 | # equal. See test__recursive_eq.py.
79 | x = JSArray[object](a=1)
80 | x["b"] = x
81 | y = JSArray[object](a=1)
82 | y["b"] = y
83 |
84 | assert x == y
85 |
86 |
87 | def test__eq__cycle_direct_unequal() -> None:
88 | x = JSArray[object](a=1)
89 | x["b"] = x
90 |
91 | # Same shape as l, but identity is different as r and _r repeat alternately
92 | y_ = JSArray[object](a=1)
93 | y = JSArray[object](a=1)
94 | y_["b"] = y
95 | y["b"] = y_
96 |
97 | assert x != y
98 |
99 |
100 | def test__eq__cycle_indirect() -> None:
101 | # Objects that contain reference cycles with the same identity structure are
102 | # equal. See test__recursive_eq.py.
103 | x = JSArray(a=1, b=(x_b := JSObject()))
104 | x_b["c"] = x
105 | y = JSArray(a=1, b=(y_b := JSObject()))
106 | y_b["c"] = y
107 |
108 | assert x == y
109 |
110 |
111 | def test__eq__cycle_indirect_unequal() -> None:
112 | x = JSArray(a=1, b=(x_b := JSArray()))
113 | x_b["c"] = x
114 |
115 | # Same shape as l, but identity is different as r and _r repeat alternately
116 | y_ = JSArray(a=1, b=(y__b := JSObject()))
117 | y = JSArray(a=1, b=(y_b := JSObject()))
118 | y__b["c"] = y
119 | y_b["c"] = y_
120 |
121 | assert x != y
122 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsbigint.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import operator
4 | import sys
5 | from typing import Any
6 |
7 | from test.utils import typeval
8 | from v8serialize.jstypes.jsbigint import JSBigInt
9 |
10 |
11 | def test_repr() -> None:
12 | assert repr(JSBigInt(42)) == "JSBigInt(42)"
13 |
14 |
15 | def test_is_instanceof_int() -> None:
16 | assert isinstance(JSBigInt(42), int)
17 |
18 |
19 | def check_int_attr(name: str, *, value: int = 42) -> None:
20 | jsbigint = JSBigInt(value)
21 | i = int(value)
22 | assert typeval(getattr(jsbigint, name)) == (JSBigInt, getattr(i, name))
23 |
24 |
25 | def check_int_method(name: str, *args: Any, value: int = 42) -> None:
26 | jsbigint = JSBigInt(value)
27 | int_value = int(value)
28 |
29 | expected: int | tuple[int, ...] = getattr(int_value, name)(*args)
30 | actual: JSBigInt | tuple[JSBigInt, ...] = getattr(jsbigint, name)(*args)
31 |
32 | if isinstance(expected, tuple):
33 | assert isinstance(actual, tuple)
34 | assert expected == actual
35 | assert set(type(v) for v in actual) == {JSBigInt}
36 | else:
37 | assert typeval(actual) == (JSBigInt, expected)
38 |
39 |
40 | def test_methods_and_operators_return_JSBigInt() -> None:
41 | """All the methods returning int values return JSBigInt."""
42 | # Same order as methods defined in JSBigInt
43 | check_int_method("as_integer_ratio")
44 | check_int_attr("real")
45 | check_int_attr("imag")
46 | check_int_attr("numerator")
47 | check_int_attr("denominator")
48 | check_int_method("conjugate")
49 | check_int_method("__add__", 2)
50 | check_int_method("__sub__", 2)
51 | check_int_method("__mul__", 2)
52 | check_int_method("__floordiv__", 2)
53 | check_int_method("__mod__", 2)
54 | check_int_method("__divmod__", 2)
55 | check_int_method("__radd__", 2)
56 | check_int_method("__rsub__", 2)
57 | check_int_method("__rmul__", 2)
58 | check_int_method("__rfloordiv__", 2)
59 | check_int_method("__rmod__", 2)
60 | check_int_method("__rdivmod__", 2)
61 | check_int_method("__pow__", 2)
62 | check_int_method("__pow__", 2, 100)
63 | check_int_method("__rpow__", 2)
64 | check_int_method("__rpow__", 2, 100)
65 | check_int_method("__and__", 2)
66 | check_int_method("__or__", 2)
67 | check_int_method("__xor__", 2)
68 | check_int_method("__lshift__", 2)
69 | check_int_method("__rshift__", 2)
70 | check_int_method("__rand__", 2)
71 | check_int_method("__ror__", 2)
72 | check_int_method("__rxor__", 2)
73 | check_int_method("__rlshift__", 2)
74 | check_int_method("__rrshift__", 2)
75 | check_int_method("__neg__")
76 | check_int_method("__pos__")
77 | check_int_method("__invert__")
78 | check_int_method("__trunc__")
79 | check_int_method("__ceil__")
80 | check_int_method("__floor__")
81 | check_int_method("__round__")
82 | check_int_method("__round__", 2)
83 | assert typeval(float(JSBigInt(42))) == (float, 42.0)
84 | assert typeval(JSBigInt(42).__int__()) == (int, 42)
85 | assert typeval(int(JSBigInt(42))) == (int, 42)
86 | check_int_method("__abs__")
87 | assert typeval(hash(JSBigInt(42))) == (int, 42)
88 |
89 | # Before 3.10 operator.index() returns the instance without calling the
90 | # __index__ method, so the type is JSBigInt (we'd rather it was int).
91 | # https://github.com/python/cpython/blob/340a82d9cff7127bb5a777d8b9a30b861bb4beee/Objects/abstract.c#L1324
92 | if sys.version_info >= (3, 10):
93 | assert typeval(operator.index(JSBigInt(42))) == (int, 42)
94 | else:
95 | assert typeval(operator.index(JSBigInt(42))) == (JSBigInt, 42)
96 |
97 |
98 | def test_types() -> None:
99 | # Can assign to int var
100 | i: int = JSBigInt(42) + 3
101 | # Types reflect that JSBigInt is preserved by operators
102 | big: JSBigInt = 4 * JSBigInt(4)
103 |
104 | assert i == 45
105 | assert big == 16
106 |
--------------------------------------------------------------------------------
/test/jstypes/test_jserror.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from v8serialize._values import AnyJSError
4 | from v8serialize.constants import JSErrorName
5 | from v8serialize.jstypes.jserror import JSError, JSErrorData
6 |
7 |
8 | def test_JSErrorData_types() -> None:
9 | # Verify that JSErrorData satisfies the AnyJSError protocol
10 | any_error: AnyJSError = JSErrorData(
11 | name=JSErrorName.Error, message="Oops", stack="..."
12 | )
13 | assert any_error.name == JSErrorName.Error
14 | assert any_error.message == "Oops"
15 | assert any_error.stack == "..."
16 |
17 |
18 | def test_JSError_types() -> None:
19 | # We don't inherit AnyJSError protocol directly because its @properties
20 | # affect the subclass, but JSError must satisfy its type signature.
21 | any_error: AnyJSError = JSError("msg")
22 | # JSError is a virtual subclass of JSErrorData, this is how the encoder
23 | # recognises it as a AnyJSError implementation.
24 | assert isinstance(any_error, JSErrorData)
25 |
26 |
27 | def test_JSError_init() -> None:
28 | jserror = JSError("msg", name=JSErrorName.UriError, stack="...", cause={})
29 | assert jserror.message == "msg"
30 | assert jserror.name == JSErrorName.UriError
31 | assert jserror.stack == "..."
32 | assert jserror.cause == {}
33 |
34 |
35 | def test_JSError_builder() -> None:
36 | jserror, state = JSError.builder(
37 | JSErrorData(message="msg", name=JSErrorName.UriError, stack="...", cause={})
38 | )
39 | assert jserror is state
40 | state.name = JSErrorName.EvalError
41 | state.message = "example"
42 | state.cause = 1
43 | state.stack = "foo"
44 | assert jserror == JSError(
45 | "example", name=JSErrorName.EvalError, stack="foo", cause=1
46 | )
47 |
48 |
49 | def test_JSError_repr() -> None:
50 | jserror = JSError("msg", name=JSErrorName.UriError, stack="Boom", cause={})
51 | assert repr(jserror) == (
52 | "JSError('msg', name=, stack='Boom', "
53 | "cause={})"
54 | )
55 |
56 | # repr is recursion safe with cycles in cause
57 | jserror.cause = jserror
58 | assert (
59 | repr(jserror) == "JSError("
60 | "'msg', name=, stack='Boom', cause=...)"
61 | )
62 |
63 |
64 | def test_JSError__eq() -> None:
65 | e1 = JSError("foo")
66 | e2 = JSError("foo")
67 |
68 | # JSError are equal as normal (unless they have different tracebacks)
69 | assert e1 == e2
70 |
71 | # recursive cause does not blow up == check
72 | e1.cause = e1
73 | e2.cause = e2
74 | assert e1 == e2
75 |
76 | e1.cause = [e1]
77 | e2.cause = [e2]
78 | assert e1 == e2
79 |
80 | # JSErrors which have been raised have tracebacks, which are not equal for
81 | # distinct raises.
82 | for e in [e1, e2]:
83 | try:
84 | raise e
85 | except JSError:
86 | pass
87 |
88 | assert e1 != e2
89 |
90 |
91 | def test_JSErrorData__eq() -> None:
92 | # Unlike JSError, JSErrorData objects are equal by value
93 | e1 = JSErrorData(name=JSErrorName.EvalError, message="Hi", stack="Xyz", cause=42)
94 | e2 = JSErrorData(name=JSErrorName.EvalError, message="Hi", stack="Xyz", cause=42)
95 |
96 | assert e1 == e2
97 |
98 | # Errors with recursive causes can be eq
99 | e1.cause = JSErrorData(cause=e1)
100 | e2.cause = JSErrorData(cause=e2)
101 |
102 | assert e1 == e2
103 |
104 | # Or not eq
105 | e2.cause.message = "Blah"
106 |
107 | assert e1 != e2
108 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsmap.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import isnan
4 |
5 | from hypothesis import given
6 | from hypothesis import strategies as st
7 |
8 | from test.strategies import values_and_objects as mk_values_and_objects
9 | from v8serialize.jstypes._equality import same_value_zero
10 | from v8serialize.jstypes._v8 import V8SharedObjectReference, V8SharedValueId
11 | from v8serialize.jstypes.jsmap import JSMap
12 |
13 | hashable_values_and_objects = mk_values_and_objects(allow_nan=False, only_hashable=True)
14 | values_and_objects = mk_values_and_objects(allow_nan=False, only_hashable=False)
15 |
16 | entries = st.lists(
17 | elements=st.tuples(values_and_objects, values_and_objects),
18 | unique_by=lambda t: same_value_zero(t[0]),
19 | )
20 |
21 |
22 | @given(
23 | mapping=st.dictionaries(keys=hashable_values_and_objects, values=values_and_objects)
24 | )
25 | def test_equal_to_other_mappings_containing_same_object_instances(
26 | mapping: dict[object, object],
27 | ) -> None:
28 | assert JSMap(mapping.items()) == mapping
29 |
30 |
31 | ID = V8SharedValueId(0)
32 |
33 |
34 | def test_equal_to_other_mappings_containing_different_object_instances() -> None:
35 | k1, k2 = V8SharedObjectReference(ID), V8SharedObjectReference(ID)
36 | assert k1 is not k2
37 | assert k1 == k2
38 |
39 | # JSMap instances are eq from the outside if they contain equal elements in
40 | # the same order
41 | jsmap_1_2, jsmap_2_1 = JSMap([(k1, 0), (k2, 0)]), JSMap([(k2, 0), (k1, 0)])
42 |
43 | assert jsmap_1_2 == jsmap_2_1
44 |
45 | # Regular dicts are equal by dict's idea of equality (de-dupe equal keys)
46 | jsmap_1, jsmap_2 = JSMap([(k1, 0), (0, 0)]), JSMap([(0, 0), (k2, 0)])
47 | assert jsmap_1 == {V8SharedObjectReference(ID): 0, 0: 0}
48 | assert jsmap_2 == {V8SharedObjectReference(ID): 0, 0: 0}
49 |
50 | # JSMaps with different item orders are not equal
51 | assert jsmap_1 != jsmap_2
52 |
53 | # Unequal lengths are not equal
54 | assert jsmap_1_2 != {V8SharedObjectReference(ID): 0}
55 | assert jsmap_2_1 != {V8SharedObjectReference(ID): 0}
56 | # ... despite them being equal if they were de-duped
57 | assert dict(jsmap_1_2.items()) == {V8SharedObjectReference(ID): 0}
58 | assert dict(jsmap_2_1.items()) == {V8SharedObjectReference(ID): 0}
59 |
60 |
61 | def test_eq_with_unhashable_keys() -> None:
62 | assert JSMap([({}, 1), ({}, 2)]) != {1: "a", 2: "b"}
63 | assert {1: "a", 2: "b"} != JSMap([({}, 1), ({}, 2)])
64 |
65 |
66 | def test_eq_with_other_type() -> None:
67 | assert JSMap().__eq__(object()) is NotImplemented
68 | assert not (JSMap() == object())
69 |
70 |
71 | def test_eq_with_cycles() -> None:
72 | x = JSMap({})
73 | x[x] = x
74 | y = JSMap({})
75 | y[y] = y
76 | assert x == y
77 |
78 | # different equality pattern
79 | x = JSMap({})
80 | x[x] = x
81 | y_ = JSMap({})
82 | y = JSMap({})
83 | y[y_] = y_
84 | y_[y] = y
85 | assert x != y
86 |
87 |
88 | def test_eq_with_cycles_indirect() -> None:
89 | # indirect
90 | x = JSMap({0: JSMap()})
91 | x[0][x] = x
92 | y = JSMap[int, JSMap]({0: JSMap()})
93 | y[0][y] = y
94 | assert x == y
95 |
96 |
97 | def test_jsmap_nan() -> None:
98 | m = JSMap[float]()
99 | nan = float("nan")
100 | m[nan] = 1
101 | assert m[nan] == 1
102 | assert isnan(list(m)[0])
103 | assert nan in m
104 | m[nan] = 2
105 | assert len(m) == 1
106 | assert m[nan] == 2
107 | del m[nan]
108 | assert len(m) == 0
109 |
110 |
111 | @given(entries=entries)
112 | def test_crud(entries: list[tuple[object, object]]) -> None:
113 | m = JSMap()
114 | assert len(m) == 0
115 | for i, (k, v) in enumerate(entries):
116 | assert k not in m
117 | m[k] = v
118 | assert len(m) == i + 1
119 | assert k in m
120 | assert m[k] is v
121 |
122 | assert list(m.items()) == entries
123 | assert list(m) == [k for k, _ in entries]
124 | assert list(m.values()) == [v for _, v in entries]
125 |
126 | for i, (k, _) in enumerate(entries):
127 | assert k in m
128 | del m[k]
129 | assert k not in m
130 | assert len(m) == len(entries) - 1 - i
131 |
132 | assert m == {}
133 |
134 |
135 | def test_init_types() -> None:
136 | assert JSMap() == {}
137 |
138 | a = JSMap(a=1, b=2)
139 | b = JSMap([("a", 1), ("b", 2)])
140 | c = JSMap([("a", 1)], b=2)
141 | d = JSMap({"a": 1, "b": 2})
142 | e = JSMap({"a": 1}, b=2)
143 |
144 | maps = [a, b, c, d, e]
145 | for m in maps:
146 | assert m == {"a": 1, "b": 2}
147 | assert list(m) == ["a", "b"]
148 |
149 |
150 | def test_repr() -> None:
151 | assert repr(JSMap(c=5, a=1, b=2)) == "JSMap({'c': 5, 'a': 1, 'b': 2})"
152 |
153 |
154 | def test__str() -> None:
155 | m = JSMap({"a": 1})
156 | assert str(m) == repr(m)
157 |
158 |
159 | def test_abc_register() -> None:
160 | class FooMapping:
161 | pass
162 |
163 | JSMap.register(FooMapping)
164 | assert isinstance(FooMapping(), JSMap)
165 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsobject.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from types import MappingProxyType
4 |
5 | import pytest
6 |
7 | from v8serialize.jstypes.jsarray import JSArray
8 | from v8serialize.jstypes.jsarrayproperties import JSHole
9 | from v8serialize.jstypes.jsobject import JSObject
10 |
11 |
12 | def test_init() -> None:
13 | assert dict(JSObject()) == {}
14 |
15 | with pytest.raises(TypeError, match=r"'NoneType' object is not iterable"):
16 | JSObject(None) # type: ignore[call-overload]
17 |
18 |
19 | def test_init__keys_and_get_item() -> None:
20 | assert dict(JSObject({})) == {}
21 | assert dict(JSObject({"x": "X"})) == {"x": "X"}
22 | # Supports arbitrary mapping types
23 | assert dict(JSObject(MappingProxyType({"x": "X"}))) == {"x": "X"}
24 | assert dict(JSObject({"x": "X", 0: "zero"})) == {0: "zero", "x": "X"}
25 | assert dict(JSObject({"x": "XA", 0: "zeroA", "0": "zeroB"}, x="XB")) == (
26 | {0: "zeroB", "x": "XB"}
27 | )
28 |
29 |
30 | def test_init__iterable_kv_pairs() -> None:
31 | assert dict(JSObject([])) == {}
32 | assert dict(JSObject([("x", "X")])) == {"x": "X"}
33 | assert dict(JSObject(iter([("x", "X")]))) == {"x": "X"}
34 | assert dict(JSObject([("x", "X"), (0, "zero")])) == {0: "zero", "x": "X"}
35 | assert dict(JSObject([("x", "XA"), (0, "zeroA"), ("0", "zeroB")], x="XB")) == (
36 | {0: "zeroB", "x": "XB"}
37 | )
38 |
39 | with pytest.raises(ValueError, match=r"not enough values to unpack"):
40 | JSObject(["a", "b", "c"]) # type: ignore[list-item]
41 |
42 |
43 | def test_jshole_assignment() -> None:
44 | # Properties created with JSHole values are the same as not providing them
45 | obj = JSObject({0: "a", 1: JSHole, 2: "c", "x": "X", "y": JSHole})
46 | assert dict(obj) == {0: "a", 2: "c", "x": "X"}
47 |
48 | # Assigning JSHole values to existing keys removes them
49 | assert 0 in obj
50 | obj[0] = JSHole
51 | assert 0 not in obj
52 |
53 | assert "x" in obj
54 | obj["x"] = JSHole
55 | assert "x" not in obj
56 |
57 |
58 | def test_write_methods_accept_float_keys_for_compatibility_with_v8_serialized_data() -> ( # noqa: E501
59 | None
60 | ):
61 | obj = JSObject()
62 | obj[-0.0] = "foo"
63 | assert obj["0"] == "foo"
64 | assert obj[0] == "foo"
65 | # works, but type error because I don't want to encourage using floats normally
66 | assert obj[0.0] == "foo" # type: ignore[index]
67 | assert obj[-0.0] == "foo" # type: ignore[index]
68 |
69 | obj.setdefault(1.0, "one")
70 | assert obj["1"] == "one"
71 | assert obj[1] == "one"
72 | assert obj[1.0] == "one" # type: ignore[index]
73 |
74 | obj.update({-0.0: "x", 1.0: "y", "2": "z"})
75 | obj.update({3: "a", 4.0: "b"})
76 | obj.update({"5": "c", 6.0: "d", -1.0: "!", "-0": "!!"})
77 |
78 | assert dict(obj) == {
79 | 0: "x",
80 | 1: "y",
81 | 2: "z",
82 | 3: "a",
83 | 4: "b",
84 | 5: "c",
85 | 6: "d",
86 | "-1": "!",
87 | "-0": "!!",
88 | }
89 |
90 |
91 | def test__eq() -> None:
92 | assert JSObject() == JSObject()
93 | assert JSObject(**{"0": 1}, a=1, b=2) == JSObject(**{"0": 1}, a=1, b=2)
94 | assert JSObject(a=1, b=2) == JSObject(a=1, b=2)
95 |
96 | assert JSObject() != {}
97 | assert JSObject(**{"0": 1}, a=1, b=2) != {0: 1, "a": 1, "b": 2}
98 | assert JSObject(a=1, b=2) != dict(a=1, b=2)
99 |
100 | # Objects are not equal to equivalent arrays, like dicts are not equal to lists
101 | assert JSObject() != JSArray()
102 | assert JSObject(**{"0": 1}) != JSArray([1])
103 | assert JSObject(a=1) != JSArray(a=1)
104 |
105 |
106 | def test__eq__cycle_direct() -> None:
107 | # Objects that contain reference cycles with the same identity structure are
108 | # equal. See test__recursive_eq.py.
109 | x = JSObject[object](a=1)
110 | x["b"] = x
111 | y = JSObject[object](a=1)
112 | y["b"] = y
113 |
114 | assert x == y
115 |
116 |
117 | def test__eq__cycle_direct_unequal() -> None:
118 | x = JSObject[object](a=1)
119 | x["b"] = x
120 |
121 | # Same shape as l, but identity is different as r and _r repeat alternately
122 | y_ = JSObject[object](a=1)
123 | y = JSObject[object](a=1)
124 | y_["b"] = y
125 | y["b"] = y_
126 |
127 | assert x != y
128 |
129 |
130 | def test__eq__cycle_indirect() -> None:
131 | # Objects that contain reference cycles with the same identity structure are
132 | # equal. See test__recursive_eq.py.
133 | x = JSObject(a=1, b=(x_b := JSObject()))
134 | x_b["c"] = x
135 | y = JSObject(a=1, b=(y_b := JSObject()))
136 | y_b["c"] = y
137 |
138 | assert x == y
139 |
140 |
141 | def test__eq__cycle_indirect_unequal() -> None:
142 | x = JSObject(a=1, b=(x_b := JSObject()))
143 | x_b["c"] = x
144 |
145 | # Same shape as l, but identity is different as r and _r repeat alternately
146 | y_ = JSObject(a=1, b=(y__b := JSObject()))
147 | y = JSObject(a=1, b=(y_b := JSObject()))
148 | y__b["c"] = y
149 | y_b["c"] = y_
150 |
151 | assert x != y
152 |
153 |
154 | def test_abc_registration() -> None:
155 | class Example:
156 | pass
157 |
158 | assert not isinstance(Example(), JSObject)
159 | JSObject.register(Example)
160 | assert isinstance(Example(), JSObject)
161 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsprimitiveobject.py:
--------------------------------------------------------------------------------
1 | from typing import Final
2 | from typing_extensions import assert_type
3 |
4 | from v8serialize.constants import FLOAT64_SAFE_INT_RANGE, SerializationTag
5 | from v8serialize.jstypes import JSPrimitiveObject
6 | from v8serialize.jstypes.jsbigint import JSBigInt
7 | from v8serialize.jstypes.jsprimitiveobject import (
8 | BigIntJSPrimitiveObject,
9 | FalseJSPrimitiveObject,
10 | NumberJSPrimitiveObject,
11 | StringJSPrimitiveObject,
12 | TrueJSPrimitiveObject,
13 | )
14 |
15 | # 2**53 + 2 is an integer that can be represented exactly as a float64, but is
16 | # outside the limit of sequential integers that float64 can represent.
17 | # (e.g. float64 cannot represent 2**53 + 1 exactly.)
18 | UNSAFE_INTEGER_FLOAT: Final = float(2**53 + 2)
19 | assert int(UNSAFE_INTEGER_FLOAT) == 2**53 + 2
20 |
21 |
22 | def test_init_types() -> None:
23 | true_obj = assert_type(JSPrimitiveObject(True), TrueJSPrimitiveObject)
24 | assert true_obj.value is True
25 | assert true_obj.tag is SerializationTag.kTrueObject
26 |
27 | false_obj = assert_type(JSPrimitiveObject(False), FalseJSPrimitiveObject)
28 | assert false_obj.value is False
29 | assert false_obj.tag is SerializationTag.kFalseObject
30 |
31 | number_obj = assert_type(JSPrimitiveObject(1.0), NumberJSPrimitiveObject)
32 | assert number_obj.value == 1.0
33 | assert type(number_obj.value) is float
34 | assert number_obj.tag is SerializationTag.kNumberObject
35 |
36 | # int values are mapped to JavaScript Number when in the safe int range,
37 | # or JavaScript bigint outside that.
38 |
39 | # mypy doesn't match the ambiguous int overload for some reason, it falls
40 | # back to the default typevar bounds.
41 | int_bigint_obj = assert_type( # type: ignore[assert-type]
42 | JSPrimitiveObject(FLOAT64_SAFE_INT_RANGE.stop),
43 | "NumberJSPrimitiveObject | BigIntJSPrimitiveObject",
44 | )
45 | assert int_bigint_obj.value == FLOAT64_SAFE_INT_RANGE.stop
46 | assert type(int_bigint_obj.value) is JSBigInt
47 | assert int_bigint_obj.tag is SerializationTag.kBigIntObject
48 |
49 | # force int as bigint by specifying tag
50 | int_bigint_obj2 = assert_type(
51 | JSPrimitiveObject(0, tag=SerializationTag.kBigIntObject),
52 | BigIntJSPrimitiveObject,
53 | )
54 | assert int_bigint_obj2.value == 0
55 | assert type(int_bigint_obj2.value) is JSBigInt
56 | assert int_bigint_obj2.tag is SerializationTag.kBigIntObject
57 |
58 | # mypy doesn't match the ambiguous int overload for some reason
59 | int_number_obj = assert_type( # type: ignore[assert-type]
60 | JSPrimitiveObject(FLOAT64_SAFE_INT_RANGE.stop - 1),
61 | "NumberJSPrimitiveObject | BigIntJSPrimitiveObject",
62 | )
63 | assert int_number_obj.value == FLOAT64_SAFE_INT_RANGE.stop - 1
64 | assert type(int_number_obj.value) is float
65 | assert int_number_obj.tag is SerializationTag.kNumberObject
66 |
67 | # force int as number by specifying tag
68 | int_number_obj2 = assert_type(
69 | JSPrimitiveObject(
70 | int(UNSAFE_INTEGER_FLOAT), tag=SerializationTag.kNumberObject
71 | ),
72 | NumberJSPrimitiveObject,
73 | )
74 | assert int_number_obj2.value == UNSAFE_INTEGER_FLOAT
75 | assert type(int_number_obj2.value) is float
76 | assert int_number_obj2.tag is SerializationTag.kNumberObject
77 |
78 | string_obj = assert_type(JSPrimitiveObject("s"), StringJSPrimitiveObject)
79 | assert string_obj.value == "s"
80 | assert string_obj.tag is SerializationTag.kStringObject
81 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsregexp.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 |
5 | import pytest
6 | from hypothesis import given
7 |
8 | from test.strategies import js_regexp_flags
9 | from v8serialize._errors import JSRegExpV8SerializeError
10 | from v8serialize.constants import JSRegExpFlag
11 | from v8serialize.jstypes.jsregexp import JSRegExp
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "jsregexp,msg",
16 | [
17 | pytest.param(
18 | JSRegExp(r".*", JSRegExpFlag.Linear),
19 | "No equivalent Python flags exist for JSRegExp.Linear",
20 | id="invalid_flags",
21 | ),
22 | pytest.param(
23 | JSRegExp(r"\cJ"),
24 | "bad escape \\c at position 0",
25 | id="invalid_syntax",
26 | ),
27 | ],
28 | )
29 | def test_compile__incompatible(jsregexp: JSRegExp, msg: str) -> None:
30 | with pytest.raises(
31 | JSRegExpV8SerializeError,
32 | match=re.escape(f"JSRegExp is not a valid Python re.Pattern: {msg}"),
33 | ):
34 | jsregexp.as_python_pattern()
35 |
36 | assert jsregexp.as_python_pattern(throw=False) is None
37 |
38 |
39 | def test_from_python_pattern() -> None:
40 | assert JSRegExp.from_python_pattern(
41 | re.compile(".*", re.UNICODE | re.DOTALL)
42 | ) == JSRegExp(".*", JSRegExpFlag.UnicodeSets | JSRegExpFlag.DotAll)
43 |
44 | assert JSRegExp.from_python_pattern(re.compile(b".*")) == JSRegExp(".*")
45 |
46 | with pytest.raises(
47 | JSRegExpV8SerializeError,
48 | match=re.escape(
49 | "Python re.Pattern flags cannot be represented by JavaScript RegExp: "
50 | "No equivalent JavaScript RegExp flags exist for RegexFlag.VERBOSE"
51 | ),
52 | ):
53 | JSRegExp.from_python_pattern(re.compile("", re.VERBOSE))
54 |
55 | assert JSRegExp.from_python_pattern(re.compile("", re.VERBOSE), throw=False) is None
56 |
57 |
58 | def test_empty_source_is_non_capturing_group() -> None:
59 | assert JSRegExp(source="").source == "(?:)"
60 |
61 |
62 | @given(any_flags=js_regexp_flags())
63 | def test_flags_cannot_have_both_unicode_flags_set(any_flags: JSRegExpFlag) -> None:
64 | with pytest.raises(
65 | ValueError, match=r"The Unicode and UnicodeSets flags cannot be set together"
66 | ):
67 | JSRegExp("", flags=any_flags | JSRegExpFlag.Unicode | JSRegExpFlag.UnicodeSets)
68 |
--------------------------------------------------------------------------------
/test/jstypes/test_jsset.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import isnan
4 |
5 | from hypothesis import given
6 | from hypothesis import strategies as st
7 |
8 | from test.strategies import values_and_objects as mk_values_and_objects
9 | from v8serialize.jstypes._equality import same_value_zero
10 | from v8serialize.jstypes._v8 import V8SharedObjectReference, V8SharedValueId
11 | from v8serialize.jstypes.jsset import JSSet
12 |
13 | hashable_values_and_objects = mk_values_and_objects(allow_nan=False, only_hashable=True)
14 | values_and_objects = mk_values_and_objects(allow_nan=False, only_hashable=False)
15 |
16 | elements = st.lists(elements=values_and_objects, unique_by=same_value_zero)
17 |
18 |
19 | @given(set_=st.sets(elements=hashable_values_and_objects))
20 | def test_equal_to_other_sets(set_: set[object]) -> None:
21 | assert JSSet(set_) == set_
22 |
23 |
24 | ID = V8SharedValueId(0)
25 |
26 |
27 | def test_equal_to_other_sets_containing_different_object_instances() -> None:
28 | k1, k2 = V8SharedObjectReference(ID), V8SharedObjectReference(ID)
29 | assert k1 is not k2
30 | assert k1 == k2
31 |
32 | # JSSet instances are eq from the outside if they contain equal elements in
33 | # the same order
34 | jsset_1_2, jsset_2_1 = JSSet([k1, k2]), JSSet([k2, k1])
35 |
36 | assert jsset_1_2 == jsset_2_1
37 |
38 | # Regular sets are equal by set's idea of equality (de-dupe equal members)
39 | jsset_1, jsset_2 = JSSet([k1, 0]), JSSet([0, k2])
40 | assert jsset_1 == {V8SharedObjectReference(ID), 0}
41 | assert jsset_2 == {V8SharedObjectReference(ID), 0}
42 |
43 | # JSSets with different member orders are not equal
44 | assert jsset_1 != jsset_2
45 |
46 | # Unequal lengths are not equal
47 | assert jsset_1_2 != {V8SharedObjectReference(ID)}
48 | assert jsset_2_1 != {V8SharedObjectReference(ID)}
49 | # ... despite them being equal if they were de-duped
50 | assert set(jsset_1_2) == {V8SharedObjectReference(ID)}
51 | assert set(jsset_2_1) == {V8SharedObjectReference(ID)}
52 |
53 |
54 | def test_eq_with_unhashable_elements() -> None:
55 | assert JSSet([{}, {}]) != {1, 2}
56 | assert {1, 2} != JSSet([{}, {}])
57 |
58 |
59 | def test_eq_with_other_type() -> None:
60 | assert JSSet().__eq__(object()) is NotImplemented
61 | assert not (JSSet() == object())
62 |
63 |
64 | def test_eq_with_cycles() -> None:
65 | x = JSSet([])
66 | x.add(x)
67 | y = JSSet([])
68 | y.add(y)
69 | assert x == y
70 |
71 | # different equality pattern
72 | x = JSSet([])
73 | x.add(x)
74 | y_ = JSSet([])
75 | y = JSSet([])
76 | y.add(y_)
77 | y_.add(y)
78 | assert x != y
79 |
80 | # indirect
81 | x = JSSet([inner_x := JSSet()])
82 | inner_x.add(x)
83 | y = JSSet([inner_y := JSSet()])
84 | inner_y.add(y)
85 | assert x == y
86 |
87 |
88 | def test_nan() -> None:
89 | s = JSSet[float]()
90 | nan = float("nan")
91 | s.add(nan)
92 | assert len(s) == 1
93 | assert isnan(next(iter(s)))
94 | assert nan in s
95 | s.add(nan)
96 | assert len(s) == 1
97 | s.remove(nan)
98 | assert len(s) == 0
99 |
100 |
101 | @given(elements=elements)
102 | def test_crud(elements: list[object]) -> None:
103 | s = JSSet()
104 | assert len(s) == 0
105 | for i, e in enumerate(elements):
106 | assert e not in s
107 | s.add(e)
108 | assert len(s) == i + 1
109 | assert e in s
110 |
111 | assert list(s) == elements
112 |
113 | for i, e in enumerate(elements):
114 | assert e in s
115 | s.remove(e)
116 | assert e not in s
117 | assert len(s) == len(elements) - 1 - i
118 |
119 | assert s == set()
120 |
121 |
122 | def test_jsset__init_types() -> None:
123 | assert JSSet() == set()
124 |
125 | s = JSSet(["c", "a", "b"])
126 | assert s == {"c", "a", "b"}
127 | assert list(s) == ["c", "a", "b"]
128 |
129 |
130 | def test_repr() -> None:
131 | assert repr(JSSet(["a", True, {}])) == "JSSet(['a', True, {}])"
132 |
133 |
134 | def test_str() -> None:
135 | s = JSSet([1, 2])
136 | assert str(s) == repr(s)
137 |
138 |
139 | def test_abc_register() -> None:
140 | class FooSet:
141 | pass
142 |
143 | JSSet.register(FooSet)
144 | assert isinstance(FooSet(), JSSet)
145 |
--------------------------------------------------------------------------------
/test/jstypes/test_normalise_property_key.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 |
5 | from hypothesis import example, given
6 | from hypothesis import strategies as st
7 |
8 | from v8serialize.jstypes._normalise_property_key import (
9 | canonical_numeric_index_string,
10 | normalise_property_key,
11 | )
12 | from v8serialize.jstypes.jsarrayproperties import MAX_ARRAY_LENGTH
13 |
14 | int_integer_indexes = st.integers(min_value=0, max_value=MAX_ARRAY_LENGTH - 1)
15 | float_integer_indexes = st.one_of(st.just(-0.0), int_integer_indexes.map(float))
16 | integer_indexes = st.one_of(int_integer_indexes, float_integer_indexes)
17 |
18 | invalid_integer_indexes = st.one_of(
19 | st.integers(max_value=-1), st.integers(min_value=MAX_ARRAY_LENGTH)
20 | )
21 | """Integers below or above the allowed range for array indexes."""
22 |
23 | invalid_float_indexes = st.one_of(
24 | st.floats(min_value=0, max_value=MAX_ARRAY_LENGTH - 1).filter(
25 | lambda f: not f.is_integer()
26 | ),
27 | st.floats(max_value=-0.0, exclude_max=True), # exclude -0.0, which is valid
28 | st.floats(min_value=MAX_ARRAY_LENGTH),
29 | )
30 |
31 |
32 | non_canonical_integers = st.integers().map(lambda i: f"0{i}")
33 | # Note: this includes the string "-0" which does not match index 0.
34 | # See test_normalise_property_key__handles_negative_zero().
35 | negative_integer_strings = st.integers(min_value=0).map(lambda i: f"-{i}")
36 | non_int_strings = st.text().filter(lambda x: not is_base10_int_str(x))
37 |
38 | non_index_strings = st.one_of(
39 | non_canonical_integers, negative_integer_strings, non_int_strings
40 | )
41 |
42 |
43 | def is_base10_int_str(value: str) -> bool:
44 | return bool(re.match(r"^(0|[1-9][0-9]*)$", value))
45 |
46 |
47 | @given(valid_index=int_integer_indexes)
48 | def test_canonical_numeric_index_string__range__matches_valid_indexes(
49 | valid_index: int,
50 | ) -> None:
51 | result = canonical_numeric_index_string(str(valid_index))
52 | assert result == valid_index
53 |
54 |
55 | @given(non_index=non_index_strings)
56 | @example("-0")
57 | def test_canonical_numeric_index_string__range__rejects_non_index_strings(
58 | non_index: str,
59 | ) -> None:
60 | result = canonical_numeric_index_string(non_index)
61 | assert result is None
62 |
63 |
64 | @given(valid_index=integer_indexes)
65 | def test_normalise_property_key__returns_int_for_integers_in_array_index_range(
66 | valid_index: int | float,
67 | ) -> None:
68 | assert normalise_property_key(valid_index) == valid_index
69 |
70 |
71 | @given(valid_index=int_integer_indexes)
72 | def test_normalise_property_key__returns_int_for_canonical_integer_strings_in_array_index_range( # noqa: E501
73 | valid_index: int,
74 | ) -> None:
75 | assert normalise_property_key(str(valid_index)) == valid_index
76 |
77 |
78 | @given(invalid_index=invalid_integer_indexes)
79 | def test_normalise_property_key__returns_str_for_ints_not_in_array_index_range(
80 | invalid_index: int,
81 | ) -> None:
82 | assert normalise_property_key(invalid_index) == str(invalid_index)
83 |
84 |
85 | @given(invalid_index=invalid_integer_indexes)
86 | def test_normalise_property_key__returns_str_for_str_ints_not_in_array_index_range(
87 | invalid_index: int,
88 | ) -> None:
89 | assert normalise_property_key(str(invalid_index)) == str(invalid_index)
90 |
91 |
92 | @given(invalid_index=invalid_float_indexes)
93 | def test_normalise_property_key__returns_str_for_floats_that_are_not_valid(
94 | invalid_index: float,
95 | ) -> None:
96 | result = normalise_property_key(invalid_index)
97 | if invalid_index.is_integer():
98 | assert result == f"{int(invalid_index)}"
99 | else:
100 | assert result == str(invalid_index)
101 |
102 |
103 | @given(invalid_index=invalid_float_indexes)
104 | def test_normalise_property_key__returns_str_for_float_strings_that_are_not_valid(
105 | invalid_index: float,
106 | ) -> None:
107 | # We can use "-1.0" rather than "-1" because we test negative int strings
108 | # separately.
109 | assert normalise_property_key(str(invalid_index)) == str(invalid_index)
110 |
111 |
112 | @given(non_index_strings)
113 | def test_normalise_property_key__returns_str_for_non_index_strings(
114 | non_index: str,
115 | ) -> None:
116 | assert normalise_property_key(non_index) == non_index
117 |
118 |
119 | def test_normalise_property_key__handles_negative_zero() -> None:
120 | # ECMA 262 notes in particular that:
121 | # > "-0" is neither an integer index nor an array index.
122 | # -0 is a special case as it could be interpreted as -0.0 or 0.
123 | assert normalise_property_key("-0") == "-0"
124 |
125 | # Despite the above, the actual float value -0.0 is treated as 0. The reason
126 | # this is the case may not be immediately obvious from reading the spec:
127 | # - https://tc39.es/ecma262/#sec-object-type
128 | # - The spec requires that CanonicalNumericIndexString(n) returns a
129 | # non-negative integer. And if you read the definition of
130 | # CanonicalNumericIndexString(n) you see a rule that "-0" returns -0.
131 | # - The thing to realise is that the argument n is a string, so -0.0 is
132 | # converted to a string first. Number::toString returns -0 as "0":
133 | # > If x is either +0𝔽 or -0𝔽, return "0".
134 | # - Hence with -0 we call CanonicalNumericIndexString("0") which is 0.
135 | assert type(normalise_property_key(-0.0)) is int
136 | assert normalise_property_key(-0.0) == 0
137 |
--------------------------------------------------------------------------------
/test/pycompat/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/test/pycompat/test_dataclasses.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from dataclasses import FrozenInstanceError, dataclass
5 | from typing import Generic, TypeVar
6 |
7 | import pytest
8 |
9 | from v8serialize._pycompat.dataclasses import FrozenAfterInitDataclass, slots_if310
10 |
11 | T = TypeVar("T")
12 |
13 |
14 | @pytest.mark.skipif(
15 | sys.version_info[:2] != (3, 10), reason="Test applies only to py310"
16 | )
17 | def test_frozen_generic_dataclass() -> None:
18 | @dataclass(frozen=True, **slots_if310())
19 | class BrokenOn310(Generic[T]):
20 | foo: T
21 |
22 | # Using the class directly works
23 | assert BrokenOn310(foo="")
24 |
25 | # Subscripting with a type does not
26 | with pytest.raises(
27 | TypeError,
28 | match=r"super\(type, obj\): obj must be an instance or subtype of type",
29 | ):
30 | BrokenOn310[str](foo="")
31 |
32 | @dataclass(**slots_if310())
33 | class OKOn310(FrozenAfterInitDataclass, Generic[T]):
34 | foo: T
35 |
36 | assert OKOn310[str](foo="")
37 |
38 |
39 | def test_FrozenAfterInitDataclass() -> None:
40 | @dataclass(unsafe_hash=True, **slots_if310())
41 | class Example(FrozenAfterInitDataclass):
42 | a: int
43 |
44 | e = Example(a=1)
45 | assert e.a == 1
46 |
47 | with pytest.raises(FrozenInstanceError):
48 | e.a = 2
49 |
50 | with pytest.raises(FrozenInstanceError):
51 | del e.a
52 |
53 | assert Example(a=1) == Example(a=1)
54 | assert hash(Example(a=1)) == hash(Example(a=1))
55 |
56 | # No need for object.__setattr__ to initialise frozen fields
57 | @dataclass(init=False)
58 | class Example2(Example):
59 | b: str
60 |
61 | def __init__(self, a: int) -> None:
62 | super().__init__(a)
63 | self.b = str(a)
64 |
65 | ex2 = Example2(10)
66 | assert ex2.a == 10
67 | assert ex2.b == "10"
68 |
--------------------------------------------------------------------------------
/test/pycompat/test_typing.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from array import array
4 |
5 | import pytest
6 |
7 | from v8serialize._pycompat.typing import (
8 | Buffer,
9 | ReadableBinary,
10 | get_buffer,
11 | is_readable_binary,
12 | )
13 |
14 |
15 | @pytest.mark.parametrize(
16 | "buffer", [b"a", bytearray(b"a"), memoryview(b"a"), array("B", b"a")]
17 | )
18 | def test_is_readable_binary(buffer: Buffer) -> None:
19 | assert is_readable_binary(buffer)
20 | assert read_something(buffer) == b"a"[0]
21 |
22 | # getitem slice must return a ReadableBinary, not just Sequence[int]
23 | piece = buffer[:1]
24 | assert read_something(piece) == b"a"[0]
25 |
26 | # regular types can be assigned to a ReadableBinary var
27 | rb: ReadableBinary = b"abc"
28 | assert rb
29 |
30 |
31 | @pytest.mark.parametrize(
32 | "buffer",
33 | [
34 | b"a",
35 | bytearray(b"a"),
36 | memoryview(b"a"),
37 | memoryview(b"a").cast("b", (1, 1)),
38 | array("B", b"a"),
39 | # get_buffer returns flat uint8 memoryview
40 | array("I", [ord(b"a")] * 4),
41 | ],
42 | )
43 | def test_get_buffer(buffer: Buffer) -> None:
44 | mv = get_buffer(buffer)
45 | assert mv.format == "B"
46 | assert mv.ndim == 1
47 | assert mv.itemsize == 1
48 | assert mv[0] == b"a"[0]
49 |
50 |
51 | def read_something(data: ReadableBinary) -> int:
52 | return data[0]
53 |
--------------------------------------------------------------------------------
/test/test__recursive_eq.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass, field
4 |
5 | import pytest
6 |
7 | from v8serialize._recursive_eq import recursive_eq
8 |
9 |
10 | @recursive_eq
11 | @dataclass
12 | class Node:
13 | value: int
14 | child: Node | tuple[Node, ...] | None = field(default=None)
15 |
16 |
17 | def test_recursive_eq__same_instance() -> None:
18 | b = Node(2)
19 | a = Node(1, child=b)
20 |
21 | for _ in range(2):
22 | assert b == b
23 | assert a == a
24 | assert a != b
25 |
26 | a.child = a
27 |
28 | assert a == a
29 | assert a != b
30 |
31 |
32 | def test_recursive_eq__same_structure_same_identity() -> None:
33 | b = Node(2)
34 | a = Node(1, child=b)
35 |
36 | _b = Node(2)
37 | _a = Node(1, child=_b)
38 |
39 | for _ in range(2):
40 | assert b == _b
41 | assert a == _a
42 |
43 | b.child = a
44 | _b.child = _a
45 |
46 | for _ in range(2):
47 | assert a == _a
48 |
49 | c = Node(3, child=a)
50 | b.child = c
51 | _c = Node(3, child=_a)
52 | _b.child = _c
53 |
54 | for _ in range(2):
55 | assert a == _a
56 |
57 |
58 | def test_recursive_eq__same_structure_different_identity() -> None:
59 | # value: #1 -> #2 -> #1 -> #2 ...
60 | # identity: a -> b -> a -> b ...
61 | b = Node(2)
62 | a = Node(1, child=b)
63 | b.child = a
64 |
65 | # value: #1 -> #2 -> #1 -> #2 ...
66 | # identity: _a1 -> _b1 -> _a2 -> _b2 -> _a1 ...
67 | _b2 = Node(2)
68 | _a2 = Node(1, child=_b2)
69 | _b1 = Node(2, child=_a2)
70 | _a1 = Node(1, child=_b1)
71 | _b2.child = _a1
72 |
73 | # These are not equal because the recursive identity structure is different.
74 | for _ in range(2):
75 | assert a != _a1
76 | # Could be made eq by using a key based on the node's state rather than id(node)
77 |
78 |
79 | def test_recursive_eq__handles_failure_in_wrapped_eq() -> None:
80 | class FailingNode(Node):
81 | def __eq__(self, value: object) -> bool:
82 | raise RuntimeError("oops")
83 |
84 | b = Node(2)
85 | a = Node(1, child=b)
86 | b.child = a
87 |
88 | _b = Node(2)
89 | _a = Node(1, child=_b)
90 | _b.child = _a
91 |
92 | for _ in range(2):
93 | assert a == _a
94 |
95 | bad_b = Node(2, child=FailingNode(1)) # like a, but eq fails
96 | bad_a = Node(1, child=bad_b)
97 |
98 | with pytest.raises(RuntimeError, match="oops"):
99 | a.__eq__(bad_a)
100 |
101 | # State tracking in-progress eq was correctly reset after the error
102 | for _ in range(2):
103 | assert a == _a
104 |
--------------------------------------------------------------------------------
/test/test_constants.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import operator
4 | from functools import reduce
5 |
6 | import pytest
7 | from packaging.version import Version
8 |
9 | from v8serialize._errors import JSRegExpV8SerializeError
10 | from v8serialize._pycompat.re import RegexFlag
11 | from v8serialize.constants import (
12 | JSErrorName,
13 | JSRegExpFlag,
14 | SerializationErrorTag,
15 | SerializationFeature,
16 | SerializationTag,
17 | SymbolicVersion,
18 | )
19 |
20 |
21 | def test_SerializationTag() -> None:
22 | assert int(SerializationTag.kBeginJSObject) in SerializationTag
23 | assert -1 not in SerializationTag
24 | assert 0xFFFF not in SerializationTag
25 | assert SerializationTag(int(SerializationTag.kRegExp)) is SerializationTag.kRegExp
26 |
27 |
28 | def test_SerializationErrorTag() -> None:
29 | assert SerializationErrorTag.Message in SerializationErrorTag
30 | assert int(SerializationErrorTag.Message) in SerializationErrorTag
31 | assert -1 not in SerializationErrorTag
32 | assert 0xFFFF not in SerializationErrorTag
33 | assert (
34 | SerializationErrorTag(int(SerializationErrorTag.Cause))
35 | is SerializationErrorTag.Cause
36 | )
37 |
38 |
39 | def test_RegExpFlag() -> None:
40 | assert JSRegExpFlag.Global == 1 # type: ignore[comparison-overlap]
41 | assert JSRegExpFlag.UnicodeSets == 1 << 8
42 |
43 | assert str(JSRegExpFlag.Global) == "g"
44 | assert str(JSRegExpFlag.UnicodeSets) == "v"
45 |
46 | assert str(JSRegExpFlag.Global | JSRegExpFlag.UnicodeSets) == "gv"
47 |
48 | assert str(JSRegExpFlag(0b111111111)) == "dgilmsuvy"
49 |
50 | assert JSRegExpFlag.IgnoreCase.as_python_flags() == RegexFlag.IGNORECASE
51 | assert JSRegExpFlag.Multiline.as_python_flags() == RegexFlag.MULTILINE
52 |
53 | assert (JSRegExpFlag.IgnoreCase | JSRegExpFlag.Multiline).as_python_flags() == (
54 | RegexFlag.IGNORECASE | RegexFlag.MULTILINE
55 | )
56 | assert JSRegExpFlag.Global.as_python_flags() == RegexFlag.NOFLAG
57 |
58 | # Linear has no equivalent in Python, so its presence invalidates tags its with
59 | assert JSRegExpFlag.Linear.as_python_flags(throw=False) is None
60 | assert (JSRegExpFlag.Multiline | JSRegExpFlag.Linear).as_python_flags(
61 | throw=False
62 | ) is None
63 | assert (
64 | JSRegExpFlag.Multiline | JSRegExpFlag.Linear | JSRegExpFlag.IgnoreCase
65 | ).as_python_flags(throw=False) is None
66 |
67 | with pytest.raises(
68 | JSRegExpV8SerializeError,
69 | match=r"No equivalent Python flags exist for JSRegExp\.Linear",
70 | # ValueError, match=r"No equivalent Python flags exist for JSRegExp\.Linear"
71 | ):
72 | assert (JSRegExpFlag.Multiline | JSRegExpFlag.Linear).as_python_flags()
73 |
74 |
75 | def test_RegExpFlag__canonical() -> None:
76 | for f in JSRegExpFlag:
77 | assert f.canonical == f
78 |
79 | all: JSRegExpFlag = reduce(operator.or_, JSRegExpFlag) # type: ignore[assignment]
80 | assert all.canonical == all
81 | non_canonical = JSRegExpFlag(0xFFF)
82 | assert list(non_canonical) == list(all)
83 | assert non_canonical != all
84 | assert non_canonical.canonical == all
85 |
86 | assert JSRegExpFlag(0).canonical == 0
87 |
88 |
89 | def test_RegExpFlag__from_python_flags() -> None:
90 | assert JSRegExpFlag.from_python_flags(RegexFlag.NOFLAG) == (JSRegExpFlag.NoFlag)
91 |
92 | assert JSRegExpFlag.from_python_flags(RegexFlag.MULTILINE | RegexFlag.DOTALL) == (
93 | JSRegExpFlag.Multiline | JSRegExpFlag.DotAll
94 | )
95 |
96 | with pytest.raises(
97 | JSRegExpV8SerializeError,
98 | match=r"No equivalent JavaScript RegExp flags exist for RegexFlag\.VERBOSE",
99 | ):
100 | assert JSRegExpFlag.from_python_flags(RegexFlag.MULTILINE | RegexFlag.VERBOSE)
101 |
102 |
103 | def test_ErrorTag() -> None:
104 | assert SerializationErrorTag.EvalErrorPrototype == ord("E")
105 |
106 |
107 | def test_SerializationFeature() -> None:
108 | assert SerializationFeature.CircularErrorCause.first_v8_version == Version(
109 | "12.1.109"
110 | )
111 | assert (
112 | SerializationFeature.CircularErrorCause.first_v8_version
113 | > SerializationFeature.MaxCompatibility.first_v8_version
114 | )
115 |
116 |
117 | def test_SerializationFeature__for_name() -> None:
118 | assert (
119 | SerializationFeature.for_name("CircularErrorCause")
120 | is SerializationFeature.CircularErrorCause
121 | )
122 |
123 | with pytest.raises(LookupError, match=r"^Frob$"):
124 | SerializationFeature.for_name("Frob")
125 |
126 |
127 | @pytest.mark.parametrize(
128 | "v8_version, features",
129 | [
130 | (Version("0"), None),
131 | (Version("10.0.28"), None),
132 | (Version("10.0.28"), None),
133 | (Version("10.0.29"), SerializationFeature.MaxCompatibility),
134 | (Version("10.6.0"), SerializationFeature.MaxCompatibility),
135 | (Version("10.7.123"), SerializationFeature.RegExpUnicodeSets),
136 | ("10.7.123", SerializationFeature.RegExpUnicodeSets),
137 | (Version("13.1.201"), ~SerializationFeature.MaxCompatibility),
138 | (SymbolicVersion.Unreleased, ~SerializationFeature.MaxCompatibility),
139 | ],
140 | )
141 | def test_SerializationFeature__supported_by(
142 | v8_version: Version, features: SerializationFeature | None
143 | ) -> None:
144 | if features is None:
145 | with pytest.raises(LookupError, match=r"V8 version .+ is earlier than"):
146 | SerializationFeature.supported_by(v8_version=v8_version)
147 | else:
148 | assert SerializationFeature.supported_by(v8_version=v8_version) == features
149 |
150 |
151 | def test_JSErrorName() -> None:
152 | assert str(JSErrorName.Error) == "Error"
153 | assert str(JSErrorName.Error) in JSErrorName
154 | assert (
155 | JSErrorName.SyntaxError.error_tag == SerializationErrorTag.SyntaxErrorPrototype
156 | )
157 |
158 |
159 | def test_SymbolicVersion() -> None:
160 | assert SymbolicVersion.Unreleased > Version("0.0.0")
161 | assert Version("0.0.0") < SymbolicVersion.Unreleased
162 | assert SymbolicVersion.Unreleased > Version("99999999999.0.0")
163 | assert Version("99999999999.0.0") < SymbolicVersion.Unreleased
164 |
--------------------------------------------------------------------------------
/test/test_errors.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 |
5 | from v8serialize._errors import (
6 | NormalizedKeyError,
7 | UnhandledTagDecodeV8SerializeError,
8 | V8SerializeError,
9 | )
10 | from v8serialize.constants import SerializationTag
11 |
12 |
13 | @dataclass(init=False)
14 | class ExampleV8CodecError(V8SerializeError):
15 | level: int
16 | limit: float
17 |
18 | def __init__(self, message: str, *, level: int, limit: float) -> None:
19 | super().__init__(message)
20 | self.level = level
21 | self.limit = limit
22 |
23 |
24 | def test_v8codecerror_str_with_fields() -> None:
25 | assert (
26 | str(ExampleV8CodecError("Level too high", level=3, limit=2.123))
27 | == "Level too high: level=3, limit=2.123"
28 | )
29 |
30 |
31 | @dataclass(init=False)
32 | class RecursiveV8CodecError(V8SerializeError):
33 | obj: object
34 |
35 | def __init__(self, message: str, *, obj: object) -> None:
36 | super().__init__(message)
37 | self.obj = obj
38 |
39 |
40 | def test_v8codecerror_str_with_recursive_dataclass_field() -> None:
41 | @dataclass(repr=False, init=False)
42 | class RecursiveThing:
43 | obj: object
44 |
45 | def __init__(self) -> None:
46 | self.obj = self
47 |
48 | def __repr__(self) -> str:
49 | return "RecursiveThing()"
50 |
51 | assert (
52 | str(RecursiveV8CodecError("Example", obj=RecursiveThing()))
53 | == "Example: obj=RecursiveThing()"
54 | )
55 |
56 |
57 | def test_v8codecerror_str_without_fields() -> None:
58 | assert str(V8SerializeError("Something went wrong")) == "Something went wrong"
59 |
60 |
61 | def test_NormalizedKeyError() -> None:
62 | nke = NormalizedKeyError(0, "0")
63 |
64 | assert nke.normalized_key == 0
65 | assert nke.raw_key == "0"
66 | assert repr(nke) == "NormalizedKeyError(normalized_key=0, raw_key='0')"
67 | # str being the repr of a str is kind of weird, but this is what all errors do
68 | assert str(nke) == repr("0 (normalized from '0')")
69 |
70 |
71 | def test_UnmappedTagDecodeV8CodecError() -> None:
72 | err = UnhandledTagDecodeV8SerializeError(
73 | "Msg", tag=SerializationTag.kArrayBuffer, position=2, data=b"foo"
74 | )
75 |
76 | assert (
77 | str(err)
78 | == "Msg: position=2, data=b'foo', tag="
79 | )
80 |
--------------------------------------------------------------------------------
/test/test_extensions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from array import array
4 | from base64 import b64decode
5 | from typing import TYPE_CHECKING
6 |
7 | import pytest
8 |
9 | from v8serialize.constants import ArrayBufferViewTag
10 | from v8serialize.decode import ReadableTagStream
11 | from v8serialize.encode import WritableTagStream
12 | from v8serialize.extensions import (
13 | NodeBufferFormat,
14 | NodeJsArrayBufferViewHostObjectHandler,
15 | )
16 | from v8serialize.jstypes.jsbuffers import (
17 | JSArrayBuffer,
18 | JSTypedArray,
19 | JSUint8Array,
20 | JSUint32Array,
21 | )
22 |
23 | if TYPE_CHECKING:
24 | from typing_extensions import Buffer
25 |
26 |
27 | def test_NodeBufferFormat() -> None:
28 | assert NodeBufferFormat(0) is NodeBufferFormat.Int8Array
29 |
30 | assert not NodeBufferFormat.supports(ArrayBufferViewTag.kFloat16Array)
31 | assert NodeBufferFormat.supports(ArrayBufferViewTag.kDataView)
32 | assert NodeBufferFormat(ArrayBufferViewTag.kDataView) is NodeBufferFormat.DataView
33 |
34 |
35 | @pytest.mark.parametrize(
36 | "serialized,node_type,py_type,data",
37 | [
38 | (
39 | "/w9cBgj/////Fc1bBw==",
40 | NodeBufferFormat.Uint32Array,
41 | JSUint32Array,
42 | array("I", [2**32 - 1, 123456789]),
43 | ),
44 | (
45 | "/w9cCgQBAgME",
46 | NodeBufferFormat.FastBuffer,
47 | JSUint8Array,
48 | array("B", [1, 2, 3, 4]),
49 | ),
50 | ],
51 | )
52 | def test_NodeJsArrayBufferViewHostObjectHandler_deserialize_host_object(
53 | serialized: str,
54 | node_type: NodeBufferFormat,
55 | py_type: type[JSTypedArray],
56 | data: Buffer,
57 | ) -> None:
58 | stream = ReadableTagStream(b64decode(serialized))
59 |
60 | assert stream.read_header() == 15
61 | assert NodeBufferFormat(stream.data[3]) is node_type
62 | view = stream.read_host_object(
63 | deserializer=NodeJsArrayBufferViewHostObjectHandler(), tag=True
64 | )
65 | assert stream.eof
66 |
67 | assert view == py_type(JSArrayBuffer(data))
68 |
69 |
70 | @pytest.mark.parametrize(
71 | "node_type,py_type,data",
72 | [
73 | (
74 | NodeBufferFormat.Uint32Array,
75 | JSUint32Array,
76 | array("I", [2**32 - 1, 123456789]),
77 | ),
78 | (
79 | # Note that we write Uint8Arrays as Uint8Array, not FastBuffer. We
80 | # don't currently have type to map to FastBuffer.
81 | NodeBufferFormat.Uint8Array,
82 | JSUint8Array,
83 | array("B", [1, 2, 3, 4]),
84 | ),
85 | ],
86 | )
87 | def test_NodeJsArrayBufferViewHostObjectHandler_serialize_host_object(
88 | node_type: NodeBufferFormat,
89 | py_type: type[JSTypedArray],
90 | data: Buffer,
91 | ) -> None:
92 | value = py_type(JSArrayBuffer(data))
93 |
94 | stream = WritableTagStream()
95 | stream.write_header()
96 | stream.write_host_object(value, serializer=NodeJsArrayBufferViewHostObjectHandler())
97 |
98 | rts = ReadableTagStream(stream.data)
99 | rts.read_header()
100 | result = rts.read_host_object(
101 | tag=True, deserializer=NodeJsArrayBufferViewHostObjectHandler()
102 | )
103 | assert rts.eof
104 | assert NodeBufferFormat(rts.data[3]) == node_type
105 | assert result == value
106 |
--------------------------------------------------------------------------------
/test/test_protocol_abc_inheritance.py:
--------------------------------------------------------------------------------
1 | """A simplified demo of the protocols defined in v8serialize.typing."""
2 |
3 | from __future__ import annotations
4 |
5 | from collections.abc import Sequence
6 | from typing import TYPE_CHECKING
7 | from typing_extensions import Protocol, TypeVar, overload, runtime_checkable
8 |
9 | T = TypeVar("T")
10 |
11 | if TYPE_CHECKING:
12 |
13 | class SpecialisedSequenceProtocol(Sequence[T]):
14 | def extra_method(self) -> None: ...
15 |
16 | else:
17 |
18 | @runtime_checkable
19 | class SpecialisedSequenceProtocol(Protocol[T]):
20 | def extra_method(self) -> None: ...
21 |
22 | Sequence.register(SpecialisedSequenceProtocol)
23 |
24 |
25 | class SpecialisedSequence(SpecialisedSequenceProtocol[T], Sequence[T]):
26 | pass
27 |
28 |
29 | class ConcreteSpecialisedSequence(SpecialisedSequence[T]):
30 | def extra_method(self) -> None:
31 | return None
32 |
33 | @overload
34 | def __getitem__(self, i: int, /) -> T: ...
35 |
36 | @overload
37 | def __getitem__(self, i: slice, /) -> Sequence[T]: ...
38 |
39 | def __getitem__(self, index: int | slice, /) -> T | Sequence[T]:
40 | raise NotImplementedError
41 |
42 | def __len__(self) -> int:
43 | raise NotImplementedError
44 |
45 |
46 | @SpecialisedSequence.register
47 | class OtherImpl(SpecialisedSequenceProtocol[T]):
48 | def extra_method(self) -> None:
49 | return
50 |
51 | @overload
52 | def __getitem__(self, i: int, /) -> T: ...
53 |
54 | @overload
55 | def __getitem__(self, i: slice, /) -> Sequence[T]: ...
56 |
57 | def __getitem__(self, index: int | slice, /) -> T | Sequence[T]:
58 | raise NotImplementedError
59 |
60 | def __len__(self) -> int:
61 | raise NotImplementedError
62 |
63 |
64 | class Unrelated:
65 | pass
66 |
67 |
68 | class CustomList(list[T]):
69 | def extra_method(self) -> None:
70 | raise NotImplementedError
71 |
72 |
73 | def test_other_impl() -> None:
74 | impl: SpecialisedSequenceProtocol[object] = OtherImpl()
75 | assert isinstance(impl, SpecialisedSequence)
76 | assert isinstance(impl, SpecialisedSequenceProtocol)
77 |
78 | seq: Sequence[object] = impl # noqa: F841
79 | seq2: Sequence[object] = OtherImpl() # noqa: F841
80 |
81 |
82 | def test_spec_seq() -> None:
83 | specs: SpecialisedSequenceProtocol[object] = ConcreteSpecialisedSequence()
84 | assert isinstance(specs, SpecialisedSequence)
85 | assert isinstance(specs, SpecialisedSequenceProtocol)
86 |
87 | seq: Sequence[object] = specs # noqa: F841
88 | seq2: Sequence[object] = ConcreteSpecialisedSequence() # noqa: F841
89 |
90 |
91 | def test_runtime_checkable() -> None:
92 | assert not isinstance(Unrelated(), SpecialisedSequenceProtocol)
93 | assert isinstance(CustomList(), SpecialisedSequenceProtocol)
94 |
--------------------------------------------------------------------------------
/test/test_protocol_dataclass_interaction.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 | from typing import TYPE_CHECKING, Protocol, runtime_checkable
5 |
6 | if TYPE_CHECKING:
7 |
8 | @runtime_checkable
9 | class Proto(Protocol):
10 | @property
11 | def thing(self) -> int: ...
12 |
13 | else:
14 |
15 | @runtime_checkable
16 | class Proto(Protocol):
17 | # Assigning a field is enough for @runtime_checkable to check its
18 | # presence, but without an annotation it won't affect @dataclass.
19 | thing = ...
20 |
21 |
22 | @dataclass
23 | class Thing(Proto):
24 | thing: int
25 |
26 |
27 | @dataclass
28 | class ImplicitThing:
29 | thing: int
30 |
31 |
32 | @dataclass
33 | class NonThing:
34 | thing2: int
35 |
36 |
37 | def test_runtime_checkable_dual_typing_runtime_protocol() -> None:
38 | assert isinstance(Thing(1), Proto)
39 | assert isinstance(ImplicitThing(1), Proto)
40 | assert not isinstance(NonThing(1), Proto)
41 |
42 |
43 | # Can also have one definition and override the descriptors immediately
44 | @runtime_checkable
45 | class Proto2(Protocol):
46 | if TYPE_CHECKING:
47 |
48 | @property
49 | def thing(self) -> int: ...
50 |
51 | else:
52 | thing = ...
53 |
54 |
55 | @dataclass
56 | class Thing2(Proto2):
57 | thing: int
58 |
59 |
60 | @dataclass
61 | class ImplicitThing2:
62 | thing: int
63 |
64 |
65 | @dataclass
66 | class NonThing2:
67 | thing2: int
68 |
69 |
70 | def test_runtime_checkable_removed_descriptor_runtime_protocol() -> None:
71 | # type check
72 | obj: Proto2 = Thing2(1)
73 | foo = ImplicitThing2(1)
74 | foo = NonThing2(1) # type: ignore[assignment]
75 |
76 | assert obj and foo # not unused
77 |
78 | assert isinstance(Thing2(1), Proto2)
79 | assert isinstance(ImplicitThing2(1), Proto2)
80 | assert not isinstance(NonThing2(1), Proto2)
81 |
--------------------------------------------------------------------------------
/test/test_references.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from copy import copy
4 |
5 | import pytest
6 |
7 | from v8serialize._references import (
8 | ObjectNotSerializedV8SerializeError,
9 | SerializedId,
10 | SerializedIdOutOfRangeV8SerializeError,
11 | SerializedObjectLog,
12 | )
13 |
14 |
15 | def test_serialized_object_log__objects_receive_sequential_ids_from_0() -> None:
16 | objects = SerializedObjectLog()
17 |
18 | assert objects.record_reference(object()) == SerializedId(0)
19 | assert objects.record_reference(object()) == SerializedId(1)
20 | same_obj = set[object]()
21 | assert objects.record_reference(same_obj) == SerializedId(2)
22 | assert objects.record_reference(same_obj) == SerializedId(3)
23 |
24 |
25 | def test_serialized_object_log__can_be_retrieved_by_id() -> None:
26 | obj1, obj2, set1, set2 = object(), object(), set[object](), set[object]()
27 | objects = SerializedObjectLog()
28 |
29 | obj1_id = objects.record_reference(obj1)
30 | obj2_id = objects.record_reference(obj2)
31 | set1_id = objects.record_reference(set1)
32 | set2_id = objects.record_reference(set2)
33 |
34 | assert objects.get_object(obj1_id) is obj1
35 | assert objects.get_object(obj2_id) is obj2
36 | assert objects.get_object(set1_id) is set1
37 | assert objects.get_object(set2_id) is set2
38 |
39 |
40 | def test_serialized_object_log__id_can_be_retrieved_by_object() -> None:
41 | obj1, obj2, set1, set2 = object(), object(), set[object](), set[object]()
42 | objects = SerializedObjectLog()
43 |
44 | obj1_id = objects.record_reference(obj1)
45 | obj2_id = objects.record_reference(obj2)
46 | set1_id = objects.record_reference(set1)
47 | set2_id = objects.record_reference(set2)
48 |
49 | assert objects.get_serialized_id(obj1) == obj1_id
50 | assert objects.get_serialized_id(obj2) == obj2_id
51 | assert objects.get_serialized_id(set1) == set1_id
52 | assert objects.get_serialized_id(set2) == set2_id
53 |
54 |
55 | def test_serialized_object_log__contains_referenced_objects() -> None:
56 | obj1, obj2, set1, set2 = object(), object(), set[object](), set[object]()
57 | objects = SerializedObjectLog()
58 |
59 | objects.record_reference(obj1)
60 | objects.record_reference(obj2)
61 | objects.record_reference(set1)
62 | objects.record_reference(set2)
63 |
64 | assert obj1 in objects
65 | assert obj2 in objects
66 | assert set1 in objects
67 | assert set2 in objects
68 |
69 |
70 | def test_serialized_object_log__does_not_contain_unreferenced_objects() -> None:
71 | obj1, obj2, set1, set2 = object(), object(), set[object](), set[object]()
72 | objects = SerializedObjectLog()
73 |
74 | objects.record_reference(obj1)
75 | objects.record_reference(obj2)
76 | objects.record_reference(set1)
77 | objects.record_reference(set2)
78 |
79 | assert copy(obj1) not in objects
80 | assert copy(obj2) not in objects
81 | assert copy(set1) not in objects
82 | assert copy(set2) not in objects
83 |
84 |
85 | def test_serialized_object_log__getting_unrecorded_id_throws() -> None:
86 | objects = SerializedObjectLog()
87 |
88 | with pytest.raises(SerializedIdOutOfRangeV8SerializeError) as exc_info:
89 | objects.get_object(SerializedId(42))
90 |
91 | assert exc_info.value.serialized_id == SerializedId(42)
92 | assert exc_info.value.message == "Serialized ID has not been recorded in the log"
93 |
94 |
95 | def test_serialized_object_log__getting_unrecorded_object_throws() -> None:
96 | objects = SerializedObjectLog()
97 |
98 | unrecorded = object()
99 | with pytest.raises(ObjectNotSerializedV8SerializeError) as exc_info:
100 | objects.get_serialized_id(unrecorded)
101 |
102 | assert exc_info.value.obj is unrecorded
103 | assert exc_info.value.message == "Object has not been recorded in the log"
104 |
--------------------------------------------------------------------------------
/test/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TypeVar
4 |
5 | T = TypeVar("T")
6 |
7 |
8 | def typeval(x: T) -> tuple[type[T], T]:
9 | return type(x), x
10 |
--------------------------------------------------------------------------------
/testing/smoketest/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h4l/v8serialize/ad6b5782fb24396b3e7b1a8d7d51130913390dab/testing/smoketest/README.md
--------------------------------------------------------------------------------
/testing/smoketest/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "smoketest"
3 | version = "0.1.0"
4 | description = "A package that depends on v8serialize with the oldest supported dependencies."
5 | authors = ["Your Name "]
6 | readme = "README.md"
7 |
8 | [tool.poetry.dependencies]
9 | python = "^3.9"
10 | v8serialize = "*"
11 | packaging = "<=14.5" # require the oldest version supported by v8serialize
12 |
13 | [build-system]
14 | requires = ["poetry-core"]
15 | build-backend = "poetry.core.masonry.api"
16 |
--------------------------------------------------------------------------------
/testing/smoketest/smoketest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from v8serialize import SerializationFeature, dumps, loads
4 |
5 |
6 | def main() -> None:
7 | msg = "Don't let the smoke out!"
8 | msg_out = loads(
9 | dumps(
10 | "Don't let the smoke out!",
11 | v8_version=SerializationFeature.MaxCompatibility.first_v8_version,
12 | )
13 | )
14 | if msg != msg_out:
15 | raise AssertionError("Smoke test failed")
16 | print(msg_out)
17 |
18 |
19 | if __name__ == "__main__":
20 | main()
21 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/.dockerignore:
--------------------------------------------------------------------------------
1 | .*
2 | npm
3 | Dockerfile
4 | docker-bake.hcl
5 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/.envrc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | version=$(
4 | grep -P --only-matching \
5 | '(?<=ECHOSERVER_VERSION = ")(\d+\.\d+\.\d+[\w-]*)(?=")' \
6 | main.ts
7 | )
8 |
9 | if [[ ! ${version?} ]]; then
10 | echo "Error: Failed to read ECHOSERVER_VERSION constant from main.ts" >&2
11 | exit 1
12 | fi
13 |
14 | export ECHOSERVER_VERSION=${version:?}
15 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/.gitignore:
--------------------------------------------------------------------------------
1 | npm
2 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to v8serialize/echoserver will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 | and this project adheres to
7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8 |
9 | ## [Unreleased]
10 |
11 | Nothing yet.
12 |
13 | ## [0.3.0] - 2024-09-24
14 |
15 | ### Added
16 |
17 | - Container healthcheck — The container image now includes a HEALTHCHECK that
18 | passes when the server is listening.
19 | ([#4](https://github.com/h4l/v8serialize/pull/4))
20 |
21 | ## [0.2.0] - 2024-09-24
22 |
23 | ### Added
24 |
25 | - Server introspection — `GET /` reports supported serialization features and
26 | software version numbers of the running server.
27 |
28 | ## [0.1.0] - 2024-08-28
29 |
30 | - The first stable release.
31 |
32 | [unreleased]:
33 | https://github.com/h4l/v8serialize/compare/echoserver-v0.3.0...HEAD
34 | [0.3.0]: https://github.com/h4l/v8serialize/releases/tag/echoserver-v0.3.0
35 | [0.2.0]: https://github.com/h4l/v8serialize/releases/tag/echoserver-v0.2.0
36 | [0.1.0]: https://github.com/h4l/v8serialize/releases/tag/echoserver-v0.1.0
37 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG DENO_VERSION NODE_VERSION DNT_NODE_VERSION DNT_DENO_VERSION
2 |
3 | FROM denoland/deno:${DENO_VERSION:-alpine} AS echoserver-deno
4 |
5 | RUN apk add --no-cache curl
6 | COPY main.ts .
7 | USER deno
8 |
9 | ENV V8SERIALIZE_LOG_WITH_COLOR=true \
10 | V8SERIALIZE_LOG_LISTEN=true \
11 | V8SERIALIZE_LOG_RE_SERIALIZATION=text \
12 | V8SERIALIZE_PORT=8000 \
13 | V8SERIALIZE_HOSTNAME=0.0.0.0
14 |
15 | EXPOSE 8000
16 |
17 | CMD ["run", "--no-remote", "--allow-net", "--allow-env", "main.ts"]
18 |
19 | HEALTHCHECK --start-period=20s --start-interval=1s \
20 | CMD curl --fail --silent --show-error -o /dev/null \
21 | "http://localhost:${V8SERIALIZE_PORT:?}/?healthcheck"
22 |
23 |
24 | FROM denoland/deno:bin-${DNT_DENO_VERSION:-latest} AS dnt-deno
25 |
26 |
27 | FROM node:${DNT_NODE_VERSION:-latest} AS npm-package
28 |
29 | COPY --from=dnt-deno /deno /usr/local/bin/deno
30 |
31 | WORKDIR /build
32 |
33 | COPY . ./
34 | RUN deno -A scripts/build_npm.ts
35 | RUN npm pack ./npm
36 |
37 |
38 | FROM node:${NODE_VERSION:-alpine} AS echoserver-node
39 | ARG ECHOSERVER_VERSION
40 |
41 | RUN apk add --no-cache tini curl
42 | WORKDIR /app
43 |
44 | RUN --mount=from=npm-package,source=/build/,target=/build/ \
45 | npm install "/build/v8serialize-echoserver-${ECHOSERVER_VERSION:?}.tgz"
46 |
47 | ENV V8SERIALIZE_LOG_WITH_COLOR=true \
48 | V8SERIALIZE_LOG_LISTEN=true \
49 | V8SERIALIZE_LOG_RE_SERIALIZATION=text \
50 | V8SERIALIZE_PORT=8000 \
51 | V8SERIALIZE_HOSTNAME=0.0.0.0
52 |
53 | EXPOSE 8000
54 | USER node
55 |
56 | ENTRYPOINT ["/sbin/tini", "--", "docker-entrypoint.sh"]
57 | CMD ["-e", "require('v8serialize-echoserver').main()"]
58 |
59 | HEALTHCHECK --start-period=20s --start-interval=1s \
60 | CMD curl --fail --silent --show-error -o /dev/null \
61 | "http://localhost:${V8SERIALIZE_PORT:?}/?healthcheck"
62 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "dev": "deno run --allow-net --watch main.ts"
4 | },
5 | "imports": {
6 | "@deno/dnt": "jsr:@deno/dnt@^0.41.3"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "3",
3 | "packages": {
4 | "specifiers": {
5 | "jsr:@david/code-block-writer@^13.0.2": "jsr:@david/code-block-writer@13.0.2",
6 | "jsr:@deno/cache-dir@^0.10.3": "jsr:@deno/cache-dir@0.10.3",
7 | "jsr:@deno/dnt@^0.41.3": "jsr:@deno/dnt@0.41.3",
8 | "jsr:@deno/graph@^0.73.1": "jsr:@deno/graph@0.73.1",
9 | "jsr:@std/assert@^0.223.0": "jsr:@std/assert@0.223.0",
10 | "jsr:@std/assert@^0.226.0": "jsr:@std/assert@0.226.0",
11 | "jsr:@std/bytes@^0.223.0": "jsr:@std/bytes@0.223.0",
12 | "jsr:@std/encoding": "jsr:@std/encoding@1.0.3",
13 | "jsr:@std/fmt@1": "jsr:@std/fmt@1.0.1",
14 | "jsr:@std/fmt@^0.223": "jsr:@std/fmt@0.223.0",
15 | "jsr:@std/fs@1": "jsr:@std/fs@1.0.2",
16 | "jsr:@std/fs@^0.223": "jsr:@std/fs@0.223.0",
17 | "jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3",
18 | "jsr:@std/io@^0.223": "jsr:@std/io@0.223.0",
19 | "jsr:@std/path@1": "jsr:@std/path@1.0.3",
20 | "jsr:@std/path@1.0.0-rc.1": "jsr:@std/path@1.0.0-rc.1",
21 | "jsr:@std/path@^0.223": "jsr:@std/path@0.223.0",
22 | "jsr:@std/path@^0.225.2": "jsr:@std/path@0.225.2",
23 | "jsr:@std/path@^1.0.3": "jsr:@std/path@1.0.3",
24 | "jsr:@ts-morph/bootstrap@^0.24.0": "jsr:@ts-morph/bootstrap@0.24.0",
25 | "jsr:@ts-morph/common@^0.24.0": "jsr:@ts-morph/common@0.24.0",
26 | "npm:@types/node": "npm:@types/node@18.16.19"
27 | },
28 | "jsr": {
29 | "@david/code-block-writer@13.0.2": {
30 | "integrity": "14dd3baaafa3a2dea8bf7dfbcddeccaa13e583da2d21d666c01dc6d681cd74ad"
31 | },
32 | "@deno/cache-dir@0.10.3": {
33 | "integrity": "eb022f84ecc49c91d9d98131c6e6b118ff63a29e343624d058646b9d50404776",
34 | "dependencies": [
35 | "jsr:@deno/graph@^0.73.1",
36 | "jsr:@std/fmt@^0.223",
37 | "jsr:@std/fs@^0.223",
38 | "jsr:@std/io@^0.223",
39 | "jsr:@std/path@^0.223"
40 | ]
41 | },
42 | "@deno/dnt@0.41.3": {
43 | "integrity": "b2ef2c8a5111eef86cb5bfcae103d6a2938e8e649e2461634a7befb7fc59d6d2",
44 | "dependencies": [
45 | "jsr:@david/code-block-writer@^13.0.2",
46 | "jsr:@deno/cache-dir@^0.10.3",
47 | "jsr:@std/fmt@1",
48 | "jsr:@std/fs@1",
49 | "jsr:@std/path@1",
50 | "jsr:@ts-morph/bootstrap@^0.24.0"
51 | ]
52 | },
53 | "@deno/graph@0.73.1": {
54 | "integrity": "cd69639d2709d479037d5ce191a422eabe8d71bb68b0098344f6b07411c84d41"
55 | },
56 | "@std/assert@0.223.0": {
57 | "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24"
58 | },
59 | "@std/assert@0.226.0": {
60 | "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3"
61 | },
62 | "@std/bytes@0.223.0": {
63 | "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8"
64 | },
65 | "@std/encoding@1.0.3": {
66 | "integrity": "5dbc2d7f5aa6062de7e19862ea856ac7a0dcce0b6fb46bb7b332d3bdcd4408b7"
67 | },
68 | "@std/fmt@0.223.0": {
69 | "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208"
70 | },
71 | "@std/fmt@1.0.1": {
72 | "integrity": "ef76c37faa7720faa8c20fd8cc74583f9b1e356dfd630c8714baa716a45856ab"
73 | },
74 | "@std/fs@0.223.0": {
75 | "integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c"
76 | },
77 | "@std/fs@0.229.3": {
78 | "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb",
79 | "dependencies": [
80 | "jsr:@std/path@1.0.0-rc.1"
81 | ]
82 | },
83 | "@std/fs@1.0.2": {
84 | "integrity": "af57555c7a224a6f147d5cced5404692974f7a628ced8eda67e0d22d92d474ec",
85 | "dependencies": [
86 | "jsr:@std/path@^1.0.3"
87 | ]
88 | },
89 | "@std/io@0.223.0": {
90 | "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1",
91 | "dependencies": [
92 | "jsr:@std/assert@^0.223.0",
93 | "jsr:@std/bytes@^0.223.0"
94 | ]
95 | },
96 | "@std/path@0.223.0": {
97 | "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989",
98 | "dependencies": [
99 | "jsr:@std/assert@^0.223.0"
100 | ]
101 | },
102 | "@std/path@0.225.2": {
103 | "integrity": "0f2db41d36b50ef048dcb0399aac720a5348638dd3cb5bf80685bf2a745aa506",
104 | "dependencies": [
105 | "jsr:@std/assert@^0.226.0"
106 | ]
107 | },
108 | "@std/path@1.0.0-rc.1": {
109 | "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6"
110 | },
111 | "@std/path@1.0.3": {
112 | "integrity": "cd89d014ce7eb3742f2147b990f6753ee51d95276bfc211bc50c860c1bc7df6f"
113 | },
114 | "@ts-morph/bootstrap@0.24.0": {
115 | "integrity": "a826a2ef7fa8a7c3f1042df2c034d20744d94da2ee32bf29275bcd4dffd3c060",
116 | "dependencies": [
117 | "jsr:@ts-morph/common@^0.24.0"
118 | ]
119 | },
120 | "@ts-morph/common@0.24.0": {
121 | "integrity": "12b625b8e562446ba658cdbe9ad77774b4bd96b992ae8bd34c60dbf24d06c1f3",
122 | "dependencies": [
123 | "jsr:@std/fs@^0.229.3",
124 | "jsr:@std/path@^0.225.2"
125 | ]
126 | }
127 | },
128 | "npm": {
129 | "@types/node@18.16.19": {
130 | "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
131 | "dependencies": {}
132 | }
133 | }
134 | },
135 | "remote": {},
136 | "workspace": {
137 | "dependencies": [
138 | "jsr:@deno/dnt@^0.41.3"
139 | ]
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/docker-bake.hcl:
--------------------------------------------------------------------------------
1 | DNT_NODE_VERSION = "22"
2 | DNT_DENO_VERSION = "1.46.1"
3 |
4 | // Set via .envrc
5 | variable "ECHOSERVER_VERSION" {}
6 |
7 | variable "CI" { default = "" }
8 |
9 | group "default" {
10 | targets = ["echoserver-deno", "echoserver-node"]
11 | }
12 |
13 | target "_base" {
14 | args = {
15 | DNT_NODE_VERSION = DNT_NODE_VERSION,
16 | DNT_DENO_VERSION = DNT_DENO_VERSION,
17 | ECHOSERVER_VERSION = ECHOSERVER_VERSION,
18 | }
19 | labels = {
20 | "org.opencontainers.image.title" = "v8serialize-echoserver"
21 | "org.opencontainers.image.version" = ECHOSERVER_VERSION
22 | }
23 | platforms = CI == "true" ? ["linux/amd64", "linux/arm64"] : ["local"]
24 | }
25 |
26 | target "echoserver-deno" {
27 | name = "echoserver-deno-${replace(version, ".", "-")}"
28 | matrix = {
29 | version = [ "1.46.1" ]
30 | }
31 | inherits = ["_base"]
32 | target = "echoserver-deno"
33 | args = {
34 | DENO_VERSION = "alpine-${version}",
35 | }
36 | tags = [
37 | "ghcr.io/h4l/v8serialize/echoserver:deno-${version}",
38 | "ghcr.io/h4l/v8serialize/echoserver:${ECHOSERVER_VERSION}-deno-${version}",
39 | ]
40 | labels = {
41 | "org.opencontainers.image.title" = "v8serialize-echoserver-deno"
42 | "org.opencontainers.image.description" = "v8serialize-echoserver running on deno ${version}"
43 | }
44 | }
45 |
46 | target "echoserver-node" {
47 | name = "echoserver-node-${version}"
48 | matrix = {
49 | version = ["18", "22"]
50 | }
51 | inherits = ["_base"]
52 | target = "echoserver-node"
53 | args = {
54 | NODE_VERSION = "${version}-alpine",
55 | }
56 | tags = [
57 | "ghcr.io/h4l/v8serialize/echoserver:node-${version}",
58 | "ghcr.io/h4l/v8serialize/echoserver:${ECHOSERVER_VERSION}-node-${version}",
59 | ]
60 | labels = {
61 | "org.opencontainers.image.title" = "v8serialize-echoserver-node"
62 | "org.opencontainers.image.description" = "v8serialize-echoserver running on NodeJS ${version}"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/testing/v8serialize-echo/scripts/build_npm.ts:
--------------------------------------------------------------------------------
1 | // ex. scripts/build_npm.ts
2 | import { build, emptyDir } from "@deno/dnt";
3 |
4 | await emptyDir("./npm");
5 |
6 | function readVersion(filename: string): string {
7 | const versionPattern = /ECHOSERVER_VERSION = "(\d+\.\d+\.\d+[\w-]*)"/g;
8 | const version = versionPattern.exec(Deno.readTextFileSync(filename))?.[1];
9 | if (!version) {
10 | throw new Error(
11 | `No version found in '${filename}' matching pattern ${versionPattern}`,
12 | );
13 | }
14 | return version;
15 | }
16 |
17 | await build({
18 | entryPoints: ["./main.ts"],
19 | outDir: "./npm",
20 | shims: {
21 | // see JS docs for overview and more options
22 | deno: true,
23 | },
24 | package: {
25 | // package.json properties
26 | name: "v8serialize-echoserver",
27 | version: readVersion("main.ts"),
28 | description:
29 | "A simple HTTP server that deserializes a V8-serialized payload and reserializes it in the response.",
30 | license: "MIT",
31 | repository: {
32 | type: "git",
33 | url: "git+https://github.com/h4l/v8serialize.git",
34 | },
35 | bugs: {
36 | url: "https://github.com/h4l/v8serialize/issues",
37 | },
38 | },
39 | postBuild() {
40 | // steps to run after building and before running the tests
41 | },
42 | });
43 |
--------------------------------------------------------------------------------