Compare commits

..

24 Commits

Author SHA1 Message Date
Ted Patrick
52bd64c80d Update CI tests (#1719) 2023-09-15 12:12:33 -05:00
Ted Patrick
5c90b19707 Update tests for CI on next (#1718) 2023-09-15 11:26:00 -05:00
Ted Patrick
eb0d1ff740 Update CI tests on Next (#1717) 2023-09-15 10:59:35 -05:00
Ted Patrick
ca8a22e5db Turn on CI tests for next (#1716) 2023-09-15 10:44:54 -05:00
Ted Patrick
63fab105b2 Fix path variable (#1710)
* Fix sed delimiter for html generation

* Fix [[PATH]] to _PATH

* Fix sed command
2023-09-14 13:36:07 -05:00
Ted Patrick
71ca7753e1 Fix sed delimiter for html generation (#1707) 2023-09-14 12:58:08 -05:00
Ted Patrick
6dced95954 Generate HTML for snapshot (#1706)
* Generate HTML for snapshot

* format
2023-09-14 12:45:33 -05:00
Ted Patrick
ffd9e6f4d0 Next Build Snapshot (#1703) 2023-09-14 11:04:13 -05:00
Kees Hink
b9c3eb6442 Fix minor spelling error in getting-started.md (#1671)
it's -> its
2023-09-04 07:00:14 -07:00
Shubhashis
be79f70f66 Fix #1577: Change script sources that include Cross-Origin Resource Policy (#1627) 2023-08-09 10:35:57 +02:00
Fabio Pliger
71e2555213 fix LICENSE info around dates and project owner (#1606)
* fix info around dates and project owner

* add originated at Anaconda, Inc to license

* add . on Inc

---------

Co-authored-by: Fabio Pliger <fpliger@anaconda.com>
2023-08-08 08:21:50 -07:00
Aapo Soukkio
1b9cb562ec Remove unused comment (#1604) 2023-07-20 11:34:30 -05:00
Neon22
55cf6b9e4c Update py-click.md (#1553)
used wrong file extension so link was missing
2023-06-22 08:12:11 -05:00
Neon22
9a70943a36 Update when.md (#1554)
* Update when.md: include info on importing when to get access

---------

Co-authored-by: Jeff Glass <glass.jeffrey@gmail.com>
2023-06-21 17:56:47 -05:00
Jeff Glass
7765a629c7 Update verison (#1552)
* Update verison

* Fix test to account for single-digit months
2023-06-21 06:23:26 -05:00
Ted Patrick
848d77b1c2 Same steps as build-unstable (#1550)
* Same steps as build-unstable

* make setup
2023-06-20 13:01:56 -05:00
Ted Patrick
e4e8f2edae Fix build unstable to run on ubuntu-latest-8core (#1549)
* Fix build unstable to run on ubuntu-latest-8core

* ubuntu-latest to ubuntu-latest-8core
2023-06-20 12:17:08 -05:00
Jeff Glass
ea9bdcc961 Fix previous changlog date (#1546)
* Previous version updated from 2023.01.1 to 2023.03.1
2023-06-20 08:08:24 -05:00
Hood Chatham
79ad39260e Fix lifetime for pyExec results (#1540)
Currently if the result from pyExec is a PyProxy, it gets destroyed.
This switches to using `to_js` to handle this (it is better to use
than an explicit `create_proxy` since it automatically decides whether
to create a proxy or not).

I also added `destroyIfProxy` which checks if something is a `PyProxy`
and then destroys it. Each use of `pyExec` needs to call `destroyIfProxy`
on the result after it is done with it.
2023-06-15 11:51:36 -07:00
Jeff Glass
f4936316ab Make getPySrc() a Documented Feature (#1516)
* Add documentation and tests (no real code changes)

* Add Changelog
2023-06-14 12:56:57 -05:00
Madhur Tandon
8879187e6a fix access to interpreter globals without awaits (#1529) 2023-06-14 19:42:38 +05:30
Fábio Rosado
258b80a6a5 Add step to install next deps and run test (#1528) 2023-06-14 14:12:42 +01:00
Fábio Rosado
a108e6e97e Add workflow to run next CI (#1527)
* Add workflow to run next CI

* Don't actually replace the whole unstable

* Unstable name

* Add empty line

* Make CI run on changes to workflow build-unstable-next

* Rename workflow for test-next instead of build

* Update path to use the next test-next

* Add pyscript.core to path
2023-06-13 15:06:19 +01:00
Peter W
dfef7eda3b Use 8-core, 32GB runners for testing and faster builds (#1524)
* swap 3 jobs to 8 core runners

* 16 core test

* test 4 core

* back to 8 core
2023-06-12 15:25:58 -05:00
584 changed files with 38138 additions and 22597 deletions

View File

@@ -11,9 +11,7 @@ body:
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
- type: checkboxes

View File

@@ -4,9 +4,12 @@
## Changes
<!-- List the technical changes done to fix a bug or introduce a new feature. -->
<!-- List the changes done to fix a bug or introduce a new feature.Please note both user-facing changes and changes to internal API's here -->
## Checklist
- [ ] I have checked `make build` works locally.
- [ ] I have created / updated documentation for this change (if applicable).
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
- [ ] All tests pass locally
- [ ] I have updated `docs/changelog.md`
- [ ] I have created documentation for this(if applicable)

View File

@@ -1,13 +0,0 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly

142
.github/workflows/build-unstable.yml vendored Normal file
View File

@@ -0,0 +1,142 @@
name: "[CI] Build Unstable"
on:
push: # Only run on merges into main that modify files under pyscriptjs/ and examples/
branches:
- main
paths:
- pyscriptjs/**
- examples/**
- .github/workflows/build-unstable.yml # Test that workflow works when changed
pull_request: # Run on any PR that modifies files under pyscriptjs/ and examples/
branches:
- main
paths:
- pyscriptjs/**
- examples/**
- .github/workflows/build-unstable.yml # Test that workflow works when changed
workflow_dispatch:
jobs:
BuildAndTest:
runs-on: ubuntu-latest-8core
defaults:
run:
working-directory: pyscriptjs
env:
MINICONDA_PYTHON_VERSION: py38
MINICONDA_VERSION: 4.11.0
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v2
- name: Setup Environment
run: make setup
- name: Build
run: make build
- name: TypeScript Tests
run: make test-ts
- name: Python Tests
run: make test-py
- name: Integration Tests
run: make test-integration-parallel
- name: Examples Tests
run: make test-examples
- uses: actions/upload-artifact@v3
with:
name: pyscript
path: |
pyscriptjs/build/
if-no-files-found: error
retention-days: 7
- uses: actions/upload-artifact@v3
if: success() || failure()
with:
name: test_results
path: pyscriptjs/test_results
if-no-files-found: error
eslint:
runs-on: ubuntu-latest-8core
defaults:
run:
working-directory: pyscriptjs
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: npm install
run: npm install
- name: Eslint
run: npx eslint src -c .eslintrc.js
Deploy:
runs-on: ubuntu-latest-8core
needs: BuildAndTest
if: github.ref == 'refs/heads/main' # Only deploy on merge into main
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: pyscript
path: ./build/
# Deploy to S3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3
run: aws s3 sync --quiet ./build/ s3://pyscript.net/unstable/

62
.github/workflows/docs-release.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: "[Docs] Build Release"
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest-8core
permissions:
contents: read
id-token: write
env:
SPHINX_HTML_BASE_URL: https://docs.pyscript.net/
steps:
- name: Checkout
uses: actions/checkout@v3
with:
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Setup
uses: conda-incubator/setup-miniconda@v2
with:
auto-update-conda: true
activate-environment: docs
environment-file: docs/environment.yml
python-version: "3.9"
- name: Build
shell: bash -l {0}
run: |
cd docs/
make html
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: pyscript-docs-${{ github.ref_name }}
path: docs/_build/html/
# Deploy to S3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Copy redirect file
run: aws s3 cp --quiet ./docs/_build/html/_static/redirect.html s3://docs.pyscript.net/index.html
- name: Sync to S3
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/${{ github.ref_name }}/
# Make sure to remove the latest folder so we sync the full docs upon release
- name: Delete latest directory
run: aws s3 rm --recursive s3://docs.pyscript.net/latest/
# Note that the files are the same as above, but we want to have folders with
# /<tag name>/ AND /latest/ which latest will always point to the latest release
- name: Sync to /latest
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/latest/

53
.github/workflows/docs-review.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: "[Docs] Build Review"
on:
pull_request:
branches:
- "*"
paths:
- docs/**
concurrency:
# Concurrency group that uses the workflow name and PR number if available
# or commit SHA as a fallback. If a new build is triggered under that
# concurrency group while a previous build is running it will be canceled.
# Repeated pushes to a PR will cancel all previous builds, while multiple
# merges to main will not cancel.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
build:
if: github.repository_owner == 'pyscript'
runs-on: ubuntu-latest-8core
permissions:
contents: read
id-token: write
env:
SPHINX_HTML_BASE_URL: https://docs.pyscript.net/
steps:
- name: Checkout
uses: actions/checkout@v3
with:
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Setup
uses: conda-incubator/setup-miniconda@v2
with:
auto-update-conda: true
activate-environment: docs
environment-file: docs/environment.yml
python-version: "3.9"
- name: Build
shell: bash -l {0}
run: |
cd docs/
make html
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: pyscript-docs-review-${{ github.event.number }}
path: docs/_build/html/

58
.github/workflows/docs-unstable.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: "[Docs] Build Latest"
on:
push:
branches:
- main
paths:
- docs/**
jobs:
build:
runs-on: ubuntu-latest-8core
permissions:
contents: read
id-token: write
env:
SPHINX_HTML_BASE_URL: https://docs.pyscript.net/
steps:
- name: Checkout
uses: actions/checkout@v3
with:
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Setup
uses: conda-incubator/setup-miniconda@v2
with:
auto-update-conda: true
activate-environment: docs
environment-file: docs/environment.yml
python-version: "3.9"
- name: Build
shell: bash -l {0}
run: |
cd docs/
make html
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: pyscript-docs-latest
path: docs/_build/html/
# Deploy to S3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
# Sync will only copy changed files
- name: Sync Error
run: aws s3 cp --quiet ./docs/_static/s3_error.html s3://docs.pyscript.net/error.html
# Sync will only copy changed files
- name: Sync to S3
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/unstable/

View File

@@ -0,0 +1,62 @@
name: "Next Build Snapshot"
on:
workflow_dispatch:
inputs:
snapshot_version:
description: "The calver version of this snapshot: 2022.09.1 or 2022.09.1.RC1"
type: string
required: true
jobs:
next-build-snapshot:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: next
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install Dependencies
working-directory: ./pyscript.core
run: npm install
- name: Build Pyscript.core
working-directory: ./pyscript.core
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Generate index.html in snapshot
working-directory: .
run: sed 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' ./public/index.html > ./pyscript.core/dist/index.html
- name: Copy to Snapshot
working-directory: ./pyscript.core/dist
run: >
aws s3 sync . s3://pyscript.net/snapshots/${{ inputs.snapshot_version }}/

View File

@@ -1,43 +1,32 @@
name: "Prepare Release"
name: "[CI] Prepare Release"
on:
push:
tags:
- "[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9]+" # YYYY.MM.MICRO
env:
MINICONDA_PYTHON_VERSION: py38
MINICONDA_VERSION: 4.11.0
defaults:
run:
working-directory: ./core
working-directory: pyscriptjs
jobs:
prepare-release:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest-8core
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v5
uses: actions/setup-node@v3
with:
node-version: 20.x
- name: Python venv
run: python -m venv env
- name: Activate Python
run: source env/bin/activate
- name: Update pip
run: pip install --upgrade pip
- name: Install PyMinifier
run: pip install --ignore-requires-python python-minifier
- name: Install Setuptools
run: pip install setuptools
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
@@ -49,21 +38,32 @@ jobs:
${{ runner.os }}-build-
${{ runner.os }}-
- name: NPM Install
run: npm install && npx playwright install chromium
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v2
- name: Setup Environment
run: make setup
- name: Build
run: npm run build
run: make build
- name: Generate index.html
working-directory: .
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
- name: TypeScript Tests
run: make test-ts
- name: Zip dist folder
run: zip -r -q ./build.zip ./dist
- name: Python Tests
run: make test-py
- name: Integration Tests
run: make test-integration-parallel
- name: Examples Tests
run: make test-examples
- name: Zip build folder
run: zip -r -q ./build.zip ./build
- name: Prepare Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
with:
draft: true
prerelease: true

View File

@@ -1,45 +1,34 @@
name: "Publish Release"
name: "[CI] Publish Release"
on:
release:
types: [published]
env:
MINICONDA_PYTHON_VERSION: py38
MINICONDA_VERSION: 4.11.0
defaults:
run:
working-directory: ./core
working-directory: pyscriptjs
jobs:
publish-release:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest-8core
permissions:
contents: read
id-token: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v5
uses: actions/setup-node@v3
with:
node-version: 20.x
- name: Python venv
run: python -m venv env
- name: Activate Python
run: source env/bin/activate
- name: Update pip
run: pip install --upgrade pip
- name: Install PyMinifier
run: pip install --ignore-requires-python python-minifier
- name: Install Setuptools
run: pip install setuptools
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
@@ -51,38 +40,36 @@ jobs:
${{ runner.os }}-build-
${{ runner.os }}-
- name: npm install
run: npm install && npx playwright install chromium
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v2
- name: build
run: npm run build
- name: Setup Environment
run: make setup
- name: build offline
run: npm run build:offline
- name: Build
run: make build
- name: Rename offline.zip with version metadata
run: mv ./dist/offline.zip ./dist/offline_${{ github.ref_name }}.zip
- name: TypeScript Tests
run: make test-ts
- name: Generate index.html in snapshot
working-directory: .
run: sed -e 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#g' -e 's#_DOC_VERSION_#${{ github.ref_name }}#g' -e 's#_TAG_VERSION_#/tag/${{ github.ref_name }}#g' -e 's#_VERSION_#${{ github.ref_name }}#g' ./public/index.html > ./core/dist/index.html
- name: Python Tests
run: make test-py
- name: Generate release.tar from snapshot and put it in dist/
working-directory: .
run: tar -cvf ../release.tar * && mv ../release.tar .
- name: Integration Tests
run: make test-integration-parallel
- name: Upload offline.zip to release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload ${{ github.ref_name }} ./dist/offline_${{ github.ref_name }}.zip
- name: Examples Tests
run: make test-examples
# Upload to S3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3
run:
| # Create an explicitly versioned directory under releases/YYYY.MM.MICRO/
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/
| # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
aws s3 sync --quiet ./build/ s3://pyscript.net/latest/
aws s3 sync --quiet ./build/ s3://pyscript.net/releases/${{ github.ref_name }}/

View File

@@ -1,4 +1,5 @@
name: "Publish Snapshot"
name: "[CI] Publish Snapshot"
# Copy /unstable/ to /snapshots/2022.09.1.RC1/
on:
workflow_dispatch:
@@ -8,69 +9,18 @@ on:
type: string
required: true
defaults:
run:
working-directory: ./core
jobs:
publish-snapshot:
runs-on: ubuntu-latest
snapshot:
runs-on: ubuntu-latest-8core
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install node
uses: actions/setup-node@v5
with:
node-version: 20.x
- name: Python venv
run: python -m venv env
- name: Activate Python
run: source env/bin/activate
- name: Update pip
run: pip install --upgrade pip
- name: Install PyMinifier
run: pip install --ignore-requires-python python-minifier
- name: Install Setuptools
run: pip install setuptools
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install Dependencies
run: npm install && npx playwright install chromium
- name: Build Pyscript.core
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Generate index.html in snapshot
working-directory: .
run: sed -e 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' -e 's#_DOC_VERSION_#${{ inputs.snapshot_version }}#' -e 's#_TAG_VERSION_#/tag/${{ inputs.snapshot_version }}#' -e 's#_VERSION_#${{ inputs.snapshot_version }}#' ./public/index.html > ./core/dist/index.html
- name: Copy to Snapshot
- name: Sync to S3
run: >
aws s3 sync ./dist/ s3://pyscript.net/snapshots/${{ inputs.snapshot_version }}/
aws s3 sync s3://pyscript.net/unstable/ s3://pyscript.net/snapshots/${{ inputs.snapshot_version }}/

View File

@@ -1,76 +0,0 @@
name: "Publish Unstable"
on:
push: # Only run on merges into main that modify files under core/ and examples/
branches:
- main
paths:
- core/**
- examples/**
workflow_dispatch:
jobs:
publish-unstable:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
defaults:
run:
working-directory: ./core
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install node
uses: actions/setup-node@v5
with:
node-version: 20.x
- name: Python venv
run: python -m venv env
- name: Activate Python
run: source env/bin/activate
- name: Update pip
run: pip install --upgrade pip
- name: Install PyMinifier
run: pip install --ignore-requires-python python-minifier
- name: Install Setuptools
run: pip install setuptools
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: NPM Install
run: npm install && npx playwright install chromium
- name: Build
run: npm run build
- name: Generate index.html in snapshot
working-directory: .
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3
run: aws s3 sync --quiet ./dist/ s3://pyscript.net/unstable/

29
.github/workflows/sync-examples.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: "[CI] Sync Examples"
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest-8core
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: examples
steps:
# Deploy to S3
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name:
Sync to S3
# Sync outdated or new files, delete ones no longer in source
run: aws s3 sync --quiet --delete . s3://pyscript.net/examples/ # Sync directory, delete what is not in source

119
.github/workflows/test-next.yml vendored Normal file
View File

@@ -0,0 +1,119 @@
name: "[CI] Test Next"
on:
push: # Only run on merges into main that modify files under pyscriptjs/ and examples/
branches:
- next
paths:
- pyscript.core/**
- pyscriptjs/**
- examples/**
- .github/workflows/test-next.yml # Test that workflow works when changed
pull_request: # Run on any PR that modifies files under pyscriptjs/ and examples/
branches:
- next
paths:
- pyscript.core/**
- examples/**
- .github/workflows/test-next.yml # Test that workflow works when changed
workflow_dispatch:
jobs:
BuildAndTest:
runs-on: ubuntu-latest-8core
env:
MINICONDA_PYTHON_VERSION: py38
MINICONDA_VERSION: 4.11.0
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: next
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v2
- name: Setup Environment
working-directory: ./pyscriptjs
run: make setup
- name: Python Tests
working-directory: ./pyscriptjs
run: make test
- name: Integration Tests
working-directory: ./pyscriptjs
run: make test-integration
#run: make test-integration-parallel
- name: Examples Tests
working-directory: ./pyscriptjs
run: make test-examples
- uses: actions/upload-artifact@v3
with:
name: pyscript
path: |
pyscriptjs/build/
if-no-files-found: error
retention-days: 7
- uses: actions/upload-artifact@v3
if: success() || failure()
with:
name: test_results
path: pyscriptjs/test_results
if-no-files-found: error
eslint:
runs-on: ubuntu-latest-8core
defaults:
run:
working-directory: pyscriptjs
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: next
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: npm install
run: npm install
- name: Eslint
run: npx eslint src -c .eslintrc.js

View File

@@ -1,80 +0,0 @@
name: "[CI] Test"
on:
push: # Only run on merges into main that modify certain files
branches:
- main
paths:
- core/**
- .github/workflows/test.yml
pull_request: # Only run on merges into main that modify certain files
branches:
- main
paths:
- core/**
- .github/workflows/test.yml
workflow_dispatch:
jobs:
BuildAndTest:
runs-on: ubuntu-latest
env:
MINICONDA_PYTHON_VERSION: py38
MINICONDA_VERSION: 4.11.0
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 3
# display a git log: when you run CI on PRs, github automatically
# merges the PR into main and run the CI on that commit. The idea
# here is to show enough of git log to understand what is the
# actual commit (in the PR) that we are using. See also
# 'fetch-depth: 3' above.
- name: git log
run: git log --graph -3
- name: Install node
uses: actions/setup-node@v5
with:
node-version: 20.x
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v3
- name: Create and activate virtual environment
run: |
python3 -m venv test_venv
source test_venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
echo VIRTUAL_ENV=$VIRTUAL_ENV >> $GITHUB_ENV
- name: Setup dependencies in virtual environment
run: |
make setup
- name: Build
run: make build # Integration tests run in the build step.
- uses: actions/upload-artifact@v4
with:
name: pyscript
path: |
core/dist/
if-no-files-found: error
retention-days: 7

16
.github/workflows/test_report.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Test Report
on:
workflow_run:
workflows: ['\[CI\] Build Unstable']
types:
- completed
jobs:
report:
runs-on: ubuntu-latest-8core
steps:
- uses: dorny/test-reporter@v1.6.0
with:
artifact: test_results
name: Test reports
path: "*.xml"
reporter: java-junit

11
.gitignore vendored
View File

@@ -51,6 +51,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pyscriptjs/examples
# Translations
*.mo
@@ -140,13 +141,3 @@ coverage/
# junit xml for test results
test_results
# @pyscript/core npm artifacts
core/test-results/*
core/core.*
core/dist
core/dist.zip
core/src/plugins.js
core/src/stdlib/pyscript.js
core/src/3rd-party/*
!core/src/3rd-party/READMEmd

View File

@@ -1,52 +1,60 @@
# This is the configuration for pre-commit, a local framework for managing pre-commit hooks
# Check out the docs at: https://pre-commit.com/
ci:
#skip: [eslint]
skip: [eslint]
autoupdate_schedule: monthly
default_stages: [pre-commit]
default_stages: [commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v4.4.0
hooks:
- id: check-builtin-literals
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-json
exclude: tsconfig\.json
exclude: tsconfig.json
- id: check-toml
exclude: bad\.toml
- id: check-xml
- id: check-yaml
- id: detect-private-key
- id: end-of-file-fixer
exclude: core/dist|\.min\.js$
exclude: \.min\.js$
- id: trailing-whitespace
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.9.0
hooks:
- id: black
exclude: core/tests
args: ["-l", "88", "--skip-string-normalization"]
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell # See 'pyproject.toml' for args
exclude: fs\.py|\.js\.map$
additional_dependencies:
- tomli
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.257
hooks:
- id: ruff
exclude: core/tests
args: [--fix]
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/codespell-project/codespell
rev: v2.2.4
hooks:
- id: codespell # See 'pyproject.toml' for args
additional_dependencies:
- tomli
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
rev: "v3.0.0-alpha.6"
hooks:
- id: prettier
exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
args: [--tab-width, "4"]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.36.0
hooks:
- id: eslint
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
types: [file]
additional_dependencies:
- eslint@8.25.0
- typescript@5.0.4
- "@typescript-eslint/eslint-plugin@5.58.0"
- "@typescript-eslint/parser@5.58.0"

View File

@@ -1,4 +1,5 @@
ISSUE_TEMPLATE
*.min.*
package-lock.json
bridge/
docs
examples/panel.html

28
.readthedocs.yml Normal file
View File

@@ -0,0 +1,28 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
tools:
python: miniconda3-4.7
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
conda:
environment: docs/environment.yml
# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
# - pdf
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: docs/requirements.txt

View File

@@ -1,4 +1,81 @@
# Contributing to PyScript
Please see our guide to contributing to PyScript
[in our documentation](https://docs.pyscript.net/latest/contributing/).
Thank you for wanting to contribute to the PyScript project!
## Table of contents
- [Contributing to PyScript](#contributing-to-pyscript)
- [Table of contents](#table-of-contents)
- [Code of Conduct](#code-of-conduct)
- [Contributing](#contributing)
- [Reporting bugs](#reporting-bugs)
- [Creating useful issues](#creating-useful-issues)
- [Reporting security issues](#reporting-security-issues)
- [Asking questions](#asking-questions)
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
- [Developing](#developing)
- [Rebasing changes](#rebasing-changes)
- [Building the docs](#building-the-docs)
- [Places to start](#places-to-start)
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
- [Submitting a change](#submitting-a-change)
- [License terms for contributions](#license-terms-for-contributions)
- [Becoming a maintainer](#becoming-a-maintainer)
- [Trademarks](#trademarks)
# Code of Conduct
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
# Contributing
## Reporting bugs
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
## Creating useful issues
- Use a clear and descriptive title.
- Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
- Describe the behavior you observed, and the behavior you had expected.
- Include screenshots if they help make the issue clear.
## Reporting security issues
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
## Asking questions
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
## Places to start
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions:
- **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
- **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
- **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
- **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
## Setting up your local environment and developing
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/development/setting-up-environment.html) will help you get started.
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/development/developing.html) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
## License terms for contributions
This Project welcomes contributions, suggestions, and feedback. All contributions, suggestions, and feedback you submitted are accepted under the [Apache 2.0](./LICENSE) license. You represent that if you do not own copyright in the code that you have the authority to submit it under the [Apache 2.0](./LICENSE) license. All feedback, suggestions, or contributions are not confidential.
## Becoming a maintainer
Contributors are invited to be maintainers of the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md). New maintainers are invited by a 3/4 vote of the existing maintainers.
## Trademarks
The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md).
---
Part of MVG-0.1-beta.
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).

View File

@@ -186,12 +186,10 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2022-present, PyScript Development Team
Originated at Anaconda, Inc. in 2022
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

View File

@@ -1,93 +0,0 @@
MIN_NODE_VER := 20
MIN_NPM_VER := 6
MIN_PY3_VER := 8
NODE_VER := $(shell node -v | cut -d. -f1 | sed 's/^v\(.*\)/\1/')
NPM_VER := $(shell npm -v | cut -d. -f1)
PY3_VER := $(shell python3 -c "import sys;t='{v[1]}'.format(v=list(sys.version_info[:2]));print(t)")
PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
all:
@echo "\nThere is no default Makefile target right now. Try:\n"
@echo "make setup - check your environment and install the dependencies."
@echo "make update - update dependencies."
@echo "make clean - clean up auto-generated assets."
@echo "make build - build PyScript."
@echo "make precommit-check - run the precommit checks (run eslint)."
@echo "make test - run all automated tests in playwright."
@echo "make fmt - format the code."
@echo "make fmt-check - check the code formatting.\n"
.PHONY: check-node
check-node:
@if [ $(NODE_VER) -lt $(MIN_NODE_VER) ]; then \
echo "\033[0;31mBuild requires Node $(MIN_NODE_VER).x or higher: $(NODE_VER) detected.\033[0m"; \
false; \
fi
.PHONY: check-npm
check-npm:
@if [ $(NPM_VER) -lt $(MIN_NPM_VER) ]; then \
echo "\033[0;31mBuild requires Node $(MIN_NPM_VER).x or higher: $(NPM_VER) detected.\033[0m"; \
false; \
fi
.PHONY: check-python
check-python:
@if [ $(PY_OK) -eq 0 ]; then \
echo "\033[0;31mRequires Python 3.$(MIN_PY3_VER).x or higher: 3.$(PY3_VER) detected.\033[0m"; \
false; \
fi
# Check the environment, install the dependencies.
setup: check-node check-npm check-python
cd core && npm ci && cd ..
ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
false
else
python -m pip install -r requirements.txt
endif
# Clean up generated assets.
clean:
find . -name \*.py[cod] -delete
rm -rf $(env) *.egg-info
rm -rf .pytest_cache .coverage coverage.xml
# Build PyScript.
build: precommit-check
cd core && npx playwright install chromium && npm run build
# Update the dependencies.
update:
python -m pip install -r requirements.txt --upgrade
# Run the precommit checks (run eslint).
precommit-check:
pre-commit run --all-files
# Run all automated tests in playwright.
test:
cd core && npm run test:integration
# Serve the repository with the correct headers.
serve:
npx mini-coi .
# Format the code.
fmt: fmt-py
@echo "Format completed"
# Check the code formatting.
fmt-check: fmt-py-check
@echo "Format check completed"
# Format Python code.
fmt-py:
black -l 88 --skip-string-normalization .
# Check the format of Python code.
fmt-py-check:
black -l 88 --check .
.PHONY: $(MAKECMDGOALS)

112
README.md
View File

@@ -1,94 +1,56 @@
# PyScript
## PyScript is an open source platform for Python in the browser.
## What is PyScript
Using PyScript is as simple as:
### Summary
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [WASM](https://webassembly.org/), and modern web technologies.
To get started see the [getting started tutorial](docs/tutorials/getting-started.md).
For examples see [here](examples).
### Longer Version
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
## Try PyScript
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page with:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>PyScript!</title>
<link
rel="stylesheet"
href="https://pyscript.net/releases/2025.11.2/core.css"
/>
<script
type="module"
src="https://pyscript.net/releases/2025.11.2/core.js"
></script>
</head>
<body>
<!-- type mpy (MicroPython) or py (Pyodide) to run some Python -->
<script type="mpy" terminal>
print("Hello, world!")
</script>
</body>
</html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
```
PyScript enables the creation of rich Python applications in the browser using
[Pyodide](https://pyodide.org/en/stable/) (a version of
[CPython](https://python.org/)), [MicroPython](https://micropython.org/),
[WASM](https://webassembly.org/), and modern web technologies. It means Python
now runs anywhere a browser runs: desktop, laptop, mobile, tablet, or any other
browser enabled device.
You can then use PyScript components in your html page. PyScript currently implements the following elements:
To start building, read the
[Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
- `<py-script>`: can be used to define python code that is executable within the web page. The element itself is not rendered to the page and is only used to add logic
- `<py-repl>`: creates a REPL component that is rendered to the page as a code editor and allows users to write executable code
For example applications, see [here](https://pyscript.com/@examples).
Check out the [the examples directory](examples) folder for more examples on how to use it, all you need to do is open them in Chrome.
Other useful resources:
## How to Contribute
- Our [Home Page](https://pyscript.net/) as an open source project.
- The [official technical docs](https://docs.pyscript.net/).
- A [YouTube channel](https://www.youtube.com/@PyScriptTV) with helpful videos
and community content.
- A free-to-use [online IDE](https://pyscript.com/) for trying PyScript.
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in
touch .
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
Every Tuesday at 15:30 UTC there is the _PyScript Community Call_ on zoom,
where we can talk about PyScript development in the open. Most of the
maintainers regularly participate in the call, and everybody is welcome to
join. This meeting is recorded and uploaded to our YouTube channel.
Check out the [developing process](https://docs.pyscript.net/latest/development/developing.html) documentation for more information on how to setup your development environment.
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: the focus
of this call is to share fun projects, goofy hacks or clever uses of PyScript.
It's a supportive, energetic and entertaining meeting. This meeting is also
recorded and uploaded to our YouTube channel.
## Resources
For more details on how to join the calls and up to date schedule, consult the
official calendar:
- [Official docs](https://docs.pyscript.net)
- [Discussion board](https://community.anaconda.cloud/c/tech-topics/pyscript)
- [Home Page](https://pyscript.net/)
- [Blog Post](https://engineering.anaconda.com/2022/04/welcome-pyscript.html)
- [Discord Channel](https://discord.gg/BYB2kvyFwm)
- [Google calendar](https://calendar.google.com/calendar/u/0/embed?src=d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0@group.calendar.google.com&ctz=UTC) in UTC time;
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
## Notes
## Contribute
For technical details of the code, please see the [README](core/README.md) in
the `core` directory.
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
to learn about our development process, reporting bugs and improvements,
creating issues and asking questions.
Check out the [development process](https://docs.pyscript.net/latest/developers/)
documentation for more information on how to setup your development environment.
- This is an extremely experimental project, so expect things to break!
- PyScript has been only tested on Chrome at the moment.
## Governance
The [PyScript organization governance](https://github.com/pyscript/governance)
is documented in a separate repository.
## Supporters
PyScript is an independent open source project.
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
core contributors are currently employed by Anaconda to work on PyScript. We
would like to acknowledge and celebrate Anaconda's continued support of this
project. Thank you [Anaconda Inc](https://anaconda.com/)!
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.

19
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,19 @@
# Troubleshooting
This page is meant for troubleshooting common problems with PyScript.
## Table of contents:
- [Make Setup](#make-setup)
## Make setup
A lot of problems related to `make setup` are related to node and npm being outdated. Once npm and node are updated, `make setup` should work. You can follow the steps on the [npm documentation](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) to update npm (the update command for Linux should work for Mac as well). Once npm has been updated you can continue to the instructions to update node below.
To update Node run the following commands in order (Most likely you'll be prompted for your user password, this is normal):
```
sudo npm cache clean -f
sudo npm install -g n
sudo n stable
```

View File

@@ -1,59 +0,0 @@
# @pyscript/bridge
Import Python utilities directly in JS
```js
// main thread
const { ffi: { func_a, func_b } } = await import('./test.js');
// test.js
import bridge from 'https://esm.run/@pyscript/bridge';
export const ffi = bridge(import.meta.url, { type: 'mpy', worker: false });
// test.py
def func_a(value):
print(f"hello {value}")
def func_b():
import sys
return sys.version
```
### Options
* **pyscript**: the release version to automatically import if not already available on the page. If no version is provided the *developers' channel* version will be used instead (for developers' purposes only).
* **type**: `py` by default to bootstrap *Pyodide*.
* **worker**: `true` by default to bootstrap in a *Web Worker*.
* **config**: either a *string* or a PyScript compatible config *JS literal* to make it possible to bootstrap files and whatnot. If specified, the `worker` becomes implicitly `true` to avoid multiple configs conflicting on the main thread.
* **env**: to share the same environment across multiple modules loaded at different times.
## Tests
Run `npx mini-coi .` within this folder to then reach out `http://localhost:8080/test/` that will show:
```
PyScript Bridge
------------------
no config
```
The [test.js](./test/test.js) files uses the following defaults:
* `pyscript` as `"2025.8.1"`
* `type` as `"mpy"`
* `worker` as `false`
* `config` as `undefined`
* `env` as `undefined`
To test any variant use query string parameters so that `?type=py` will use `py` instead, `worker` will use a worker and `config` will use a basic *config* that brings in another file from the same folder which exposes the version.
To recap: `http://localhost:8080/test/?type=py&worker&config` will show this instead:
```
PyScript Bridge
------------------
3.12.7 (main, May 15 2025, 18:47:24) ...
```
Please note when a *config* is used, the `worker` attribute is always `true`.

View File

@@ -1,163 +0,0 @@
/*! (c) PyScript Development Team */
const { stringify } = JSON;
const { assign, create, entries } = Object;
const el = (name, props) => assign(document.createElement(name), props);
/**
* Transform a list of keys into a Python dictionary.
* `['a', 'b']` => `{ "a": a, "b": b }`
* @param {Iterable<string>} keys
* @returns {string}
*/
const dictionary = keys => {
const fields = [];
for (const key of keys)
fields.push(`${stringify(key)}: ${key}`);
return `{ ${fields.join(',')} }`;
};
/**
* Resolve properly config files relative URLs.
* @param {string|Object} config - The configuration to normalize.
* @param {string} base - The base URL to resolve relative URLs against.
* @returns {string} - The JSON serialized config.
*/
const normalize = async (config, base) => {
if (typeof config === 'string') {
base = config;
config = await fetch(config).then(res => res.json());
}
if (typeof config.files === 'object') {
const files = {};
for (const [key, value] of entries(config.files)) {
files[key.startsWith('{') ? key : new URL(key, base)] = value;
}
config.files = files;
}
return stringify(config);
};
// this logic is based on a 3 levels cache ...
const cache = new Map;
/**
* Return a bridge to a Python module via a `.js` file that has a `.py` alter ego.
* @param {string} url - The URL of the JS module that has a Python counterpart.
* @param {Object} options - The options for the bridge.
* @param {string} [options.type='py'] - The `py` or `mpy` interpreter type, `py` by default.
* @param {boolean} [options.worker=true] - Whether to use a worker, `true` by default.
* @param {string|Object} [options.config=null] - The configuration for the bridge, `null` by default.
* @param {string} [options.env=null] - The optional shared environment to use.
* @param {string} [options.serviceWorker=null] - The optional service worker to use as fallback.
* @returns {Object} - The bridge to the Python module.
*/
export default (url, {
type = 'py',
worker = true,
config = null,
env = null,
serviceWorker = null,
pyscript = null,
} = {}) => {
const { protocol, host, pathname } = new URL(url);
const py = pathname.replace(/\.m?js(?:\/\+\w+)?$/, '.py');
const file = `${protocol}//${host}${py}`;
// the first cache is about the desired file in the wild ...
if (!cache.has(file)) {
// the second cache is about all fields one needs to access out there
const exports = new Map;
let python;
cache.set(file, new Proxy(create(null), {
get(_, field) {
if (!exports.has(field)) {
// create an async callback once and always return the same later on
exports.set(field, async (...args) => {
// the third cache is about reaching lazily the code only once
// augmenting its content with exports once and drop it on done
if (!python) {
// do not await or multiple calls will fetch multiple times
// just assign the fetch `Promise` once and return it
python = fetch(file).then(async response => {
const code = await response.text();
// create a unique identifier for the Python context
const identifier = pathname.replace(/[^a-zA-Z0-9_]/g, '');
const name = `__pyscript_${identifier}${Date.now()}`;
// create a Python dictionary with all accessed fields
const detail = `{"detail":${dictionary(exports.keys())}}`;
// create the arguments for the `dispatchEvent` call
const eventArgs = `${stringify(name)},${name}to_ts(${detail})`;
// bootstrap the script element type and its attributes
const script = el('script', { type, textContent: [
'\n', code, '\n',
// this is to avoid local scope name clashing
`from pyscript import window as ${name}`,
`from pyscript.ffi import to_js as ${name}to_ts`,
`${name}.dispatchEvent(${name}.CustomEvent.new(${eventArgs}))`,
// remove these references even if non-clashing to keep
// the local scope clean from undesired entries
`del ${name}`,
`del ${name}to_ts`,
].join('\n') });
// if config is provided it needs to be a worker to avoid
// conflicting with main config on the main thread (just like always)
script.toggleAttribute('worker', !!config || !!worker);
if (config) {
const attribute = await normalize(config, file);
script.setAttribute('config', attribute);
}
if (env) script.setAttribute('env', env);
if (serviceWorker) script.setAttribute('service-worker', serviceWorker);
// let PyScript resolve and execute this script
document.body.appendChild(script);
// intercept once the unique event identifier with all exports
globalThis.addEventListener(
name,
event => {
resolve(event.detail);
script.remove();
},
{ once: true }
);
// return a promise that will resolve only once the event
// has been emitted and the interpreter evaluated the code
const { promise, resolve } = Promise.withResolvers();
if (!(Symbol.for('@pyscript/core') in globalThis)) {
// bring in PyScript if not available already
const cdn = pyscript ?
`https://pyscript.net/releases/${pyscript}` :
// ⚠️ fallback to developers' channel !!!
'https://cdn.jsdelivr.net/npm/@pyscript/core/dist'
;
document.head.appendChild(
el('link', { rel: 'stylesheet', href: `${cdn}/core.css` }),
);
try { await import(`${cdn}/core.js`) }
catch {}
}
return promise;
});
}
// return the `Promise` that will after invoke the exported field
return python.then(foreign => foreign[field](...args));
});
}
// return the lazily to be resolved once callback to invoke
return exports.get(field);
}
}));
}
return cache.get(file);
};

View File

@@ -1,31 +0,0 @@
{
"name": "@pyscript/bridge",
"version": "0.2.2",
"description": "A JS based way to use PyScript modules",
"type": "module",
"module": "./index.js",
"unpkg": "./index.js",
"jsdelivr": "./jsdelivr.js",
"browser": "./index.js",
"main": "./index.js",
"keywords": [
"PyScript",
"JS",
"Python",
"bridge"
],
"files": [
"index.js",
"README.md"
],
"author": "Anaconda Inc.",
"license": "APACHE-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/pyscript/pyscript.git"
},
"bugs": {
"url": "https://github.com/pyscript/pyscript/issues"
},
"homepage": "https://github.com/pyscript/pyscript#readme"
}

View File

@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PyScript Bridge</title>
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
<!-- for local testing purpose only-->
<script type="importmap">{"imports":{"https://esm.run/@pyscript/bridge":"../index.js"}}</script>
<script type="module">
const { ffi: { test_func, test_other, version } } = await import('./test.js');
console.time("⏱️ first invoke");
const result = await test_func("PyScript Bridge");
console.timeEnd("⏱️ first invoke");
document.body.append(
Object.assign(
document.createElement("h3"),
{ textContent: result },
),
document.createElement("hr"),
await version(),
);
console.time("⏱️ other invokes");
await test_other("🐍");
console.timeEnd("⏱️ other invokes");
</script>
</head>
</html>

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PyScript Bridge</title>
<script type="importmap">
{
"imports": {
"https://esm.run/@pyscript/bridge": "https://esm.run/@pyscript/bridge@latest",
"https://esm.run/@pyscript/bridge/test/test.js": "https://esm.run/@pyscript/bridge@latest/test/test.js"
}
}
</script>
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
<script type="module">
const cdn_test = 'https://esm.run/@pyscript/bridge/test/test.js';
const { ffi: { test_func, test_other, version } } = await import(cdn_test);
console.time("⏱️ first invoke");
const result = await test_func("PyScript Bridge");
console.timeEnd("⏱️ first invoke");
document.body.append(
Object.assign(
document.createElement("h3"),
{ textContent: result },
),
document.createElement("hr"),
await version(),
);
console.time("⏱️ other invokes");
await test_other("🐍");
console.timeEnd("⏱️ other invokes");
</script>
</head>
</html>

View File

@@ -1,5 +0,0 @@
import sys
def version():
return sys.version

View File

@@ -1,18 +0,0 @@
import bridge from "https://esm.run/@pyscript/bridge";
// for local testing purpose only
const { searchParams } = new URL(location.href);
// the named (or default) export for test.py
export const ffi = bridge(import.meta.url, {
pyscript: "2025.8.1",
env: searchParams.get("env"),
type: searchParams.get("type") || "mpy",
worker: searchParams.has("worker"),
config: searchParams.has("config") ?
({
files: {
"./sys_version.py": "./sys_version.py",
},
}) : undefined,
});

View File

@@ -1,22 +0,0 @@
from pyscript import config, RUNNING_IN_WORKER
type = config["type"]
print(f"{type}-script", RUNNING_IN_WORKER and "worker" or "main")
def test_func(message):
print("Python", message)
return message
def test_other(message):
print("Python", message)
return message
def version():
try:
from sys_version import version
except ImportError:
version = lambda: "no config"
return version()

View File

@@ -1,205 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2022-present, PyScript Development Team
Originated at Anaconda, Inc. in 2022
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,168 +0,0 @@
# @pyscript/core
PyScript brings two Python interpreters to the browser:
- [MicroPython](https://micropython.org/) - a lean and efficient implementation
of the Python 3 programming language that includes a small subset of the
Python standard library and is optimised to run on microcontrollers and in
constrained environments (like the browser).
- [Pyodide](https://pyodide.org)) - a port of all CPython to WebAssembly.
These interpreters are compiled to [WebAssembly](https://webassembly.org/)
(shortened to WASM). The browser provides a secure WASM computing sandbox. Both
interpreters are compiled to web assembly with
[Emscripten](https://emscripten.org/). PyScript core maintainers work closely
with the core maintainers of both MicroPython and Pyodide (and CPython). We
work hard to ensure PyScript works efficiently in browsers on all platforms:
desktop, mobile, or elsewhere.
Our technical documentation for using this project can be
[found here](https://docs.pyscript.net/).
PyScript sits on two further projects (both written in JavaScript):
1. [polyscript](https://github.com/pyscript/polyscript/#readme) - used to
bootstrap WASM compiled interpreters in a browser.
2. [coincident](https://github.com/WebReflection/coincident) - used to simplify
worker based tasks.
PyScript itself is mostly written in JavaScript. The test suite for JavaScript
is in two parts: automated tests run in [playwright](https://playwright.dev/),
and manual tests you have to run in a browser and check yourself. PyScript also
has a plugin system so third parties can extend its capabilities with
JavaScript. Our built-in core plugins can be found in the `src/plugins`
directory. We describe how to write third party plugins in our
[developer documentation](https://docs.pyscript.net/latest/user-guide/plugins/).
We provide a `pyscript` namespace containing Python modules for common browser
based APIs and features (i.e. you can `import pyscript` in Python code running
inside PyScript, to access these features). The Python code for the `pyscript`
namespace is in `src/stdlib/pyscript` with the associated test suite in
`tests/python`. The tests use the browser friendly
[uPyTest](https://github.com/ntoll/upytest) test framework for checking Python
code running _within_ PyScript. All the Python tests are run in each each
available interpreter in both the main thread and a web worker (i.e. the
test suite is run four times, accounting for each combination of interpreter
and main/worker context).
When you create a local build all the automated tests (JavaScript and Python)
are run.
## Developer Guide
Full instructions for setting up a working development environment, how to
build PyScript and how to test it can be
[found in our official docs](https://docs.pyscript.net/latest/developers/).
The short version is:
- Ensure you have Python, node and npm installed.
- Create a Python virtual environment.
- In the root of this repository `make setup`.
- `make build` to build PyScript.
- As dependencies change over time, `make update` to keep in sync.
To start using the locally built version of PyScript, you'll need an HTML
page something like this (note the relative paths to assets in the `dist`
directory, in the `<head>` of the document):
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pure Python PyScript tests</title>
<link rel="stylesheet" href="../../dist/core.css" />
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy" src="./main.py" config="./conf.toml"></script>
</body>
</html>
```
Once set up, you should be able to run the most common activities via the
`make` command:
```
$ make
There is no default Makefile target right now. Try:
make setup - check your environment and install the dependencies.
make update - update dependencies.
make clean - clean up auto-generated assets.
make build - build PyScript.
make precommit-check - run the precommit checks (run eslint).
make test - run all automated tests in playwright.
make fmt - format the code.
make fmt-check - check the code formatting.
```
## Artifacts
There are two main artifacts in this project:
- **stdlib** and its content: `src/stdlib/pyscript.js` exposes, as a
JavaScript object literal, all the _Python_ content within the folder
(recursively).
- **plugins** and its content: `src/plugins.js` exposes all available
_dynamic imports_, and is able to instrument the bundler to create files
apart from the `_dist/_` folder, so that by default _core_ remains as small
as possible.
Accordingly, whenever a file contains this warning at its first line, **please
do not change such file directly before submitting a merge request**, as that
file will be overwritten at the next `npm run build` command, either here or
in _CI_:
```js
// ⚠️ This file is an artifact: DO NOT MODIFY
```
## Plugins
While community or third party plugins don't need to be part of this repository
and can be added just importing `@pyscript/core` as module, there are a few
plugins that we would like to make available by default and these are
considered _core plugins_.
To add a _core plugin_ to this project define the plugin entry-point and name
in the `src/plugins` folder (see the `error.js` example) and create, if
necessary, a folder with the same name where extra files or dependencies can be
added.
The _build_ command will include plugins by name as artifacts so that the
bundler can create ad-hoc files within the `dist/` folder.
## Python
The `pyscript` package available in _Python_ lives in the folder
`src/stdlib/pyscript/`.
All _Python_ files will be embedded automatically whenever `npm run build`
happens and reflected into the `src/stdlib/pyscript.js` file.
Its _core_ responsibility is to ensure those files will be available through
the filesystem in either the _main_ thread, or any _worker_.
## Release
To cut a new release of PyScript simply
[add a new release](https://github.com/pyscript/pyscript/releases) while
remembering to write a comprehensive changelog. A
[GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
will kick in and ensure the release is described and deployed to a URL with the
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
our [CalVer](https://calver.org/) versioning scheme).
Then, the following three separate repositories need updating:
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
file in the root of the directory and then `node version-update.js`.
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
referenced in `index.html` is the latest version.
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
Anaconda) to let the PSDC team know there's a new version, so they can update
their project templates.

View File

@@ -1,31 +0,0 @@
let queue = Promise.resolve();
const { exec } = require("node:child_process");
const build = (fileName) => {
if (fileName) console.log(fileName, "changed");
else console.log("building without optimizations");
queue = queue.then(
() =>
new Promise((resolve) => {
exec(
"npm run build:stdlib && npm run build:plugins && npm run build:core",
{ cwd: __dirname, env: { ...process.env, NO_MIN: true } },
(error) => {
if (error) console.error(error);
else console.log(fileName || "", "build completed");
resolve();
},
);
}),
);
};
const options = {
ignored: /\/(?:toml|plugins|pyscript)\.[mc]?js$/,
persistent: true,
};
require("chokidar").watch("./src", options).on("change", build);
build();

View File

@@ -1,22 +0,0 @@
import globals from "globals";
import js from "@eslint/js";
export default [
js.configs.recommended,
{
ignores: ["**/3rd-party/"],
},
{
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.browser,
...globals.es2021,
},
},
rules: {
"no-implicit-globals": ["error"],
},
},
];

View File

@@ -1 +0,0 @@
export * from "./dist/core.js";

View File

@@ -1,2 +0,0 @@
// @see https://github.com/jsdelivr/jsdelivr/issues/18528
export * from "./core/dist/core.js";

4079
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +0,0 @@
{
"name": "@pyscript/core",
"version": "0.7.11",
"type": "module",
"description": "PyScript",
"module": "./index.js",
"unpkg": "./index.js",
"jsdelivr": "./jsdelivr.js",
"browser": "./index.js",
"main": "./index.js",
"engines": {
"node": ">=20"
},
"files": [
"./dist/",
"./src/",
"./types/",
"./index.js",
"./jsdelivr.js",
"LICENSE",
"README.md"
],
"exports": {
".": {
"types": "./types/core.d.ts",
"import": "./src/core.js"
},
"./js": {
"types": "./types/core.d.ts",
"import": "./dist/core.js"
},
"./css": {
"import": "./dist/core.css"
},
"./storage": {
"import": "./dist/storage.js"
},
"./service-worker": {
"import": "./dist/service-worker.js"
},
"./package.json": "./package.json"
},
"scripts": {
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run test:integration; fi",
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:flatted": "node rollup/flatted.cjs",
"build:plugins": "node rollup/plugins.cjs",
"build:stdlib": "node rollup/stdlib.cjs",
"build:3rd-party": "node rollup/3rd-party.cjs",
"build:offline": "node rollup/offline.cjs | bash",
"build:tests-index": "node rollup/build_test_index.cjs",
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
"dev": "node dev.cjs",
"release": "npm run build && npm run zip",
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
"ts": "rm -rf types && tsc -p .",
"zip": "zip -r dist.zip ./dist"
},
"keywords": [
"pyscript",
"core"
],
"author": "Anaconda Inc.",
"license": "APACHE-2.0",
"dependencies": {
"@ungap/with-resolvers": "^0.1.0",
"@webreflection/idb-map": "^0.3.2",
"@webreflection/utils": "^0.1.1",
"add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6",
"polyscript": "^0.20.0",
"sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7"
},
"devDependencies": {
"@codemirror/commands": "^6.10.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.11.3",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.8",
"@playwright/test": "^1.56.1",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.4",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"bun": "^1.3.3",
"chokidar": "^4.0.3",
"codedent": "^0.1.2",
"codemirror": "^6.0.2",
"eslint": "^9.39.1",
"flatted": "^3.3.3",
"rollup": "^4.53.3",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3",
"string-width": "^8.1.0",
"typescript": "^5.9.3",
"xterm-readline": "^1.1.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pyscript/pyscript.git"
},
"bugs": {
"url": "https://github.com/pyscript/pyscript/issues"
},
"homepage": "https://github.com/pyscript/pyscript#readme"
}

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
# This script assumes the following folder structure:
# ./pyscript - it must be a GitHub clone/fork
# ./polyscript - it must be a GitHub clone/fork
#
# Running from ./pyscript/core via:
#
# cd ./pyscript/core
# bash ./pyodide.sh
#
# will print a JSON compatible string like:
#
# {
# "2024.10.1": "0.26.2",
# ...
# "2025.11.1": "0.29.0",
# "": null
# }
#
# Each key represents the PyScript release and each
# value represents the Pyodide version used by that PyScript release.
#
# The last empty key with `null` value is used just to close the JSON object.
# One could remove manually that entry as long as there are no dangling commas.
#
current_pyscript=$(git branch | grep \\* | cut -d ' ' -f2)
echo "{"
for release in $(git tag --list --sort=version:refname); do
git checkout ${release} > /dev/null 2>&1
if test -e "package.json"; then
polyscript=$(cat package.json | jq -r '.dependencies.polyscript')
tag="v${polyscript:1:${#polyscript}-1}"
cd ../../polyscript > /dev/null 2>&1
current_polyscript=$(git branch | grep \\* | cut -d ' ' -f2)
git checkout ${tag} > /dev/null 2>&1
if test -e "versions/pyodide"; then
echo " \"${release}\": \"$(cat versions/pyodide)\","
fi
git checkout ${current_polyscript} > /dev/null 2>&1
cd - > /dev/null 2>&1
fi
git checkout ${current_pyscript} > /dev/null 2>&1
done
echo " \"\": null"
echo "}"

View File

@@ -1,77 +0,0 @@
const { copyFileSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");
const CDN = "https://cdn.jsdelivr.net/npm";
const targets = join(__dirname, "..", "src", "3rd-party");
const node_modules = join(__dirname, "..", "node_modules");
const { devDependencies } = require(join(__dirname, "..", "package.json"));
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
const dropSourceMap = (str) =>
str.replace(/^\/.+? sourceMappingURL=\/.+$/m, "");
// Fetch a module via jsdelivr CDN `/+esm` orchestration
// then sanitize the resulting outcome to avoid importing
// anything via `/npm/...` through Rollup
const resolve = (name) => {
const cdn = `${CDN}/${name}@${v(name)}/+esm`;
console.debug("fetching", cdn);
return fetch(cdn)
.then((b) => b.text())
.then((text) =>
text.replace(
/("|')\/npm\/(.+)?\+esm\1/g,
// normalize `/npm/module@version/+esm` as
// just `module` so that rollup can do the rest
(_, quote, module) => {
const i = module.lastIndexOf("@");
return `${quote}${module.slice(0, i)}${quote}`;
},
),
);
};
// create a file rollup can then process and understand
const reBundle = (name) => Promise.resolve(`export * from "${name}";\n`);
// key/value pairs as:
// "3rd-party/file-name.js"
// string as content or
// Promise<string> as resolved content
const modules = {
// toml
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
// xterm
"xterm.js": resolve("@xterm/xterm"),
"xterm-readline.js": resolve("xterm-readline"),
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
b.text(),
),
"xterm_addon-web-links.js": fetch(
`${CDN}/@xterm/addon-web-links/+esm`,
).then((b) => b.text()),
"xterm.css": fetch(
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
).then((b) => b.text()),
// codemirror
"codemirror.js": reBundle("codemirror"),
"codemirror_state.js": reBundle("@codemirror/state"),
"codemirror_lang-python.js": reBundle("@codemirror/lang-python"),
"codemirror_language.js": reBundle("@codemirror/language"),
"codemirror_view.js": reBundle("@codemirror/view"),
"codemirror_commands.js": reBundle("@codemirror/commands"),
};
for (const [target, source] of Object.entries(modules)) {
if (typeof source === "string") copyFileSync(source, join(targets, target));
else {
source.then((text) =>
writeFileSync(join(targets, target), dropSourceMap(text)),
);
}
}

View File

@@ -1,73 +0,0 @@
const { join } = require("node:path");
const { lstatSync, readdirSync, writeFileSync } = require("node:fs");
// folders to not consider while crawling
const EXCLUDE_DIR = new Set(["ws"]);
const TEST_DIR = join(__dirname, "..", "tests");
const TEST_INDEX = join(TEST_DIR, "index.html");
const crawl = (path, tree = {}) => {
for (const file of readdirSync(path)) {
const current = join(path, file);
if (current === TEST_INDEX) continue;
if (lstatSync(current).isDirectory()) {
if (EXCLUDE_DIR.has(file)) continue;
const sub = {};
tree[file] = sub;
crawl(current, sub);
if (!Reflect.ownKeys(sub).length) {
delete tree[file];
}
} else if (file.endsWith(".html")) {
const name = file === "index.html" ? "." : file.slice(0, -5);
tree[name] = current.replace(TEST_DIR, "");
}
}
return tree;
};
const createList = (tree) => {
const ul = ["<ul>"];
for (const [key, value] of Object.entries(tree)) {
ul.push("<li>");
if (typeof value === "string") {
ul.push(`<a href=".${value}">${key}<small>.html</small></a>`);
} else {
if ("." in value) {
ul.push(`<strong><a href=".${value["."]}">${key}</a></strong>`);
delete value["."];
} else {
ul.push(`<strong><span>${key}</span></strong>`);
}
if (Reflect.ownKeys(value).length) ul.push(createList(value));
}
ul.push("</li>");
}
ul.push("</ul>");
return ul.join("");
};
writeFileSync(
TEST_INDEX,
`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript tests</title>
<style>
body { font-family: sans-serif; }
a {
display: block;
transition: opacity .3s;
}
a, span { opacity: .7; }
a:hover { opacity: 1; }
</style>
</head>
<body>${createList(crawl(TEST_DIR))}</body>
</html>
`,
);

View File

@@ -1,67 +0,0 @@
// This file generates /core.js minified version of the module, which is
// the default exported as npm entry.
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import terser from "@rollup/plugin-terser";
import postcss from "rollup-plugin-postcss";
const plugins = [];
export default [
{
input: "./src/core.js",
plugins: plugins.concat(
process.env.NO_MIN
? [nodeResolve(), commonjs()]
: [nodeResolve(), commonjs(), terser()],
),
output: {
esModule: true,
dir: "./dist",
sourcemap: true,
},
},
{
input: "./src/core.css",
plugins: [
postcss({
extract: true,
sourceMap: false,
minimize: !process.env.NO_MIN,
plugins: [],
}),
],
output: {
file: "./dist/core.css",
},
onwarn(warning, warn) {
if (warning.code === "FILE_NAME_CONFLICT") return;
warn(warning);
},
},
{
input: "./src/storage.js",
plugins: plugins.concat(
process.env.NO_MIN
? [nodeResolve(), commonjs()]
: [nodeResolve(), commonjs(), terser()],
),
output: {
esModule: true,
dir: "./dist",
sourcemap: true,
},
},
{
input: "./src/service-worker.js",
plugins: plugins.concat(
process.env.NO_MIN
? [nodeResolve(), commonjs()]
: [nodeResolve(), commonjs(), terser()],
),
output: {
file: "./dist/service-worker.js",
},
},
];

View File

@@ -1,17 +0,0 @@
const { writeFileSync, readFileSync } = require("node:fs");
const { join } = require("node:path");
const flatted = "# https://www.npmjs.com/package/flatted\n\n";
const source = join(
__dirname,
"..",
"node_modules",
"flatted",
"python",
"flatted.py",
);
const dest = join(__dirname, "..", "src", "stdlib", "pyscript", "flatted.py");
const clear = (str) => String(str).replace(/^#.*/gm, "").trimStart();
writeFileSync(dest, flatted + clear(readFileSync(source)));

View File

@@ -1,79 +0,0 @@
const { readFileSync, writeFileSync } = require("node:fs");
const { join, resolve } = require("node:path");
const versions = resolve(
__dirname,
"..",
"node_modules",
"polyscript",
"versions",
);
let pyodide = String(readFileSync(join(versions, "pyodide"), "utf8")).trim();
let micropython = String(
readFileSync(join(versions, "micropython"), "utf8"),
).trim();
writeFileSync(
join(process.cwd(), "offline.html"),
`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Offline</title>
<script src="./mini-coi-fd.js"></script>
<script type="module" src="./pyscript/core.js" offline></script>
<link rel="stylesheet" href="./pyscript/core.css">
</head>
<body>
<script type="mpy">
from pyscript import document
document.body.append("MicroPython Offline", document.createElement("hr"))
</script>
<script type="py" worker>
from pyscript import document
document.body.append("Pyodide Offline")
</script>
</body>
</html>
`,
"utf8",
);
let bash = `#!/usr/bin/env bash
rm -rf dist/offline
mkdir -p dist/offline/node_modules
echo '{"dependencies":{"pyodide":"${pyodide}","@micropython/micropython-webassembly-pyscript":"${micropython}"}}' > dist/offline/package.json
cd dist/offline
curl -sLO https://raw.githubusercontent.com/WebReflection/mini-coi/refs/heads/main/mini-coi-fd.js
npm i
cd -
mkdir -p dist/offline/pyscript/pyodide
cd dist/offline/pyscript/pyodide
cp ../../node_modules/pyodide/pyodide* ./
cp ../../node_modules/pyodide/python_stdlib.zip ./
cd -
mkdir -p dist/offline/pyscript/micropython
cd dist/offline/pyscript/micropython
cp ../../node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./
cd -
rm -rf dist/offline/node_modules
rm -rf dist/offline/*.json
mv offline.html dist/offline/index.html
cp dist/*.* dist/offline/pyscript/
rm -f dist/offline/pyscript/offline.zip
cd dist
zip -r offline.zip offline
rm -rf offline
cd -
`;
console.log(bash);

View File

@@ -1,33 +0,0 @@
const { readdirSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");
const plugins = [""];
for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
if (/\.js$/.test(file)) {
const name = file.slice(0, -3);
const key = /^[a-zA-Z0-9$_]+$/.test(name)
? name
: `[${JSON.stringify(name)}]`;
const value = JSON.stringify(`./plugins/${file}`);
plugins.push(
// this comment is needed to avoid bundlers eagerly embedding lazy
// dependencies, causing all sort of issues once in production
// ⚠️ THIS HAS TO BE LIKE THIS or prettier changes it every single time
` ${key}: () =>
import(
/* webpackIgnore: true */
${value}
),`,
);
}
}
plugins.push("");
writeFileSync(
join(__dirname, "..", "src", "plugins.js"),
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default {${plugins.join(
"\n",
)}};\n`,
);

View File

@@ -1,66 +0,0 @@
const {
readdirSync,
readFileSync,
statSync,
writeFileSync,
} = require("node:fs");
const { spawnSync } = require("node:child_process");
const { join } = require("node:path");
const dedent = require("codedent");
const crawl = (path, json) => {
for (const file of readdirSync(path)) {
const full = join(path, file);
if (/\.py$/.test(file)) {
if (process.env.NO_MIN) json[file] = readFileSync(full).toString();
else {
try {
const {
output: [error, result],
} = spawnSync("pyminify", [
"--remove-literal-statements",
full,
]);
if (error) {
console.error(error);
process.exit(1);
}
json[file] = result.toString();
} catch (error) {
console.error(error);
console.log(
dedent(`
\x1b[1m⚠ is your env activated?\x1b[0m
\x1b[2mYou need a Python env to run \x1b[0mpyminify\x1b[2m.\x1b[0m
\x1b[2mTo do so, you can try the following:\x1b[0m
python -m venv env
source env/bin/activate
pip install --upgrade pip
pip install --ignore-requires-python python-minifier
pip install setuptools
\x1b[2mand you can then try \x1b[0mnpm run build\x1b[2m again.\x1b[0m
`),
);
process.exit(1);
}
}
} else if (statSync(full).isDirectory() && !file.endsWith("_"))
crawl(full, (json[file] = {}));
}
};
const json = {};
crawl(join(__dirname, "..", "src", "stdlib"), json);
writeFileSync(
join(__dirname, "..", "src", "stdlib", "pyscript.js"),
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default ${JSON.stringify(
json,
null,
" ",
)};\n`,
);

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Jak Wings
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,25 +0,0 @@
Copyright 2021 Erik Bremen
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,19 +0,0 @@
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,19 +0,0 @@
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,11 +0,0 @@
# PyScript 3rd Party
This folder contains artifacts created via [3rd-party.cjs](../../rollup/3rd-party.cjs).
As we would like to offer a way to run PyScript offline, and we already offer a `dist` folder with all the necessary scripts, we have created a foreign dependencies resolver that allow to lazy-load CDN dependencies out of the box.
Please **note** these dependencies are **not interpreters**, because interpreters have their own mechanism, folders structure, WASM files, and whatnot, to work locally, but at least XTerm or the TOML parser, among other lazy dependencies, should be available within the dist folder.
## Licenses
All licenses provided by 3rd-party authors can be found in [3rd-party-licenses](../3rd-party-licenses/) folder.

View File

@@ -1 +0,0 @@
export * from "codemirror";

View File

@@ -1 +0,0 @@
export * from "@codemirror/commands";

View File

@@ -1 +0,0 @@
export * from "@codemirror/lang-python";

View File

@@ -1 +0,0 @@
export * from "@codemirror/language";

View File

@@ -1 +0,0 @@
export * from "@codemirror/state";

View File

@@ -1 +0,0 @@
export * from "@codemirror/view";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
/**
* Minified by jsDelivr using clean-css v5.3.3.
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
/**
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
var e,t,r={exports:{}};self;var s=r.exports=(e=t={},Object.defineProperty(e,"__esModule",{value:!0}),e.FitAddon=void 0,e.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(s.getPropertyValue("height")),o=Math.max(0,parseInt(s.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=i-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=o-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}},t),i=r.exports.FitAddon,o=r.exports.__esModule;export{i as FitAddon,o as __esModule,s as default};

View File

@@ -1,7 +0,0 @@
/**
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
var e={exports:{}};self;var t=e.exports=(()=>{var e={6:(e,t)=>{function r(e){try{const t=new URL(e),r=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(r.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,r,n={}){this._terminal=e,this._regex=t,this._handler=r,this._options=n}provideLinks(e,t){const r=n.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(r))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,r)=>{if(this._options.hover){const{range:n}=e;this._options.hover(t,r,n)}},e)))}};class n{static computeLink(e,t,o,s){const i=new RegExp(t.source,(t.flags||"")+"g"),[a,l]=n._getWindowedLineStrings(e-1,o),c=a.join("");let d;const p=[];for(;d=i.exec(c);){const e=d[0];if(!r(e))continue;const[t,i]=n._mapStrIdx(o,l,0,d.index),[a,c]=n._mapStrIdx(o,t,i,e.length);if(-1===t||-1===i||-1===a||-1===c)continue;const h={start:{x:i+1,y:t+1},end:{x:c,y:a+1}};p.push({range:h,text:e,activate:s})}return p}static _getWindowedLineStrings(e,t){let r,n=e,o=e,s=0,i="";const a=[];if(r=t.buffer.active.getLine(e)){const e=r.translateToString(!0);if(r.isWrapped&&" "!==e[0]){for(s=0;(r=t.buffer.active.getLine(--n))&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),r.isWrapped&&-1===i.indexOf(" ")););a.reverse()}for(a.push(e),s=0;(r=t.buffer.active.getLine(++o))&&r.isWrapped&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),-1===i.indexOf(" ")););}return[a,n]}static _mapStrIdx(e,t,r,n){const o=e.buffer.active,s=o.getNullCell();let i=r;for(;n;){const e=o.getLine(t);if(!e)return[-1,-1];for(let r=i;r<e.length;++r){e.getCell(r,s);const i=s.getChars();if(s.getWidth()&&(n-=i.length||1,r===e.length-1&&""===i)){const e=o.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,s),2===s.getWidth()&&(n+=1))}if(n<0)return[t,r]}t++,i=0}return[t,i]}}t.LinkComputer=n}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=r(6),o=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function s(e,t){const r=window.open();if(r){try{r.opener=null}catch{}r.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=s,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const r=this._options,n=r.urlRegex||o;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,n,this._handler,r))}dispose(){this._linkProvider?.dispose()}}})(),n})(),r=e.exports.WebLinksAddon,n=e.exports.__esModule;export{r as WebLinksAddon,n as __esModule,t as default};

View File

@@ -1,18 +0,0 @@
import withResolvers from "@webreflection/utils/with-resolvers";
import TYPES from "./types.js";
const waitForIt = [];
for (const [TYPE] of TYPES) {
const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`];
for (const element of document.querySelectorAll(selectors.join(","))) {
const { promise, resolve } = withResolvers();
waitForIt.push(promise);
element.addEventListener(`${TYPE}:done`, resolve, { once: true });
}
}
// wait for all the things then cleanup
Promise.all(waitForIt).then(() => {
dispatchEvent(new Event("py:all-done"));
});

View File

@@ -1,169 +0,0 @@
/**
* This file parses a generic <py-config> or config attribute
* to use as base config for all py-script elements, importing
* also a queue of plugins *before* the interpreter (if any) resolves.
*/
import { $$ } from "basic-devtools";
import TYPES from "./types.js";
import allPlugins from "./plugins.js";
import { robustFetch as fetch, getText } from "./fetch.js";
import { ErrorCode } from "./exceptions.js";
const { BAD_CONFIG, CONFLICTING_CODE } = ErrorCode;
const badURL = (url, expected = "") => {
let message = `(${BAD_CONFIG}): Invalid URL: ${url}`;
if (expected) message += `\nexpected ${expected} content`;
throw new Error(message);
};
/**
* Given a string, returns its trimmed content as text,
* fetching it from a file if the content is a URL.
* @param {string} config either JSON, TOML, or a file to fetch
* @param {string?} type the optional type to enforce
* @returns {{json: boolean, toml: boolean, text: string}}
*/
export const configDetails = async (config, type) => {
let text = config?.trim();
// we only support an object as root config
let url = "",
toml = false,
json = /^{/.test(text) && /}$/.test(text);
// handle files by extension (relaxing urls parts after)
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
const ext = RegExp.$1;
if (ext === "json" && type !== "toml") json = true;
else if (ext === "toml" && type !== "json") toml = true;
else badURL(text, type);
url = text;
text = (await fetch(url).then(getText)).trim();
}
return { json, toml: toml || (!json && !!text), text, url };
};
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
const relative_url = (url, base = location.href) => new URL(url, base).href;
const syntaxError = (type, url, { message }) => {
let str = `(${BAD_CONFIG}): Invalid ${type}`;
if (url) str += ` @ ${url}`;
return new SyntaxError(`${str}\n${message}`);
};
const configs = new Map();
for (const [TYPE] of TYPES) {
/** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
let plugins;
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
let parsed;
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
let error;
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
let configURL;
let config,
type,
parser,
pyElement,
pyConfigs = $$(`${TYPE}-config`),
attrConfigs = $$(
[
`script[type="${TYPE}"][config]:not([worker])`,
`${TYPE}-script[config]:not([worker])`,
].join(","),
);
// throw an error if there are multiple <py-config> or <mpy-config>
if (pyConfigs.length > 1) {
error = conflictError(`Too many ${TYPE}-config`);
} else {
// throw an error if there are <x-config> and config="x" attributes
if (pyConfigs.length && attrConfigs.length) {
error = conflictError(
`Ambiguous ${TYPE}-config VS config attribute`,
);
} else if (pyConfigs.length) {
[pyElement] = pyConfigs;
config = pyElement.getAttribute("src") || pyElement.textContent;
type = pyElement.getAttribute("type");
parser = pyElement.getAttribute("config-parser");
} else if (attrConfigs.length) {
[pyElement, ...attrConfigs] = attrConfigs;
config = pyElement.getAttribute("config");
parser = pyElement.getAttribute("config-parser");
// throw an error if dirrent scripts use different configs
if (
attrConfigs.some((el) => el.getAttribute("config") !== config)
) {
error = conflictError(
"Unable to use different configs on main",
);
}
}
}
// catch possible fetch errors
if (!error && config) {
try {
const { json, toml, text, url } = await configDetails(config, type);
if (url) configURL = relative_url(url);
config = text;
if (json || type === "json") {
try {
parsed = JSON.parse(text);
} catch (e) {
error = syntaxError("JSON", url, e);
}
} else if (toml || type === "toml") {
try {
const module = parser
? await import(parser)
: await import(
/* webpackIgnore: true */ "./3rd-party/toml.js"
);
const parse = module.parse || module.default;
parsed = parse(text);
} catch (e) {
error = syntaxError("TOML", url, e);
}
}
} catch (e) {
error = e;
}
}
// parse all plugins and optionally ignore only
// those flagged as "undesired" via `!` prefix
plugins = async () => {
const toBeAwaited = [];
for (const [key, value] of Object.entries(allPlugins)) {
if (error) {
if (key === "error") {
// show on page the config is broken, meaning that
// it was not possible to disable error plugin neither
// as that part wasn't correctly parsed anyway
value().then(({ notify }) => notify(error.message));
}
} else if (!parsed?.plugins?.includes(`!${key}`)) {
toBeAwaited.push(value().then(({ default: p }) => p));
} else if (key === "error") {
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
}
}
return await Promise.all(toBeAwaited);
};
if (Number.isSafeInteger(parsed?.experimental_ffi_timeout))
globalThis.reflected_ffi_timeout = parsed?.experimental_ffi_timeout;
configs.set(TYPE, { config: parsed, configURL, plugins, error });
}
export { configs, relative_url };

View File

@@ -1,61 +0,0 @@
py-script,
py-config,
mpy-script,
mpy-config {
display: none;
}
/* PyEditor */
.py-editor-box,
.mpy-editor-box {
padding: 0.5rem;
}
.py-editor-input,
.mpy-editor-input {
position: relative;
}
.py-editor-box::before,
.mpy-editor-box::before {
content: attr(data-env);
display: block;
font-size: x-small;
text-align: end;
}
.py-editor-output,
.mpy-editor-output {
white-space: pre;
}
.py-editor-run-button,
.mpy-editor-run-button {
position: absolute;
display: flex;
right: 0.5rem;
bottom: 0.5rem;
opacity: 0;
transition: opacity 0.25s;
z-index: 1;
padding: 0;
}
.py-editor-box:hover .py-editor-run-button,
.mpy-editor-box:hover .mpy-editor-run-button,
.py-editor-run-button:focus,
.py-editor-run-button.running,
.mpy-editor-run-button:focus,
.mpy-editor-run-button.running {
opacity: 1;
}
py-terminal span,
mpy-terminal span {
letter-spacing: 0 !important;
}
dialog.pyscript-fs {
border-radius: 8px;
border-width: 1px;
}
dialog.pyscript-fs > div {
display: flex;
justify-content: space-between;
}

View File

@@ -1,381 +0,0 @@
/*! (c) PyScript Development Team */
import "./zero-redirect.js";
import stickyModule from "sticky-module";
import withResolvers from "@webreflection/utils/with-resolvers";
import {
INVALID_CONTENT,
Hook,
XWorker,
assign,
dedent,
define,
defineProperty,
dispatch,
isSync,
queryTarget,
unescape,
whenDefined,
} from "polyscript/exports";
import "./all-done.js";
import TYPES from "./types.js";
import { configs, relative_url } from "./config.js";
import sync from "./sync.js";
import bootstrapNodeAndPlugins from "./plugins-helper.js";
import { ErrorCode } from "./exceptions.js";
import { robustFetch as fetch, getText } from "./fetch.js";
import {
hooks,
main,
worker,
codeFor,
createFunction,
inputFailure,
} from "./hooks.js";
import * as fs from "./fs.js";
import codemirror from "./plugins/codemirror.js";
export { codemirror };
import { stdlib, optional } from "./stdlib.js";
export { stdlib, optional, inputFailure };
export const donkey = (options) =>
import(/* webpackIgnore: true */ "./plugins/donkey.js").then((module) =>
module.default(options),
);
// generic helper to disambiguate between custom element and script
const isScript = ({ tagName }) => tagName === "SCRIPT";
// Used to create either Pyodide or MicroPython workers
// with the PyScript module available within the code
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
([TYPE, interpreter]) =>
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Promise<Worker & {sync: object}>}
*/
async function PyScriptWorker(file, options) {
await configs.get(TYPE).plugins;
const xworker = XWorker.call(
new Hook(null, hooked.get(TYPE)),
file,
{
...options,
type: interpreter,
},
);
assign(xworker.sync, sync);
return xworker.ready;
},
);
// avoid multiple initialization of the same library
const [
{
PyWorker: exportedPyWorker,
MPWorker: exportedMPWorker,
hooks: exportedHooks,
config: exportedConfig,
whenDefined: exportedWhenDefined,
},
alreadyLive,
] = stickyModule("@pyscript/core", {
PyWorker,
MPWorker,
hooks,
config: {},
whenDefined,
});
export {
TYPES,
relative_url,
exportedPyWorker as PyWorker,
exportedMPWorker as MPWorker,
exportedHooks as hooks,
exportedConfig as config,
exportedWhenDefined as whenDefined,
};
export const offline_interpreter = (config) =>
config?.interpreter && relative_url(config.interpreter);
const hooked = new Map();
for (const [TYPE, interpreter] of TYPES) {
// avoid any dance if the module already landed
if (alreadyLive) break;
const dispatchDone = (element, isAsync, result) => {
if (isAsync) result.then(() => dispatch(element, TYPE, "done"));
else dispatch(element, TYPE, "done");
};
let { config, configURL, plugins, error } = configs.get(TYPE);
// create a unique identifier when/if needed
let id = 0;
const getID = (prefix = TYPE) => `${prefix}-${id++}`;
/**
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
* It either throws an error if the 'src' can't be fetched or it returns a fallback
* content as source.
*/
const fetchSource = async (tag, io, asText) => {
if (tag.hasAttribute("src")) {
try {
return await fetch(tag.getAttribute("src")).then(getText);
} catch (error) {
io.stderr(error);
}
}
if (asText) return dedent(tag.textContent);
const code = dedent(unescape(tag.innerHTML));
console.warn(
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
code,
);
return code;
};
// register once any interpreter
let alreadyRegistered = false;
// allows lazy element features on code evaluation
let currentElement;
const registerModule = ({ XWorker, interpreter, io }) => {
// avoid multiple registration of the same interpreter
if (alreadyRegistered) return;
alreadyRegistered = true;
// automatically use the pyscript stderr (when/if defined)
// this defaults to console.error
function PyWorker(...args) {
const worker = XWorker(...args);
worker.onerror = ({ error }) => io.stderr(error);
return worker;
}
// enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", {
PyWorker,
fs,
interpreter,
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
get target() {
return isScript(currentElement)
? currentElement.target.id
: currentElement.id;
},
});
};
// define the module as both `<script type="py">` and `<py-script>`
// but only if the config didn't throw an error
if (!error) {
// ensure plugins are bootstrapped already before custom type definition
// NOTE: we cannot top-level await in here as plugins import other utilities
// from core.js itself so that custom definition should not be blocking.
plugins().then(() => {
// possible early errors sent by polyscript
const errors = new Map();
// specific main and worker hooks
const hooks = {
main: {
...codeFor(main, TYPE),
async onReady(wrap, element) {
registerModule(wrap);
// allows plugins to do whatever they want with the element
// before regular stuff happens in here
for (const callback of main("onReady"))
await callback(wrap, element);
// now that all possible plugins are configured,
// bail out if polyscript encountered an error
if (errors.has(element)) {
let { message } = errors.get(element);
errors.delete(element);
const clone = message === INVALID_CONTENT;
message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
message += element.cloneNode(clone).outerHTML;
wrap.io.stderr(message);
return;
}
if (isScript(element)) {
const isAsync = !isSync(element);
const target = element.getAttribute("target");
const show = target
? queryTarget(element, target)
: document.createElement("script-py");
if (!target) {
const { head, body } = document;
if (head.contains(element)) body.append(show);
else element.after(show);
}
if (!show.id) show.id = getID();
// allows the code to retrieve the target element via
// document.currentScript.target if needed
defineProperty(element, "target", { value: show });
// notify before the code runs
dispatch(element, TYPE, "ready");
dispatchDone(
element,
isAsync,
wrap[`run${isAsync ? "Async" : ""}`](
await fetchSource(element, wrap.io, true),
),
);
} else {
// resolve PyScriptElement to allow connectedCallback
element._wrap.resolve(wrap);
}
console.debug("[pyscript/main] PyScript Ready");
},
onWorker(_, xworker) {
assign(xworker.sync, sync);
for (const callback of main("onWorker"))
callback(_, xworker);
},
onBeforeRun(wrap, element) {
currentElement = element;
bootstrapNodeAndPlugins(
main,
wrap,
element,
"onBeforeRun",
);
},
onBeforeRunAsync(wrap, element) {
currentElement = element;
return bootstrapNodeAndPlugins(
main,
wrap,
element,
"onBeforeRunAsync",
);
},
onAfterRun(wrap, element) {
bootstrapNodeAndPlugins(
main,
wrap,
element,
"onAfterRun",
);
},
onAfterRunAsync(wrap, element) {
return bootstrapNodeAndPlugins(
main,
wrap,
element,
"onAfterRunAsync",
);
},
},
worker: {
...codeFor(worker, TYPE),
// these are lazy getters that returns a composition
// of the current hooks or undefined, if no hook is present
get onReady() {
return createFunction(this, "onReady", true);
},
get onBeforeRun() {
return createFunction(this, "onBeforeRun", false);
},
get onBeforeRunAsync() {
return createFunction(this, "onBeforeRunAsync", true);
},
get onAfterRun() {
return createFunction(this, "onAfterRun", false);
},
get onAfterRunAsync() {
return createFunction(this, "onAfterRunAsync", true);
},
},
};
hooked.set(TYPE, hooks);
// allow offline interpreter detection via [offline] attribute
let version = offline_interpreter(config);
if (!version) {
const css = "script[type='module'][offline]";
const s = document.querySelector(css)?.src;
if (s && import.meta.url.startsWith(s.replace(/\.js$/, ""))) {
version = `./pyscript/${interpreter}/${interpreter}.mjs`;
version = offline_interpreter({ interpreter: version });
}
}
define(TYPE, {
config,
configURL,
interpreter,
hooks,
version,
env: `${TYPE}-script`,
onerror(error, element) {
errors.set(element, error);
},
});
customElements.define(
`${TYPE}-script`,
class extends HTMLElement {
constructor() {
assign(super(), {
_wrap: withResolvers(),
srcCode: "",
executed: false,
});
}
get id() {
return super.id || (super.id = getID());
}
set id(value) {
super.id = value;
}
async connectedCallback() {
if (!this.executed) {
this.executed = true;
const isAsync = !isSync(this);
const { io, run, runAsync } = await this._wrap
.promise;
this.srcCode = await fetchSource(
this,
io,
!this.childElementCount,
);
this.replaceChildren();
this.style.display = "block";
dispatch(this, TYPE, "ready");
dispatchDone(
this,
isAsync,
(isAsync ? runAsync : run)(this.srcCode),
);
}
}
},
);
});
}
// export the used config without allowing leaks through it
exportedConfig[TYPE] = structuredClone(config);
}

View File

@@ -1,109 +0,0 @@
import { assign } from "polyscript/exports";
const CLOSEBUTTON =
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor' width='12px'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>";
/**
* These error codes are used to identify the type of error that occurred.
* @see https://pyscript.github.io/docs/latest/reference/exceptions.html?highlight=errors
*/
export const ErrorCode = {
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
CONFLICTING_CODE: "PY0409",
BAD_CONFIG: "PY1000",
MICROPIP_INSTALL_ERROR: "PY1001",
BAD_PLUGIN_FILE_EXTENSION: "PY2000",
NO_DEFAULT_EXPORT: "PY2001",
TOP_LEVEL_AWAIT: "PY9000",
// Currently these are created depending on error code received from fetching
FETCH_ERROR: "PY0001",
FETCH_NAME_ERROR: "PY0002",
FETCH_UNAUTHORIZED_ERROR: "PY0401",
FETCH_FORBIDDEN_ERROR: "PY0403",
FETCH_NOT_FOUND_ERROR: "PY0404",
FETCH_SERVER_ERROR: "PY0500",
FETCH_UNAVAILABLE_ERROR: "PY0503",
};
/**
* Keys of the ErrorCode object
* @typedef {keyof ErrorCode} ErrorCodes
* */
export class UserError extends Error {
/**
* @param {ErrorCodes} errorCode
* @param {string} message
* @param {string} messageType
* */
constructor(errorCode, message = "", messageType = "text") {
super(`(${errorCode}): ${message}`);
this.errorCode = errorCode;
this.messageType = messageType;
this.name = "UserError";
}
}
export class FetchError extends UserError {
/**
* @param {ErrorCodes} errorCode
* @param {string} message
* */
constructor(errorCode, message) {
super(errorCode, message);
this.name = "FetchError";
}
}
export class InstallError extends UserError {
/**
* @param {ErrorCodes} errorCode
* @param {string} message
* */
constructor(errorCode, message) {
super(errorCode, message);
this.name = "InstallError";
}
}
/**
* Internal function for creating alert banners on the page
* @param {string} message The message to be displayed to the user
* @param {string} level The alert level of the message. Can be any string; 'error' or 'warning' cause matching messages to be emitted to the console
* @param {string} [messageType="text"] If set to "html", the message content will be assigned to the banner's innerHTML directly, instead of its textContent
* @param {any} [logMessage=true] An additional flag for whether the message should be sent to the console log.
*/
export function _createAlertBanner(
message,
level,
messageType = "text",
logMessage = true,
) {
switch (`log-${level}-${logMessage}`) {
case "log-error-true":
console.error(message);
break;
case "log-warning-true":
console.warn(message);
break;
}
const content = messageType === "html" ? "innerHTML" : "textContent";
const banner = assign(document.createElement("div"), {
className: `alert-banner py-${level}`,
[content]: message,
});
if (level === "warning") {
const closeButton = assign(document.createElement("button"), {
id: "alert-close-button",
innerHTML: CLOSEBUTTON,
});
banner.appendChild(closeButton).addEventListener("click", () => {
banner.remove();
});
}
document.body.prepend(banner);
}

View File

@@ -1,82 +0,0 @@
import IDBMap from "@webreflection/idb-map";
import withResolvers from "@webreflection/utils/with-resolvers";
import { assign } from "polyscript/exports";
import { $$ } from "basic-devtools";
const stop = (event) => {
event.preventDefault();
event.stopImmediatePropagation();
};
// ⚠️ these two constants MUST be passed as `fs`
// within the worker onBeforeRunAsync hook!
export const NAMESPACE = "@pyscript.fs";
export const ERROR = "storage permissions not granted";
export const idb = new IDBMap(NAMESPACE);
/**
* Ask a user action via dialog and returns the directory handler once granted.
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {Promise<FileSystemDirectoryHandle>}
*/
export const getFileSystemDirectoryHandle = async (options) => {
if (!("showDirectoryPicker" in globalThis)) {
return Promise.reject(
new Error("showDirectoryPicker is not supported"),
);
}
const { promise, resolve, reject } = withResolvers();
const how = { id: "pyscript", mode: "readwrite", ...options };
if (options.hint) how.startIn = options.hint;
const transient = async () => {
try {
/* eslint-disable */
const handler = await showDirectoryPicker(how);
/* eslint-enable */
if ((await handler.requestPermission(how)) === "granted") {
resolve(handler);
return true;
}
} catch ({ message }) {
console.warn(message);
}
return false;
};
// in case the user decided to attach the event itself
// as opposite of relying our dialog walkthrough
if (navigator.userActivation?.isActive) {
if (!(await transient())) reject(new Error(ERROR));
} else {
const dialog = assign(document.createElement("dialog"), {
className: "pyscript-fs",
innerHTML: [
"<strong> Persistent FileSystem</strong><hr>",
"<p><small>PyScript would like to access a local folder.</small></p>",
"<div><button title='ok'>✅ Authorize</button>",
"<button title='cancel'>❌</button></div>",
].join(""),
});
const [ok, cancel] = $$("button", dialog);
ok.addEventListener("click", async (event) => {
stop(event);
if (await transient()) dialog.close();
});
cancel.addEventListener("click", async (event) => {
stop(event);
reject(new Error(ERROR));
dialog.close();
});
document.body.appendChild(dialog).showModal();
}
return promise;
};

View File

@@ -1,117 +0,0 @@
import { typedSet } from "type-checked-collections";
import { dedent } from "polyscript/exports";
import toJSONCallback from "to-json-callback";
import { stdlib, optional } from "./stdlib.js";
export const main = (name) => hooks.main[name];
export const worker = (name) => hooks.worker[name];
const code = (hooks, branch, key, lib) => {
hooks[key] = () => {
const arr = lib ? [lib] : [];
arr.push(...branch(key));
return arr.map(dedent).join("\n");
};
};
export const codeFor = (branch, type) => {
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
const hooks = {};
code(hooks, branch, `codeBeforeRun`, pylib);
code(hooks, branch, `codeBeforeRunAsync`, pylib);
code(hooks, branch, `codeAfterRun`);
code(hooks, branch, `codeAfterRunAsync`);
return hooks;
};
export const createFunction = (self, name) => {
const cbs = [...worker(name)];
if (cbs.length) {
const cb = toJSONCallback(
self[`_${name}`] ||
(name.endsWith("Async")
? async (wrap, xworker, ...cbs) => {
for (const cb of cbs) await cb(wrap, xworker);
}
: (wrap, xworker, ...cbs) => {
for (const cb of cbs) cb(wrap, xworker);
}),
);
const a = cbs.map(toJSONCallback).join(", ");
return Function(`return(w,x)=>(${cb})(w,x,...[${a}])`)();
}
};
const SetFunction = typedSet({ typeof: "function" });
const SetString = typedSet({ typeof: "string" });
export const inputFailure = `
import builtins
def input(prompt=""):
raise Exception("\\n ".join([
"input() doesn't work when PyScript runs in the main thread.",
"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/"
]))
builtins.input = input
del builtins
del input
`;
export const hooks = {
main: {
/** @type {Set<function>} */
onWorker: new SetFunction(),
/** @type {Set<function>} */
onReady: new SetFunction(),
/** @type {Set<function>} */
onBeforeRun: new SetFunction(),
/** @type {Set<function>} */
onBeforeRunAsync: new SetFunction(),
/** @type {Set<function>} */
onAfterRun: new SetFunction(),
/** @type {Set<function>} */
onAfterRunAsync: new SetFunction(),
/** @type {Set<string>} */
codeBeforeRun: new SetString([inputFailure]),
/** @type {Set<string>} */
codeBeforeRunAsync: new SetString(),
/** @type {Set<string>} */
codeAfterRun: new SetString(),
/** @type {Set<string>} */
codeAfterRunAsync: new SetString(),
},
worker: {
/** @type {Set<function>} */
onReady: new SetFunction(),
/** @type {Set<function>} */
onBeforeRun: new SetFunction(),
/** @type {Set<function>} */
onBeforeRunAsync: new SetFunction([
({ interpreter }) => {
interpreter.registerJsModule("_pyscript", {
// cannot be imported from fs.js
// because this code is stringified
fs: {
ERROR: "storage permissions not granted",
NAMESPACE: "@pyscript.fs",
},
interpreter,
});
},
]),
/** @type {Set<function>} */
onAfterRun: new SetFunction(),
/** @type {Set<function>} */
onAfterRunAsync: new SetFunction(),
/** @type {Set<string>} */
codeBeforeRun: new SetString(),
/** @type {Set<string>} */
codeBeforeRunAsync: new SetString(),
/** @type {Set<string>} */
codeAfterRun: new SetString(),
/** @type {Set<string>} */
codeAfterRunAsync: new SetString(),
},
};

View File

@@ -1,26 +0,0 @@
import { defineProperty } from "polyscript/exports";
// helper for all script[type="py"] out there
const before = (script) => {
defineProperty(document, "currentScript", {
configurable: true,
get: () => script,
});
};
const after = () => {
delete document.currentScript;
};
// common life-cycle handlers for any node
export default async (main, wrap, element, hook) => {
const isAsync = hook.endsWith("Async");
const isBefore = hook.startsWith("onBefore");
// make it possible to reach the current target node via Python
// or clean up for other scripts executing around this one
(isBefore ? before : after)(element);
for (const fn of main(hook)) {
if (isAsync) await fn(wrap, element);
else fn(wrap, element);
}
};

View File

@@ -1,38 +0,0 @@
// ⚠️ This file is an artifact: DO NOT MODIFY
export default {
codemirror: () =>
import(
/* webpackIgnore: true */
"./plugins/codemirror.js"
),
["deprecations-manager"]: () =>
import(
/* webpackIgnore: true */
"./plugins/deprecations-manager.js"
),
donkey: () =>
import(
/* webpackIgnore: true */
"./plugins/donkey.js"
),
error: () =>
import(
/* webpackIgnore: true */
"./plugins/error.js"
),
["py-editor"]: () =>
import(
/* webpackIgnore: true */
"./plugins/py-editor.js"
),
["py-game"]: () =>
import(
/* webpackIgnore: true */
"./plugins/py-game.js"
),
["py-terminal"]: () =>
import(
/* webpackIgnore: true */
"./plugins/py-terminal.js"
),
};

View File

@@ -1,31 +0,0 @@
// lazy loaded on-demand codemirror related files
export default {
get core() {
return import(/* webpackIgnore: true */ "../3rd-party/codemirror.js");
},
get state() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"
);
},
get python() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
);
},
get language() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"
);
},
get view() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"
);
},
get commands() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"
);
},
};

View File

@@ -1,27 +0,0 @@
// PyScript Derepcations Plugin
import { notify } from "./error.js";
import { hooks } from "../core.js";
// react lazily on PyScript bootstrap
hooks.main.onReady.add(checkDeprecations);
hooks.main.onWorker.add(checkDeprecations);
/**
* Check that there are no scripts loading from pyscript.net/latest
*/
function checkDeprecations() {
const scripts = document.querySelectorAll("script");
for (const script of scripts) checkLoadingScriptsFromLatest(script.src);
}
/**
* Check if src being loaded from pyscript.net/latest and display a notification if true
* * @param {string} src
*/
function checkLoadingScriptsFromLatest(src) {
if (/\/pyscript\.net\/latest/.test(src)) {
notify(
"Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead.",
);
}
}

View File

@@ -1,121 +0,0 @@
import addPromiseListener from "add-promise-listener";
import { assign, dedent } from "polyscript/exports";
const { stringify } = JSON;
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
const donkey = ({
type = "py",
persistent,
terminal,
config,
serviceWorker,
}) => {
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
const src = URL.createObjectURL(
new Blob([
[
// this array is to better minify this code once in production
"from pyscript import sync, config",
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
"__locals__ = {}",
'if config["type"] == "py":',
" import sys",
" def __error__(_):",
" info = sys.exc_info()",
" return __message__(info[0], info[1])",
"else:",
" __error__ = lambda e: __message__(e.__class__, e.value)",
"def execute(code):",
` try: return ${invoke("exec", args)};`,
" except Exception as e: print(__error__(e));",
"def evaluate(code):",
` try: return ${invoke("eval", args)};`,
" except Exception as e: print(__error__(e));",
"sync.execute = execute",
"sync.evaluate = evaluate",
].join("\n"),
]),
);
// create the script that exposes the code to execute or evaluate
const script = assign(document.createElement("script"), { type, src });
script.toggleAttribute("worker", true);
script.toggleAttribute("terminal", true);
if (terminal) script.setAttribute("target", terminal);
if (config) {
script.setAttribute(
"config",
typeof config === "string" ? config : stringify(config),
);
}
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
return addPromiseListener(
document.body.appendChild(script),
`${type}:done`,
{ stopPropagation: true },
).then(() => {
URL.revokeObjectURL(src);
return script;
});
};
const utils = async (options) => {
const script = await donkey(options);
const { xworker, process, terminal } = script;
const { execute, evaluate } = xworker.sync;
script.remove();
return {
xworker,
process,
terminal,
execute,
evaluate,
};
};
export default async (options = {}) => {
let farmer = await utils(options);
let working = false;
const kill = () => {
if (farmer) {
farmer.xworker.terminate();
farmer.terminal.dispose();
farmer = null;
}
working = false;
};
const reload = async () => {
kill();
farmer = await utils(options);
};
const asyncTask = (method) => async (code) => {
// race condition ... a new task has been
// assigned while the previous one didn't finish
if (working) await reload();
working = true;
try {
return await farmer[method](dedent(code));
} catch (e) {
console.error(e);
} finally {
working = false;
}
};
const asyncMethod = (method) => async () => {
if (working) await reload();
else farmer?.terminal[method]();
};
return {
process: asyncTask("process"),
execute: asyncTask("execute"),
evaluate: asyncTask("evaluate"),
clear: asyncMethod("clear"),
reset: asyncMethod("reset"),
kill,
};
};

View File

@@ -1,56 +0,0 @@
// PyScript Error Plugin
import { buffered } from "polyscript/exports";
import { hooks } from "../core.js";
let dontBotherDOM = false;
export function notOnDOM() {
dontBotherDOM = true;
}
hooks.main.onReady.add(function override(pyScript) {
// be sure this override happens only once
hooks.main.onReady.delete(override);
// trap generic `stderr` to propagate to it regardless
const { stderr } = pyScript.io;
const cb = (error, ...rest) => {
notify(error.message || error);
// let other plugins or stderr hook, if any, do the rest
return stderr(error, ...rest);
};
// override it with our own logic
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
// be sure uncaught Python errors are also visible
addEventListener("error", ({ message }) => {
if (message.startsWith("Uncaught PythonError")) notify(message);
});
});
// Error hook utilities
// Custom function to show notifications
/**
* Add a banner to the top of the page, notifying the user of an error
* @param {string} message
*/
export function notify(message) {
if (dontBotherDOM) return;
const div = document.createElement("div");
div.className = "py-error";
div.textContent = message;
div.style.cssText = `
border: 1px solid red;
background: #ffdddd;
color: black;
font-family: courier, monospace;
white-space: pre;
overflow-x: auto;
padding: 8px;
margin-top: 8px;
`;
document.body.append(div);
}

View File

@@ -1,491 +0,0 @@
// PyScript py-editor plugin
import withResolvers from "@webreflection/utils/with-resolvers";
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
import { notify } from "./error.js";
import codemirror from "./codemirror.js";
const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
let id = 0;
const getID = (type) => `${type}-editor-${id++}`;
const envs = new Map();
const configs = new Map();
const editors = new WeakMap();
const hooks = {
worker: {
codeBeforeRun: () => stdlib,
// works on both Pyodide and MicroPython
onReady: ({ runAsync, io }, { sync }) => {
io.stdout = io.buffered(sync.write);
io.stderr = io.buffered(sync.writeErr);
sync.revoke();
sync.runAsync = runAsync;
},
},
};
const validate = (config, result) => {
if (typeof result === "boolean") throw `Invalid source: ${config}`;
return result;
};
const getRelatedScript = (target, type) => {
const editor = target.closest(`.${type}-editor-box`);
return editor?.parentNode?.previousElementSibling;
};
async function execute({ currentTarget, script }) {
const { env, pySrc, outDiv } = this;
const hasRunButton = !!currentTarget;
if (hasRunButton) {
currentTarget.classList.add("running");
currentTarget.innerHTML = STOP_BUTTON;
outDiv.innerHTML = "";
}
if (!envs.has(env)) {
const srcLink = URL.createObjectURL(new Blob([""]));
const details = {
type: this.interpreter,
serviceWorker: this.serviceWorker,
};
const { config } = this;
if (config) {
// verify that config can be parsed and used
try {
details.configURL = relative_url(config);
if (config.endsWith(".toml")) {
const [{ parse }, toml] = await Promise.all([
import(
/* webpackIgnore: true */ "../3rd-party/toml.js"
),
fetch(config).then((r) => r.ok && r.text()),
]);
details.config = parse(validate(config, toml));
} else if (config.endsWith(".json")) {
const json = await fetch(config).then(
(r) => r.ok && r.json(),
);
details.config = validate(config, json);
} else {
details.configURL = relative_url("./config.txt");
details.config = JSON.parse(config);
}
details.version = offline_interpreter(details.config);
} catch (error) {
notify(error);
return;
}
} else {
details.config = {};
}
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
// expose xworker like in terminal or other workers to allow
// creation and destruction of editors on the fly
if (hasRunButton) {
for (const type of TYPES.keys()) {
script = getRelatedScript(currentTarget, type);
if (script) break;
}
}
defineProperties(script, { xworker: { value: xworker } });
const { sync } = xworker;
const { promise, resolve } = withResolvers();
envs.set(env, promise);
sync.revoke = () => {
URL.revokeObjectURL(srcLink);
resolve(xworker);
};
}
// wait for the env then set the target div
// before executing the current code
return envs.get(env).then((xworker) => {
xworker.onerror = ({ error }) => {
if (hasRunButton) {
outDiv.insertAdjacentHTML(
"beforeend",
`<span style='color:red'>${
error.message || error
}</span>\n`,
);
}
console.error(error);
};
const enable = () => {
if (hasRunButton) {
currentTarget.classList.remove("running");
currentTarget.innerHTML = RUN_BUTTON;
const { previousElementSibling } =
currentTarget.closest("[data-env]").parentElement;
previousElementSibling?.dispatchEvent(
new Event("py-editor:done", {
bubbles: true,
cancelable: true,
}),
);
}
};
const { sync } = xworker;
sync.write = (str) => {
if (hasRunButton) outDiv.innerText += `${str}\n`;
else console.log(str);
};
sync.writeErr = (str) => {
if (hasRunButton) {
outDiv.insertAdjacentHTML(
"beforeend",
`<span style='color:red'>${str}</span>\n`,
);
} else {
notify(str);
console.error(str);
}
};
sync.runAsync(pySrc).then(enable, enable);
});
}
const replaceScript = (script, type) => {
script.xworker?.terminate();
const clone = script.cloneNode(true);
clone.type = `${type}-editor`;
const editor = editors.get(script);
if (editor) {
const content = editor.state.doc.toString();
clone.textContent = content;
editors.delete(script);
script.nextElementSibling.remove();
}
script.replaceWith(clone);
};
const makeRunButton = (handler, type) => {
const runButton = document.createElement("button");
runButton.className = `absolute ${type}-editor-run-button`;
runButton.innerHTML = RUN_BUTTON;
runButton.setAttribute("aria-label", "Python Script Run Button");
runButton.addEventListener("click", async (event) => {
if (
runButton.classList.contains("running") &&
confirm("Stop evaluating this code?")
) {
const script = getRelatedScript(runButton, type);
if (script) {
const env = script.getAttribute("env");
// remove the bootstrapped env which could be one or shared
if (env) {
for (const [key, value] of TYPES) {
if (key === type) {
configs.delete(`${value}-${env}`);
envs.delete(`${value}-${env}`);
break;
}
}
}
// lonley script without setup node should be replaced
if (script.xworker) replaceScript(script, type);
// all scripts sharing the same env should be replaced
else {
const sel = `script[type^="${type}-editor"][env="${env}"]`;
for (const script of document.querySelectorAll(sel))
replaceScript(script, type);
}
}
return;
}
runButton.blur();
await handler.handleEvent(event);
});
return runButton;
};
const makeEditorDiv = (handler, type) => {
const editorDiv = document.createElement("div");
editorDiv.className = `${type}-editor-input`;
editorDiv.setAttribute("aria-label", "Python Script Area");
const runButton = makeRunButton(handler, type);
const editorShadowContainer = document.createElement("div");
// avoid outer elements intercepting key events (reveal as example)
editorShadowContainer.addEventListener("keydown", (event) => {
event.stopPropagation();
});
editorDiv.append(runButton, editorShadowContainer);
return editorDiv;
};
const makeOutDiv = (type) => {
const outDiv = document.createElement("div");
outDiv.className = `${type}-editor-output`;
outDiv.id = `${getID(type)}-output`;
return outDiv;
};
const makeBoxDiv = (handler, type) => {
const boxDiv = document.createElement("div");
boxDiv.className = `${type}-editor-box`;
const editorDiv = makeEditorDiv(handler, type);
const outDiv = makeOutDiv(type);
boxDiv.append(editorDiv, outDiv);
return [boxDiv, outDiv, editorDiv.querySelector("button")];
};
const init = async (script, type, interpreter) => {
const [
{ basicSetup, EditorView },
{ Compartment },
{ python },
{ indentUnit },
{ keymap },
{ defaultKeymap, indentWithTab },
] = await Promise.all([
codemirror.core,
codemirror.state,
codemirror.python,
codemirror.language,
codemirror.view,
codemirror.commands,
]);
let isSetup = script.hasAttribute("setup");
const hasConfig = script.hasAttribute("config");
const serviceWorker = script.getAttribute("service-worker");
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
// helps preventing too lazy ServiceWorker initialization on button run
if (serviceWorker) {
new XWorker("data:application/javascript,postMessage(0)", {
type: "dummy",
serviceWorker,
}).onmessage = ({ target }) => target.terminate();
}
if (hasConfig && configs.has(env)) {
throw new SyntaxError(
configs.get(env)
? `duplicated config for env: ${env}`
: `unable to add a config to the env: ${env}`,
);
}
configs.set(env, hasConfig);
let source = script.textContent;
// verify the src points to a valid file that can be parsed
const { src } = script;
if (src) {
try {
source = validate(
src,
await fetch(src).then((b) => b.ok && b.text()),
);
} catch (error) {
notify(error);
return;
}
}
const context = {
// allow the listener to be overridden at distance
handleEvent: execute,
serviceWorker,
interpreter,
env,
config: hasConfig && script.getAttribute("config"),
get pySrc() {
return isSetup ? source : editor.state.doc.toString();
},
get outDiv() {
return isSetup ? null : outDiv;
},
};
let target;
defineProperties(script, {
target: { get: () => target },
handleEvent: {
get: () => context.handleEvent,
set: (callback) => {
// do not bother with logic if it was set back as its original handler
if (callback === execute) context.handleEvent = execute;
// in every other case be sure that if the listener override returned
// `false` nothing happens, otherwise keep doing what it always did
else {
context.handleEvent = async (event) => {
// trap the currentTarget ASAP (if any)
// otherwise it gets lost asynchronously
const { currentTarget } = event;
// augment a code snapshot before invoking the override
defineProperties(event, {
code: { value: context.pySrc },
});
// avoid executing the default handler if the override returned `false`
if ((await callback(event)) !== false)
await execute.call(context, { currentTarget });
};
}
},
},
code: {
get: () => context.pySrc,
set: (insert) => {
if (isSetup) return;
editor.update([
editor.state.update({
changes: {
from: 0,
to: editor.state.doc.length,
insert,
},
}),
]);
},
},
process: {
/**
* Simulate a setup node overriding the source to evaluate.
* @param {string} code the Python code to evaluate.
* @param {boolean} asRunButtonAction invoke the `Run` button handler.
* @returns {Promise<...>} fulfill once code has been evaluated.
*/
value(code, asRunButtonAction = false) {
if (asRunButtonAction) return listener();
const wasSetup = isSetup;
const wasSource = source;
isSetup = true;
source = code;
const restore = () => {
isSetup = wasSetup;
source = wasSource;
};
return context
.handleEvent({ currentTarget: null })
.then(restore, restore);
},
},
});
const notifyEditor = () => {
const event = new Event(`${type}-editor`, { bubbles: true });
script.dispatchEvent(event);
};
if (isSetup) {
await context.handleEvent({ currentTarget: null, script });
notifyEditor();
return;
}
const selector = script.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement(`${type}-editor`);
target.style.display = "block";
script.after(target);
}
if (!target.id) target.id = getID(type);
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
const parent = inputChild.attachShadow({ mode: "open" });
// avoid inheriting styles from the outer component
parent.innerHTML = `<style> :host { all: initial; }</style>`;
target.appendChild(boxDiv);
const doc = dedent(script.textContent).trim();
// preserve user indentation, if any
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
const listener = () => !runButton.click();
const editor = new EditorView({
extensions: [
indentUnit.of(indentation),
new Compartment().of(python()),
keymap.of([
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
{ key: "Cmd-Enter", run: listener, preventDefault: true },
{ key: "Shift-Enter", run: listener, preventDefault: true },
// Consider removing defaultKeymap as likely redundant with basicSetup
...defaultKeymap,
// @see https://codemirror.net/examples/tab/
indentWithTab,
]),
basicSetup,
],
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
parent,
doc,
});
editors.set(script, editor);
editor.focus();
notifyEditor();
};
// avoid too greedy MutationObserver operations at distance
let timeout = 0;
// avoid delayed initialization
let queue = Promise.resolve();
// reset interval value then check for new scripts
const resetTimeout = () => {
timeout = 0;
pyEditor();
};
// triggered both ASAP on the living DOM and via MutationObserver later
const pyEditor = () => {
if (timeout) return;
timeout = setTimeout(resetTimeout, 250);
for (const [type, interpreter] of TYPES) {
const selector = `script[type="${type}-editor"]`;
for (const script of document.querySelectorAll(selector)) {
// avoid any further bootstrap by changing the type as active
script.type += "-active";
// don't await in here or multiple calls might happen
// while the first script is being initialized
queue = queue.then(() => init(script, type, interpreter));
}
}
return queue;
};
new MutationObserver(pyEditor).observe(document, {
childList: true,
subtree: true,
});
// try to check the current document ASAP
export default pyEditor();

View File

@@ -1,112 +0,0 @@
import {
dedent,
define,
createProgress,
loadProgress,
} from "polyscript/exports";
import { stdlib } from "../core.js";
import { configDetails } from "../config.js";
import { getText } from "../fetch.js";
const progress = createProgress("py-game");
const inputPatch = `
import builtins
def input(prompt=""):
import js
return js.prompt(prompt)
builtins.input = input
del builtins
del input
`;
let toBeWarned = true;
const hooks = {
main: {
onReady: async (wrap, script) => {
if (toBeWarned) {
toBeWarned = false;
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
}
let config = {};
if (script.hasAttribute("config")) {
const value = script.getAttribute("config");
const { json, toml, text, url } = await configDetails(value);
if (json) config = JSON.parse(text);
else if (toml) {
const { parse } = await import(
/* webpackIgnore: true */ "../3rd-party/toml.js"
);
config = parse(text);
}
if (config.packages) {
await wrap.interpreter.loadPackage("micropip");
const micropip = wrap.interpreter.pyimport("micropip");
await micropip.install(config.packages, {
keep_going: true,
});
micropip.destroy();
}
await loadProgress(
"py-game",
progress,
wrap.interpreter,
config,
url ? new URL(url, location.href).href : location.href,
);
}
wrap.interpreter.registerJsModule("_pyscript", {
PyWorker() {
throw new Error(
"Unable to use PyWorker in py-game scripts",
);
},
js_import: (...urls) =>
Promise.all(urls.map((url) => import(url))),
get target() {
return script.id;
},
});
await wrap.interpreter.runPythonAsync(stdlib);
wrap.interpreter.runPython(inputPatch);
let code = dedent(script.textContent);
if (script.src) code = await fetch(script.src).then(getText);
const target = script.getAttribute("target") || "canvas";
const canvas = document.getElementById(target);
wrap.interpreter.canvas.setCanvas2D(canvas);
// allow 3rd party to hook themselves right before
// the code gets executed
const event = new CustomEvent("py-game", {
bubbles: true,
cancelable: true,
detail: {
canvas,
code,
config,
wrap,
},
});
script.dispatchEvent(event);
// run only if the default was not prevented
if (!event.defaultPrevented)
await wrap.interpreter.runPythonAsync(code);
},
},
};
define("py-game", {
config: { packages: ["pygame-ce"] },
configURL: new URL("./config.txt", location.href).href,
interpreter: "pyodide",
env: "py-game",
hooks,
});

View File

@@ -1,60 +0,0 @@
// PyScript py-terminal plugin
import { TYPES, relative_url } from "../core.js";
import { notify } from "./error.js";
import { customObserver } from "polyscript/exports";
// will contain all valid selectors
const SELECTORS = [];
// avoid processing same elements twice
const processed = new WeakSet();
// show the error on main and
// stops the module from keep executing
const notifyAndThrow = (message) => {
notify(message);
throw new Error(message);
};
const onceOnMain = ({ attributes: { worker } }) => !worker;
let addStyle = true;
for (const type of TYPES.keys()) {
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
SELECTORS.push(selector);
customObserver.set(selector, async (element) => {
// we currently support only one terminal on main as in "classic"
const terminals = document.querySelectorAll(SELECTORS.join(","));
if ([].filter.call(terminals, onceOnMain).length > 1)
notifyAndThrow("You can use at most 1 main terminal");
// import styles lazily
if (addStyle) {
addStyle = false;
document.head.append(
Object.assign(document.createElement("link"), {
rel: "stylesheet",
href: relative_url("./xterm.css", import.meta.url),
}),
);
}
if (processed.has(element)) return;
processed.add(element);
const bootstrap = (module) => module.default(element);
// we can't be smart with template literals for the dynamic import
// or bundlers are incapable of producing multiple files around
if (type === "mpy") {
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
bootstrap,
);
} else {
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
bootstrap,
);
}
});
}

View File

@@ -1,256 +0,0 @@
// PyScript pyodide terminal plugin
import withResolvers from "@webreflection/utils/with-resolvers";
import { defineProperties } from "polyscript/exports";
import { hooks, inputFailure } from "../../core.js";
const bootstrapped = new WeakSet();
// this callback will be serialized as string and it never needs
// to be invoked multiple times. Each xworker here is bootstrapped
// only once thanks to the `sync.is_pyterminal()` check.
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
if (type !== "mpy" || !sync.is_pyterminal()) return;
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
interpreter.registerJsModule("_pyscript_input", {
input: pyterminal_read,
});
run(
[
"from _pyscript_input import input",
"from polyscript import currentScript as _",
"__terminal__ = _.terminal",
"del _",
].join(";"),
);
const missingReturn = new Uint8Array([13]);
io.stdout = (buffer) => {
if (buffer[0] === 10) pyterminal_write(missingReturn);
pyterminal_write(buffer);
};
io.stderr = (error) => {
pyterminal_write(String(error.message || error));
};
sync.pyterminal_stream_write = () => {};
// tiny shim of the code module with only interact
// to bootstrap a REPL like environment
interpreter.registerJsModule("code", {
interact() {
const encoder = new TextEncoderStream();
encoder.readable.pipeTo(
new WritableStream({
write(buffer) {
for (const c of buffer) interpreter.replProcessChar(c);
},
}),
);
const writer = encoder.writable.getWriter();
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
interpreter.replInit();
},
});
pyterminal_ready();
};
export default async (element) => {
// lazy load these only when a valid terminal is found
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
),
]);
const terminalOptions = {
disableStdin: false,
cursorBlink: true,
cursorStyle: "block",
lineHeight: 1.2,
};
let stream;
// common main thread initialization for both worker
// or main case, bootstrapping the terminal on its target
const init = () => {
let target = element;
const selector = element.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement("py-terminal");
target.style.display = "block";
element.after(target);
}
const terminal = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
...terminalOptions,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.open(target);
fitAddon.fit();
terminal.focus();
defineProperties(element, {
terminal: { value: terminal },
process: {
value: async (code) => {
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
await stream.write(`${line}\r`);
}
},
},
});
return terminal;
};
// branch logic for the worker
if (element.hasAttribute("worker")) {
// add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main *BUT* ...
hooks.main.onWorker.add(function worker(_, xworker) {
// ... as multiple workers will add multiple callbacks
// be sure no xworker is ever initialized twice!
if (bootstrapped.has(xworker)) return;
bootstrapped.add(xworker);
// still cleanup this callback for future scripts/workers
hooks.main.onWorker.delete(worker);
const terminal = init();
const { sync } = xworker;
// handle the read mode on input
let promisedChunks = null;
let readChunks = "";
sync.is_pyterminal = () => true;
// put the terminal in a read-only state
// frees the worker on \r
sync.pyterminal_read = (buffer) => {
terminal.write(buffer);
promisedChunks = withResolvers();
return promisedChunks.promise;
};
// write if not reading input
sync.pyterminal_write = (buffer) => {
if (!promisedChunks) terminal.write(buffer);
};
// add the onData terminal listener which forwards to the worker
// everything typed in a queued char-by-char way
sync.pyterminal_ready = () => {
let queue = Promise.resolve();
stream = {
write: (buffer) =>
(queue = queue.then(() =>
sync.pyterminal_stream_write(buffer),
)),
};
terminal.onData((buffer) => {
if (promisedChunks) {
// handle backspace on input
if (buffer === "\x7f") {
// avoid over-greedy backspace
if (readChunks.length) {
readChunks = readChunks.slice(0, -1);
// override previous char position
// put an empty space to clear the char
// move back position again
buffer = "\b \b";
} else buffer = "";
} else readChunks += buffer;
if (buffer) {
terminal.write(buffer);
if (readChunks.endsWith("\r")) {
terminal.write("\n");
promisedChunks.resolve(readChunks.slice(0, -1));
promisedChunks = null;
readChunks = "";
}
}
} else {
stream.write(buffer);
}
});
};
});
// setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal
hooks.worker.onReady.add(workerReady);
} else {
// ⚠️ In an ideal world the inputFailure should never be used on main.
// However, Pyodide still can't compete with MicroPython REPL mode
// so while it's OK to keep that entry on main as default, we need
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
// also throw whenever an `input(...)` is required / digited.
hooks.main.codeBeforeRun.delete(inputFailure);
// in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
if (type !== "mpy") return;
hooks.main.onReady.delete(main);
const terminal = init();
const missingReturn = new Uint8Array([13]);
io.stdout = (buffer) => {
if (buffer[0] === 10) terminal.write(missingReturn);
terminal.write(buffer);
};
// expose the __terminal__ one-off reference
globalThis.__py_terminal__ = terminal;
run(
[
"from js import prompt as input",
"from js import __py_terminal__ as __terminal__",
].join(";"),
);
delete globalThis.__py_terminal__;
// NOTE: this is NOT the same as the one within
// the onWorkerReady callback!
interpreter.registerJsModule("code", {
interact() {
const encoder = new TextEncoderStream();
encoder.readable.pipeTo(
new WritableStream({
write(buffer) {
for (const c of buffer)
interpreter.replProcessChar(c);
},
}),
);
stream = encoder.writable.getWriter();
terminal.onData((buffer) => stream.write(buffer));
interpreter.replInit();
},
});
});
}
};

View File

@@ -1,192 +0,0 @@
// PyScript py-terminal plugin
import { defineProperties } from "polyscript/exports";
import { hooks } from "../../core.js";
const bootstrapped = new WeakSet();
// this callback will be serialized as string and it never needs
// to be invoked multiple times. Each xworker here is bootstrapped
// only once thanks to the `sync.is_pyterminal()` check.
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
if (type !== "py" || !sync.is_pyterminal()) return;
run(
[
"from polyscript import currentScript as _",
"__terminal__ = _.terminal",
"del _",
].join(";"),
);
let data = "";
const { pyterminal_read, pyterminal_write } = sync;
const decoder = new TextDecoder();
const generic = {
isatty: false,
write(buffer) {
data = decoder.decode(buffer);
pyterminal_write(data);
return buffer.length;
},
};
io.stderr = (error) => {
pyterminal_write(String(error.message || error));
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: false,
stdin: () => pyterminal_read(data),
});
};
export default async (element) => {
// lazy load these only when a valid terminal is found
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
await Promise.all([
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
),
]);
const readline = new Readline();
// common main thread initialization for both worker
// or main case, bootstrapping the terminal on its target
const init = (options) => {
let target = element;
const selector = element.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement("py-terminal");
target.style.display = "block";
element.after(target);
}
const terminal = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
...options,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(readline);
terminal.loadAddon(new WebLinksAddon());
terminal.open(target);
fitAddon.fit();
terminal.focus();
defineProperties(element, {
terminal: { value: terminal },
process: {
value: async (code) => {
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
terminal.paste(`${line}`);
terminal.write("\r\n");
do {
await new Promise((resolve) =>
setTimeout(resolve, 0),
);
} while (!readline.activeRead?.resolve);
readline.activeRead.resolve(line);
}
},
},
});
return terminal;
};
// branch logic for the worker
if (element.hasAttribute("worker")) {
// add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main *BUT* ...
hooks.main.onWorker.add(function worker(_, xworker) {
// ... as multiple workers will add multiple callbacks
// be sure no xworker is ever initialized twice!
if (bootstrapped.has(xworker)) return;
bootstrapped.add(xworker);
// still cleanup this callback for future scripts/workers
hooks.main.onWorker.delete(worker);
init({
disableStdin: false,
cursorBlink: true,
cursorStyle: "block",
lineHeight: 1.2,
});
xworker.sync.is_pyterminal = () => true;
xworker.sync.pyterminal_read = readline.read.bind(readline);
xworker.sync.pyterminal_write = readline.write.bind(readline);
});
// setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal
hooks.worker.onReady.add(workerReady);
// @see https://github.com/pyscript/pyscript/issues/2246
const patchInput = [
"import builtins as _b",
"from pyscript import sync as _s",
"_b.input = _s.pyterminal_read",
"del _b",
"del _s",
].join("\n");
hooks.worker.codeBeforeRun.add(patchInput);
hooks.worker.codeBeforeRunAsync.add(patchInput);
} else {
// in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
if (type !== "py") return;
console.warn("py-terminal is read only on main thread");
hooks.main.onReady.delete(main);
// on main, it's easy to trash and clean the current terminal
globalThis.__py_terminal__ = init({
disableStdin: true,
cursorBlink: false,
cursorStyle: "underline",
});
run("from js import __py_terminal__ as __terminal__");
delete globalThis.__py_terminal__;
io.stderr = (error) => {
readline.write(String(error.message || error));
};
let data = "";
const decoder = new TextDecoder();
const generic = {
isatty: false,
write(buffer) {
data = decoder.decode(buffer);
readline.write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: false,
stdin: () => readline.read(data),
});
});
}
};

View File

@@ -1 +0,0 @@
import "polyscript/service-worker";

View File

@@ -1,70 +0,0 @@
/**
* Create through Python the pyscript module through
* the artifact generated at build time.
* This the returned value is a string that must be used
* either before a worker execute code or when the module
* is registered on the main thread.
*/
import pyscript from "./stdlib/pyscript.js";
class Ignore extends Array {
#add = false;
#paths;
#array;
constructor(array, ...paths) {
super();
this.#array = array;
this.#paths = paths;
}
push(...values) {
if (this.#add) super.push(...values);
return this.#array.push(...values);
}
path(path) {
for (const _path of this.#paths) {
// bails out at the first `true` value
if ((this.#add = path.startsWith(_path))) break;
}
}
}
const { entries } = Object;
const python = [
"import os as _os",
"from pathlib import Path as _Path",
"_path = None",
];
const ignore = new Ignore(python, "-");
const write = (base, literal) => {
for (const [key, value] of entries(literal)) {
ignore.path(`${base}/${key}`);
ignore.push(`_path = _Path("${base}/${key}")`);
if (typeof value === "string") {
const code = JSON.stringify(value);
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
} else {
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
write(`${base}/${key}`, value);
}
}
};
write(".", pyscript);
// in order to fix js.document in the Worker case
// we need to bootstrap pyscript module ASAP
python.push("import pyscript as _pyscript");
python.push(
...["_Path", "_path", "_os", "_pyscript"].map((ref) => `del ${ref}`),
);
python.push("\n");
export const stdlib = python.join("\n");
export const optional = ignore.join("\n");

File diff suppressed because one or more lines are too long

View File

@@ -1,120 +0,0 @@
"""
This is the main `pyscript` namespace. It provides the primary Pythonic API
for users to interact with the
[browser's own API](https://developer.mozilla.org/en-US/docs/Web/API). It
includes utilities for common activities such as displaying content, handling
events, fetching resources, managing local storage, and coordinating with
web workers.
The most important names provided by this namespace can be directly imported
from `pyscript`, for example:
```python
from pyscript import display, HTML, fetch, when, storage, WebSocket
```
The following names are available in the `pyscript` namespace:
- `RUNNING_IN_WORKER`: Boolean indicating if the code is running in a Web
Worker.
- `PyWorker`: Class for creating Web Workers running Python code.
- `config`: Configuration object for pyscript settings.
- `current_target`: The element in the DOM that is the current target for
output.
- `document`: The standard `document` object, proxied in workers.
- `window`: The standard `window` object, proxied in workers.
- `js_import`: Function to dynamically import JS modules.
- `js_modules`: Object containing JS modules available to Python.
- `sync`: Utility for synchronizing between worker and main thread.
- `display`: Function to render Python objects in the web page.
- `HTML`: Helper class to create HTML content for display.
- `fetch`: Function to perform HTTP requests.
- `Storage`: Class representing browser storage (local/session).
- `storage`: Object to interact with browser's local storage.
- `WebSocket`: Class to create and manage WebSocket connections.
- `when`: Function to register event handlers on DOM elements.
- `Event`: Class representing user defined or DOM events.
- `py_import`: Function to lazily import Pyodide related Python modules.
If running in the main thread, the following additional names are available:
- `create_named_worker`: Function to create a named Web Worker.
- `workers`: Object to manage and interact with existing Web Workers.
All of these names are defined in the various submodules of `pyscript` and
are imported and re-exported here for convenience. Please refer to the
respective submodule documentation for more details on each component.
!!! Note
Some notes about the naming conventions and the relationship between
various similar-but-different names found within this code base.
```python
import pyscript
```
The `pyscript` package contains the main user-facing API offered by
PyScript. All the names which are supposed be used by end users should
be made available in `pyscript/__init__.py` (i.e., this source file).
```python
import _pyscript
```
The `_pyscript` module is an internal API implemented in JS. **End users
should not use it directly**. For its implementation, grep for
`interpreter.registerJsModule("_pyscript",...)` in `core.js`.
```python
import js
```
The `js` object is
[the JS `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis),
as exported by Pyodide and/or Micropython's foreign function interface
(FFI). As such, it contains different things in the main thread or in a
worker, as defined by web standards.
```python
import pyscript.context
```
The `context` submodule abstracts away some of the differences between
the main thread and a worker. Its most important features are made
available in the root `pyscript` namespace. All other functionality is
mostly for internal PyScript use or advanced users. In particular, it
defines `window` and `document` in such a way that these names work in
both cases: in the main thread, they are the "real" objects, in a worker
they are proxies which work thanks to
[coincident](https://github.com/WebReflection/coincident).
```python
from pyscript import window, document
```
These are just the `window` and `document` objects as defined by
`pyscript.context`. This is the blessed way to access them from `pyscript`,
as it works transparently in both the main thread and worker cases.
"""
from polyscript import lazy_py_modules as py_import
from pyscript.context import (
RUNNING_IN_WORKER,
PyWorker,
config,
current_target,
document,
js_import,
js_modules,
sync,
window,
)
from pyscript.display import HTML, display
from pyscript.fetch import fetch
from pyscript.storage import Storage, storage
from pyscript.websocket import WebSocket
from pyscript.events import when, Event
if not RUNNING_IN_WORKER:
from pyscript.workers import create_named_worker, workers

View File

@@ -1,198 +0,0 @@
"""
Execution context management for PyScript.
This module handles the differences between running in the
[main browser thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread)
versus running in a
[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers),
providing a consistent API regardless of the execution context.
Key features:
- Detects whether code is running in a worker or main thread. Read this via
the boolean `pyscript.context.RUNNING_IN_WORKER`.
- Parses and normalizes configuration from `polyscript.config` and adds the
Python interpreter type via the `type` key in `pyscript.context.config`.
- Provides appropriate implementations of `window`, `document`, and `sync`.
- Sets up JavaScript module import system, including a lazy `js_import`
function.
- Manages `PyWorker` creation.
- Provides access to the current display target via
`pyscript.context.display_target`.
!!! warning
These are key differences between the main thread and worker contexts:
Main thread context:
- `window` and `document` are available directly.
- `PyWorker` can be created to spawn worker threads.
- `sync` is not available (raises `NotSupported`).
Worker context:
- `window` and `document` are proxied from main thread (if SharedArrayBuffer
available).
- `PyWorker` is not available (raises `NotSupported`).
- `sync` utilities are available for main thread communication.
"""
import json
import sys
import js
from polyscript import config as _polyscript_config
from polyscript import js_modules
from pyscript.util import NotSupported
RUNNING_IN_WORKER = not hasattr(js, "document")
"""Detect execution context: True if running in a worker, False if main thread."""
config = json.loads(js.JSON.stringify(_polyscript_config))
"""Parsed and normalized configuration."""
if isinstance(config, str):
config = {}
js_import = None
"""Function to import JavaScript modules dynamically."""
window = None
"""The `window` object (proxied if in a worker)."""
document = None
"""The `document` object (proxied if in a worker)."""
sync = None
"""Sync utilities for worker-main thread communication (only in workers)."""
# Detect and add Python interpreter type to config.
if "MicroPython" in sys.version:
config["type"] = "mpy"
else:
config["type"] = "py"
class _JSModuleProxy:
"""
Proxy for JavaScript modules imported via js_modules.
This allows Python code to import JavaScript modules using Python's
import syntax:
```python
from pyscript.js_modules lodash import debounce
```
The proxy lazily retrieves the actual JavaScript module when accessed.
"""
def __init__(self, name):
"""
Create a proxy for the named JavaScript module.
"""
self.name = name
def __getattr__(self, field):
"""
Retrieve a JavaScript object/function from the proxied JavaScript
module via the given `field` name.
"""
# Avoid Pyodide looking for non-existent special methods.
if not field.startswith("_"):
return getattr(getattr(js_modules, self.name), field)
return None
# Register all available JavaScript modules in Python's module system.
# This enables: from pyscript.js_modules.xxx import yyy
for module_name in js.Reflect.ownKeys(js_modules):
sys.modules[f"pyscript.js_modules.{module_name}"] = _JSModuleProxy(module_name)
sys.modules["pyscript.js_modules"] = js_modules
# Context-specific setup: Worker vs Main Thread.
if RUNNING_IN_WORKER:
import polyscript
# PyWorker cannot be created from within a worker.
PyWorker = NotSupported(
"pyscript.PyWorker",
"pyscript.PyWorker works only when running in the main thread",
)
# Attempt to access main thread's window and document via SharedArrayBuffer.
try:
window = polyscript.xworker.window
document = window.document
js.document = document
# Create js_import function that runs imports on the main thread.
js_import = window.Function(
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
)()
except:
# SharedArrayBuffer not available - window/document cannot be proxied.
sab_error_message = (
"Unable to use `window` or `document` in worker. "
"This requires SharedArrayBuffer support. "
"See: https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
)
js.console.warn(sab_error_message)
window = NotSupported("pyscript.window", sab_error_message)
document = NotSupported("pyscript.document", sab_error_message)
# Worker-specific utilities for main thread communication.
sync = polyscript.xworker.sync
def current_target():
"""
Get the current output target in worker context.
"""
return polyscript.target
else:
# Main thread context setup.
import _pyscript
from _pyscript import PyWorker as _PyWorker
from pyscript.ffi import to_js
js_import = _pyscript.js_import
def PyWorker(url, **options):
"""
Create a Web Worker running Python code.
This spawns a new worker thread that can execute Python code
found at the `url`, independently of the main thread. The
`**options` can be used to configure the worker.
```python
from pyscript import PyWorker
# Create a worker to run background tasks.
# (`type` MUST be either `micropython` or `pyodide`)
worker = PyWorker("./worker.py", type="micropython")
```
PyWorker **can only be created from the main thread**, not from
within another worker.
"""
return _PyWorker(url, to_js(options))
# Main thread has direct access to window and document.
window = js
document = js.document
# sync is not available in main thread (only in workers).
sync = NotSupported(
"pyscript.sync", "pyscript.sync works only when running in a worker"
)
def current_target():
"""
Get the current output target in main thread context.
"""
return _pyscript.target

View File

@@ -1,263 +0,0 @@
"""
Display Pythonic content in the browser.
This module provides the `display()` function for rendering Python objects
in the web page. The function introspects objects to determine the appropriate
[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types)
and rendering method.
Supported MIME types:
- `text/plain`: Plain text (HTML-escaped)
- `text/html`: HTML content
- `image/png`: PNG images as data URLs
- `image/jpeg`: JPEG images as data URLs
- `image/svg+xml`: SVG graphics
- `application/json`: JSON data
- `application/javascript`: JavaScript code (discouraged)
The `display()` function uses standard Python representation methods
(`_repr_html_`, `_repr_png_`, etc.) to determine how to render objects.
Objects can provide a `_repr_mimebundle_` method to specify preferred formats
like this:
```python
def _repr_mimebundle_(self):
return {
"text/html": "<b>Bold HTML</b>",
"image/png": "<base64-encoded-png-data>",
}
```
Heavily inspired by
[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html).
"""
import base64
import html
import io
from collections import OrderedDict
from pyscript.context import current_target, document, window
from pyscript.ffi import is_none
def _render_image(mime, value, meta):
"""
Render image (`mime`) data (`value`) as an HTML img element with data URL.
Any `meta` attributes are added to the img tag.
Accepts both raw bytes and base64-encoded strings for flexibility. This
only handles PNG and JPEG images. SVG images are handled separately as
their raw XML content (which the browser can render directly).
"""
if isinstance(value, bytes):
value = base64.b64encode(value).decode("utf-8")
attrs = "".join([f' {k}="{v}"' for k, v in meta.items()])
return f'<img src="data:{mime};base64,{value}"{attrs}>'
# Maps MIME types to rendering functions.
_MIME_TO_RENDERERS = {
"text/plain": lambda v, m: html.escape(v),
"text/html": lambda v, m: v,
"image/png": lambda v, m: _render_image("image/png", v, m),
"image/jpeg": lambda v, m: _render_image("image/jpeg", v, m),
"image/svg+xml": lambda v, m: v,
"application/json": lambda v, m: v,
"application/javascript": lambda v, m: f"<script>{v}<\\/script>",
}
# Maps Python representation methods to MIME types. This is an ordered dict
# because the order defines preference when multiple methods are available,
# and MicroPython's limited dicts don't preserve insertion order.
_METHOD_TO_MIME = OrderedDict(
[
("savefig", "image/png"),
("_repr_png_", "image/png"),
("_repr_jpeg_", "image/jpeg"),
("_repr_svg_", "image/svg+xml"),
("_repr_html_", "text/html"),
("_repr_json_", "application/json"),
("_repr_javascript_", "application/javascript"),
("__repr__", "text/plain"),
]
)
class HTML:
"""
Wrap a string to render as unescaped HTML in `display()`. This is
necessary because plain strings are automatically HTML-escaped for safety:
```python
from pyscript import HTML, display
display(HTML("<h1>Hello World</h1>"))
```
Inspired by
[`IPython.display.HTML`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.HTML).
"""
def __init__(self, html):
self._html = html
def _repr_html_(self):
return self._html
def _get_representation(obj, method):
"""
Call the given representation `method` on an object (`obj`).
Handles special cases like matplotlib's `savefig`. Returns `None`
if the `method` doesn't exist.
"""
if method == "__repr__":
return repr(obj)
if not hasattr(obj, method):
return None
if method == "savefig":
buf = io.BytesIO()
obj.savefig(buf, format="png")
buf.seek(0)
return base64.b64encode(buf.read()).decode("utf-8")
return getattr(obj, method)()
def _get_content_and_mime(obj):
"""
Returns the formatted raw content to be inserted into the DOM representing
the given object, along with the object's detected MIME type.
Returns a tuple of (html_string, mime_type).
Prefers _repr_mimebundle_ if available, otherwise tries individual
representation methods, falling back to __repr__ (with a warning in
the console).
Implements a subset of IPython's rich display system (mimebundle support,
etc...).
"""
if isinstance(obj, str):
return html.escape(obj), "text/plain"
# Prefer an object's mimebundle.
mimebundle = _get_representation(obj, "_repr_mimebundle_")
if mimebundle:
if isinstance(mimebundle, tuple):
# Grab global metadata.
format_dict, global_meta = mimebundle
else:
format_dict, global_meta = mimebundle, {}
# Try to render using mimebundle formats.
for mime_type, output in format_dict.items():
if mime_type in _MIME_TO_RENDERERS:
meta = global_meta.get(mime_type, {})
# If output is a tuple, merge format-specific metadata.
if isinstance(output, tuple):
output, format_meta = output
meta.update(format_meta)
return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
# No mimebundle or no available renderers therein, so try individual
# methods.
for method, mime_type in _METHOD_TO_MIME.items():
if mime_type not in _MIME_TO_RENDERERS:
continue
output = _get_representation(obj, method)
if output is None:
continue
meta = {}
if isinstance(output, tuple):
output, meta = output
return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
# Ultimate fallback to repr with warning.
window.console.warn(
f"Object {type(obj).__name__} has no supported representation method. "
"Using __repr__ as fallback."
)
output = repr(obj)
return html.escape(output), "text/plain"
def _write_to_dom(element, value, append):
"""
Given an `element` and a `value`, write formatted content to the referenced
DOM element. If `append` is True, content is added to the existing content;
otherwise, the existing content is replaced.
Creates a wrapper `div` when appending multiple items to preserve
structure.
"""
html_content, mime_type = _get_content_and_mime(value)
if not html_content.strip():
return
if append:
container = document.createElement("div")
element.append(container)
else:
container = element
if mime_type in ("application/javascript", "text/html"):
container.append(document.createRange().createContextualFragment(html_content))
else:
container.innerHTML = html_content
def display(*values, target=None, append=True):
"""
Display Python objects in the web page.
* `*values`: Python objects to display. Each object is introspected to
determine the appropriate rendering method.
* `target`: DOM element ID where content should be displayed. If `None`
(default), uses the current script tag's designated output area. This
can start with '#' (which will be stripped for compatibility).
* `append`: If `True` (default), add content to existing output. If
`False`, replace existing content before displaying.
When used in a worker, `display()` requires an explicit `target` parameter
to identify where content will be displayed. If used on the main thread,
it automatically uses the current `<script>` tag as the target. If the
script tag has a `target` attribute, that element will be used instead.
A ValueError is raised if a valid target cannot be found for the current
context.
```python
from pyscript import display, HTML
# Display raw HTML.
display(HTML("<h1>Hello, World!</h1>"))
# Display in current script's output area.
display("Hello, World!")
# Display in a specific element.
display("Hello", target="my-div")
# Replace existing content (note the `#`).
display("New content", target="#my-div", append=False)
# Display multiple values in the default target.
display("First", "Second", "Third")
```
"""
if isinstance(target, str):
# There's a valid target.
target = target[1:] if target.startswith("#") else target
elif is_none(target):
target = current_target()
element = document.getElementById(target)
if is_none(element):
raise ValueError(f"Cannot find element with id='{target}' in the page.")
# If possible, use a script tag's target attribute.
if element.tagName == "SCRIPT" and hasattr(element, "target"):
element = element.target
# Clear before displaying all values when not appending.
if not append:
element.replaceChildren()
# Add each value.
for value in values:
_write_to_dom(element, value, append)

View File

@@ -1,237 +0,0 @@
"""
Event handling for PyScript.
This module provides two complementary systems:
1. The `Event` class: A simple publish-subscribe pattern for custom events
within *your* Python code.
2. The `@when` decorator: Connects Python functions to browser DOM events,
or instances of the `Event` class, allowing you to respond to user
interactions like clicks, key presses and form submissions, or to custom
events defined in your Python code.
"""
import asyncio
import inspect
from functools import wraps
from pyscript.context import document
from pyscript.ffi import create_proxy, to_js
from pyscript.util import is_awaitable
class Event:
"""
A custom event that can notify multiple listeners when triggered.
Use this class to create your own event system within Python code.
Listeners can be either regular functions or async functions.
```python
from pyscript.events import Event
# Create a custom event.
data_loaded = Event()
# Add a listener.
def on_data_loaded(result):
print(f"Data loaded: {result}")
data_loaded.add_listener(on_data_loaded)
# Time passes.... trigger the event.
data_loaded.trigger({"data": 123})
```
"""
def __init__(self):
self._listeners = []
def trigger(self, result):
"""
Trigger the event and notify all listeners with the given `result`.
"""
for listener in self._listeners:
if is_awaitable(listener):
asyncio.create_task(listener(result))
else:
listener(result)
def add_listener(self, listener):
"""
Add a function to be called when this event is triggered.
The `listener` must be callable. It can be either a regular function
or an async function. Duplicate listeners are ignored.
"""
if not callable(listener):
msg = "Listener must be callable."
raise ValueError(msg)
if listener not in self._listeners:
self._listeners.append(listener)
def remove_listener(self, *listeners):
"""
Remove specified `listeners`. If none specified, remove all listeners.
"""
if listeners:
for listener in listeners:
try:
self._listeners.remove(listener)
except ValueError:
pass # Silently ignore listeners not in the list.
else:
self._listeners = []
def when(event_type, selector=None, **options):
"""
A decorator to handle DOM events or custom `Event` objects.
For DOM events, specify the `event_type` (e.g. `"click"`) and a `selector`
for target elements. For custom `Event` objects, just pass the `Event`
instance as the `event_type`. It's also possible to pass a list of `Event`
objects. The `selector` is required only for DOM events. It should be a
[CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors),
`Element`, `ElementCollection`, or list of DOM elements.
For DOM events only, you can specify optional
[addEventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options):
`capture`, `once`, `passive`, or `signal`.
The decorated function can be either a regular function or an async
function. If the function accepts an argument, it will receive the event
object (for DOM events) or the Event's result (for custom events). A
function does not need to accept any arguments if it doesn't require them.
```python
from pyscript import when, display
# Handle DOM events.
@when("click", "#my-button")
def handle_click(event):
display("Button clicked!")
# Handle DOM events with options.
@when("click", "#my-button", once=True)
def handle_click_once(event):
display("Button clicked once!")
# Handle custom events.
my_event = Event()
@when(my_event)
def handle_custom(): # No event argument needed.
display("Custom event triggered!")
# Handle multiple custom events.
another_event = Event()
def another_handler():
display("Another custom event handler.")
# Attach the same handler to multiple events but not as a decorator.
when([my_event, another_event])(another_handler)
# Trigger an Event instance from a DOM event via @when.
@when("click", "#my-button")
def handle_click(event):
another_event.trigger("Button clicked!")
# Stacked decorators also work.
@when("mouseover", "#my-div")
@when(my_event)
def handle_both(event):
display("Either mouseover or custom event triggered!")
```
"""
if isinstance(event_type, str):
# This is a DOM event to handle, so check and use the selector.
if not selector:
raise ValueError("Selector required for DOM event handling.")
elements = _get_elements(selector)
if not elements:
raise ValueError(f"No elements found for selector: {selector}")
def decorator(func):
wrapper = _create_wrapper(func)
if isinstance(event_type, Event):
# Custom Event - add listener.
event_type.add_listener(wrapper)
elif isinstance(event_type, list) and all(
isinstance(t, Event) for t in event_type
):
# List of custom Events - add listener to each.
for event in event_type:
event.add_listener(wrapper)
else:
# DOM event - attach to all matched elements.
for element in elements:
element.addEventListener(
event_type,
create_proxy(wrapper),
to_js(options) if options else False,
)
return wrapper
return decorator
def _get_elements(selector):
"""
Convert various `selector` types into a list of DOM elements.
"""
from pyscript.web import Element, ElementCollection
if isinstance(selector, str):
return list(document.querySelectorAll(selector))
elif isinstance(selector, Element):
return [selector._dom_element]
elif isinstance(selector, ElementCollection):
return [el._dom_element for el in selector]
elif isinstance(selector, list):
return selector
else:
return [selector]
def _create_wrapper(func):
"""
Create an appropriate wrapper for the given function, `func`.
The wrapper handles both sync and async functions, and respects whether
the function expects to receive event arguments.
"""
# Get the original function if it's been wrapped. This avoids wrapper
# loops when stacking decorators.
original_func = func
while hasattr(original_func, "__wrapped__"):
original_func = original_func.__wrapped__
# Inspect the original function signature.
sig = inspect.signature(original_func)
accepts_args = bool(sig.parameters)
if is_awaitable(func):
if accepts_args:
async def wrapper(event):
return await func(event)
else:
async def wrapper(*args, **kwargs):
return await func()
else:
if accepts_args:
# Always create a new wrapper function to avoid issues with
# stacked decorators getting into an infinite loop.
def wrapper(event):
return func(event)
else:
def wrapper(*args, **kwargs):
return func()
return wraps(func)(wrapper)

View File

@@ -1,218 +0,0 @@
"""
This module provides a Python-friendly interface to the
[browser's fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
returning native Python data types and supporting directly awaiting the promise
and chaining method calls directly on the promise.
```python
from pyscript.fetch import fetch
url = "https://api.example.com/data"
# Pattern 1: Await the response, then extract data.
response = await fetch(url)
if response.ok:
data = await response.json()
else:
raise NetworkError(f"Fetch failed: {response.status}")
# Pattern 2: Chain method calls directly on the promise.
data = await fetch(url).json()
```
"""
import json
import js
from pyscript.util import as_bytearray
class _FetchResponse:
"""
Wraps a JavaScript Response object with Pythonic data extraction methods.
This wrapper ensures that data returned from fetch is, if possible, in
native Python types rather than JavaScript types.
"""
def __init__(self, response):
self._response = response
def __getattr__(self, attr):
"""
Provide access to underlying Response properties like ok, status, etc.
"""
return getattr(self._response, attr)
async def arrayBuffer(self):
"""
Get response body as a buffer (memoryview or bytes).
Returns a memoryview in MicroPython or bytes in Pyodide, representing
the raw binary data.
"""
buffer = await self._response.arrayBuffer()
if hasattr(buffer, "to_py"):
# Pyodide conversion.
return buffer.to_py()
# MicroPython conversion.
return memoryview(as_bytearray(buffer))
async def blob(self):
"""
Get response body as a JavaScript Blob object.
Returns the raw JS Blob for use with other JS APIs.
"""
return await self._response.blob()
async def bytearray(self):
"""
Get response body as a Python bytearray.
Returns a mutable bytearray containing the response data.
"""
buffer = await self._response.arrayBuffer()
return as_bytearray(buffer)
async def json(self):
"""
Parse response body as JSON and return Python objects.
Returns native Python dicts, lists, strings, numbers, etc.
"""
return json.loads(await self.text())
async def text(self):
"""
Get response body as a text string.
"""
return await self._response.text()
class _FetchPromise:
"""
Wraps the fetch promise to enable direct method chaining.
This allows calling response methods directly on the fetch promise:
`await fetch(url).json()` instead of requiring two separate awaits.
This feels more Pythonic since it matches typical usage patterns
Python developers have got used to via libraries like `requests`.
"""
def __init__(self, promise):
self._promise = promise
# To be resolved in the future via the setup() static method.
promise._response = None
# Add convenience methods directly to the promise.
promise.arrayBuffer = self.arrayBuffer
promise.blob = self.blob
promise.bytearray = self.bytearray
promise.json = self.json
promise.text = self.text
@staticmethod
def setup(promise, response):
"""
Store the resolved response on the promise for later access.
"""
promise._response = _FetchResponse(response)
return promise._response
async def _get_response(self):
"""
Get the cached response, or await the promise if not yet resolved.
"""
if not self._promise._response:
await self._promise
return self._promise._response
async def arrayBuffer(self):
response = await self._get_response()
return await response.arrayBuffer()
async def blob(self):
response = await self._get_response()
return await response.blob()
async def bytearray(self):
response = await self._get_response()
return await response.bytearray()
async def json(self):
response = await self._get_response()
return await response.json()
async def text(self):
response = await self._get_response()
return await response.text()
def fetch(url, **options):
"""
Fetch a resource from the network using a Pythonic interface.
This wraps JavaScript's fetch API, returning Python-native data types
and supporting both direct promise awaiting and method chaining.
The function takes a `url` and optional fetch `options` as keyword
arguments. The `options` correspond to the JavaScript fetch API's
[RequestInit dictionary](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit),
and commonly include:
- `method`: HTTP method (e.g., `"GET"`, `"POST"`, `"PUT"` etc.)
- `headers`: Dict of request headers.
- `body`: Request body (string, dict for JSON, etc.)
The function returns a promise that resolves to a Response-like object
with Pythonic methods to extract data:
- `await response.json()` to get JSON as Python objects.
- `await response.text()` to get text data.
- `await response.bytearray()` to get raw data as a bytearray.
- `await response.arrayBuffer()` to get raw data as a memoryview or bytes.
- `await response.blob()` to get the raw JS Blob object.
It's also possible to chain these methods directly on the fetch promise:
`data = await fetch(url).json()`
The returned response object also exposes standard properties like
`ok`, `status`, and `statusText` for checking response status.
```python
# Simple GET request.
response = await fetch("https://api.example.com/data")
data = await response.json()
# Method chaining.
data = await fetch("https://api.example.com/data").json()
# POST request with JSON.
response = await fetch(
"https://api.example.com/users",
method="POST",
headers={"Content-Type": "application/json"},
body=json.dumps({"name": "Alice"})
)
result = await response.json()
# Check response status codes.
response = await fetch("https://api.example.com/data")
if response.ok:
# Status in the range 200-299.
data = await response.json()
elif response.status == 404:
print("Resource not found")
else:
print(f"Error: {response.status} {response.statusText}")
```
"""
# Convert Python dict to JavaScript object.
js_options = js.JSON.parse(json.dumps(options))
# Setup response handler to wrap the result.
def on_response(response, *_):
return _FetchPromise.setup(promise, response)
promise = js.fetch(url, js_options).then(on_response)
_FetchPromise(promise)
return promise

View File

@@ -1,164 +0,0 @@
"""
This module provides a unified
[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface)
layer for Python/JavaScript interactions, that works consistently across both
Pyodide and MicroPython, and in a worker or main thread context, abstracting
away the differences in their JavaScript interop APIs.
The following utilities work on both the main thread and in worker contexts:
- `create_proxy`: Create a persistent JavaScript proxy of a Python function.
- `to_js`: Convert Python objects to JavaScript objects.
- `is_none`: Check if a value is Python `None` or JavaScript `null`.
- `assign`: Merge objects (like JavaScript's `Object.assign`).
The following utilities are specific to worker contexts:
- `direct`: Mark objects for direct JavaScript access.
- `gather`: Collect multiple values from worker contexts.
- `query`: Query objects in worker contexts.
More details of the `direct`, `gather`, and `query` utilities
[can be found here](https://github.com/WebReflection/reflected-ffi?tab=readme-ov-file#remote-extra-utilities).
"""
try:
# Attempt to import Pyodide's FFI utilities.
import js
from pyodide.ffi import create_proxy as _cp
from pyodide.ffi import to_js as _py_tjs
from pyodide.ffi import jsnull
from_entries = js.Object.fromEntries
def _to_js_wrapper(value, **kw):
if "dict_converter" not in kw:
kw["dict_converter"] = from_entries
return _py_tjs(value, **kw)
except:
# Fallback to jsffi for MicroPython.
from jsffi import create_proxy as _cp
from jsffi import to_js as _to_js_wrapper
import js
jsnull = js.Object.getPrototypeOf(js.Object.prototype)
def create_proxy(func):
"""
Create a persistent JavaScript proxy of a Python function.
This proxy allows JavaScript code to call the Python function
seamlessly, maintaining the correct context and argument handling.
This is especially useful when passing Python functions as callbacks
to JavaScript APIs (without `create_proxy`, the function would be
garbage collected after the declaration of the callback).
```python
from pyscript import ffi
from pyscript import document
my_button = document.getElementById("my-button")
def py_callback(x):
print(f"Callback called with {x}")
my_button.addEventListener("click", ffi.create_proxy(py_callback))
```
"""
return _cp(func)
def to_js(value, **kw):
"""
Convert Python objects to JavaScript objects.
This ensures a Python `dict` becomes a
[proper JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
rather a JavaScript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map),
which is more intuitive for most use cases.
Where required, the underlying `to_js` uses `Object.fromEntries` for
`dict` conversion.
```python
from pyscript import ffi
import js
note = {
"body": "This is a notification",
"icon": "icon.png"
}
js.Notification.new("Hello!", ffi.to_js(note))
```
"""
return _to_js_wrapper(value, **kw)
def is_none(value):
"""
Check if a value is `None` or JavaScript `null`.
In Pyodide, JavaScript `null` is represented by the `jsnull` object,
so we check for both Python `None` and `jsnull`. This function ensures
consistent behavior across Pyodide and MicroPython for null-like
values.
```python
from pyscript import ffi
import js
val1 = None
val2 = js.null
val3 = 42
print(ffi.is_none(val1)) # True
print(ffi.is_none(val2)) # True
print(ffi.is_none(val3)) # False
```
"""
return value is None or value is jsnull
try:
# Worker context utilities from reflected-ffi.
# See https://github.com/WebReflection/reflected-ffi for more details.
from polyscript import ffi as _ffi
_assign = _ffi.assign
direct = _ffi.direct
gather = _ffi.gather
query = _ffi.query
except:
# Fallback implementations for main thread context.
import js
_assign = js.Object.assign
direct = lambda source: source
def assign(source, *args):
"""
Merge JavaScript objects (like
[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)).
Takes a target object and merges properties from one or more source
objects into it, returning the modified target.
```python
obj = js.Object.new()
ffi.assign(obj, {"a": 1}, {"b": 2})
# obj now has properties a=1 and b=2
```
"""
for arg in args:
_assign(source, to_js(arg))
return source

Some files were not shown because too many files have changed in this diff Show More