Compare commits

..

11 Commits

Author SHA1 Message Date
Fabio Pliger
20e8c00f79 more cleaning 2023-08-25 18:07:10 -05:00
Fabio Pliger
b1aa4d345b remove attributes in Style that were mapping directly to JS API 2023-08-25 17:59:40 -05:00
Fabio Pliger
6f516ed4d1 remove other unused code 2023-08-25 17:58:08 -05:00
Fabio Pliger
04d117c12d clean up non used old code during experimentation 2023-08-25 17:54:07 -05:00
Fabio Pliger
579e7ab87a add initial pydom code, raw from experimentation 2023-08-25 17:51:45 -05:00
Fabio Pliger
10e497c753 Merge branch 'antocuni/tmp-next-stdlib' into fpliger/stdlib 2023-08-25 17:39:22 -05:00
Fabio Pliger
3b46609614 add the python folder to src, where we can collect all python resources 2023-08-25 11:19:26 -05:00
Antonio Cuni
320ca306bd introduce the _pyscript python package, and move the code for display inside. This is probably incomplete, see the XXX inside the comments 2023-08-25 18:12:01 +02:00
Fabio Pliger
e087deef09 account for new display.py location 2023-08-24 12:56:02 -05:00
Fabio Pliger
c3bac976c8 move display.py to a stdlib folder 2023-08-24 12:42:03 -05:00
Fabio Pliger
4c3e5fabb9 move display.py to a stdlib folder 2023-08-24 12:41:51 -05:00
561 changed files with 42654 additions and 14442 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
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
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
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
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
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
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

@@ -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:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v4
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,20 @@ 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: 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: Build and Test
run: make test
- name: Zip dist folder
run: zip -r -q ./build.zip ./dist
- 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:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v4
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,22 +40,18 @@ 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: Generate index.html in snapshot
working-directory: .
run: sed -e 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' -e 's#_DOC_VERSION_#${{ github.ref_name }}#' -e 's#_TAG_VERSION_#/tag/${{ github.ref_name }}#' -e 's#_VERSION_#${{ github.ref_name }}#' ./public/index.html > ./core/dist/index.html
- name: Generate release.tar from snapshot and put it in dist/
working-directory: .
run: tar -cvf ../release.tar * && mv ../release.tar .
- name: Build and Test
run: make test
# Upload to S3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
@@ -74,5 +59,5 @@ jobs:
- name: Sync to S3
run:
| # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
aws s3 sync --quiet ./dist/ s3://pyscript.net/latest/
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/
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:
snapshot:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v4
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@v4
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@v4
- name: Install node
uses: actions/setup-node@v4
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@v4
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
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

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

@@ -0,0 +1,74 @@
name: "[CI] Test Next"
on:
push: # Only run on merges into main that modify files under pyscriptjs/ and examples/
branches:
- next
paths:
- pyscript.core/**
- .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/**
- .github/workflows/test-next.yml # Test that workflow works when changed
workflow_dispatch:
jobs:
TestNext:
runs-on: ubuntu-latest
defaults:
run:
working-directory: pyscript.core
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 20.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 }}-
# TODO: this will likely change soon to pyscript.next
# - name: install next deps
# working-directory: pyscript.core
# run: npm i; npx playwright install
# - name: build next
# working-directory: pyscript.core
# run: npm run build
# - name: Run next tests
# working-directory: pyscript.core
# run: npm run test
# TODO: DO we want to upload next yet?
# - 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

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@v4
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@v4
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
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,13 +1,13 @@
# 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: v5.0.0
rev: v4.4.0
hooks:
- id: check-builtin-literals
- id: check-case-conflict
@@ -16,22 +16,27 @@ repos:
- id: check-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: pyscript\.core/core.*|\.min\.js$
- id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.257
hooks:
- id: ruff
exclude: pyscript\.core/test|pyscript\.core/src/display.py
args: [--fix]
- repo: https://github.com/psf/black
rev: 24.10.0
rev: 23.1.0
hooks:
- id: black
args: ["-l", "88", "--skip-string-normalization"]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.2.4
hooks:
- id: codespell # See 'pyproject.toml' for args
exclude: \.js\.map$
@@ -42,5 +47,17 @@ repos:
rev: "v3.0.0-alpha.6"
hooks:
- id: prettier
exclude: core/test|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
exclude: pyscript\.core/test|pyscript\.core/core.*|pyscript\.core/types/|pyscript\.sw/
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,3 +1,5 @@
ISSUE_TEMPLATE
*.min.*
package-lock.json
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,11 +186,7 @@
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
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,90 +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 install && cd ..
ifeq ($(VIRTUAL_ENV),)
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv 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:
cd core && npx playwright install chromium && npm run build
# Update the dependencies.
update:
cd core && npm update && cd ..
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
# 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)

103
README.md
View File

@@ -1,85 +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/snapshots/2024.9.2/core.css"
/>
<script
type="module"
src="https://pyscript.net/snapshots/2024.9.2/core.js"
></script>
</head>
<body>
<!-- Use MicroPython to evaluate 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.
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,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";

3943
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,107 +0,0 @@
{
"name": "@pyscript/core",
"version": "0.6.7",
"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"
},
"./css": {
"import": "./dist/core.css"
},
"./storage": {
"import": "./dist/storage.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 ts && 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: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 tests/py_tests.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",
"add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6",
"polyscript": "^0.16.3",
"sabayon": "^0.5.2",
"sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7"
},
"devDependencies": {
"@codemirror/commands": "^6.7.0",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.3",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.1",
"@playwright/test": "1.45.3",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"bun": "^1.1.30",
"chokidar": "^4.0.1",
"codedent": "^0.1.2",
"codemirror": "^6.0.1",
"eslint": "^9.12.0",
"flatted": "^3.3.1",
"rollup": "^4.24.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3",
"string-width": "^7.2.0",
"typescript": "^5.6.3",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.1"
},
"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,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-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@${v("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,56 +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,
},
},
];

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,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,7 +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.

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.2.
* Original file: /npm/xterm@5.3.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,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.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.1 and Terser v5.19.2.
* 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.1 and Terser v5.19.2.
* 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,17 +0,0 @@
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 } = Promise.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,160 +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}}
*/
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,
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");
} else if (attrConfigs.length) {
[pyElement, ...attrConfigs] = attrConfigs;
config = pyElement.getAttribute("config");
// 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 { parse } = await import(
/* webpackIgnore: true */ "./3rd-party/toml.js"
);
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
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()));
}
}
// assign plugins as Promise.all only if needed
plugins = Promise.all(toBeAwaited);
configs.set(TYPE, { config: parsed, configURL, plugins, error });
}
export { configs, relative_url };

View File

@@ -1,75 +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;
right: 0.5rem;
bottom: 0.5rem;
opacity: 0;
transition: opacity 0.25s;
z-index: 1;
}
.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:disabled,
.mpy-editor-run-button:focus,
.mpy-editor-run-button:disabled {
opacity: 1;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.py-editor-run-button:disabled > *,
.mpy-editor-run-button:disabled > * {
display: none; /* hide all the child elements of the run button when it is disabled */
}
.py-editor-run-button:disabled,
.mpy-editor-run-button:disabled {
border-width: 0;
}
.py-editor-run-button:disabled::before,
.mpy-editor-run-button:disabled::before {
content: "";
box-sizing: border-box;
position: absolute;
top: 100%;
left: 100%;
width: 20px;
height: 20px;
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
border-radius: 50%;
border: 2px solid #aaa;
border-top-color: #000;
background-color: #fff;
animation: spinner 0.6s linear infinite;
}

View File

@@ -1,363 +0,0 @@
/*! (c) PyScript Development Team */
import stickyModule from "sticky-module";
import "@ungap/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 { 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");
};
const { 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,
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);
define(TYPE, {
config,
configURL,
interpreter,
hooks,
env: `${TYPE}-script`,
version: offline_interpreter(config),
onerror(error, element) {
errors.set(element, error);
},
});
customElements.define(
`${TYPE}-script`,
class extends HTMLElement {
constructor() {
assign(super(), {
_wrap: Promise.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,105 +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(),
/** @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,28 +0,0 @@
// ⚠️ This file is an artifact: DO NOT MODIFY
export default {
["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-terminal"]: () =>
import(
/* webpackIgnore: true */
"./plugins/py-terminal.js"
),
};

View File

@@ -1,27 +0,0 @@
// PyScript Derepcations Plugin
import { hooks } from "../core.js";
import { notify } from "./error.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,113 +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 }) => {
const args = persistent ? ["globals()", "__locals__"] : ["{}", "{}"];
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),
);
}
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,430 +0,0 @@
// PyScript py-editor plugin
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
import { notify } from "./error.js";
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
let id = 0;
const getID = (type) => `${type}-editor-${id++}`;
const envs = new Map();
const configs = new Map();
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;
};
async function execute({ currentTarget }) {
const { env, pySrc, outDiv } = this;
const hasRunButton = !!currentTarget;
if (hasRunButton) {
currentTarget.disabled = true;
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()) {
const editor = currentTarget.closest(`.${type}-editor-box`);
const script = editor?.parentNode?.previousElementSibling;
if (script) {
defineProperties(script, { xworker: { value: xworker } });
break;
}
}
}
const { sync } = xworker;
const { promise, resolve } = Promise.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.disabled = false;
};
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 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) => {
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([
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
import(
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
]);
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 });
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([
...defaultKeymap,
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
{ key: "Cmd-Enter", run: listener, preventDefault: true },
{ key: "Shift-Enter", run: listener, preventDefault: true },
// @see https://codemirror.net/examples/tab/
indentWithTab,
]),
basicSetup,
],
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
parent,
doc,
});
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,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,252 +0,0 @@
// PyScript pyodide terminal plugin
import { hooks, inputFailure } from "../../core.js";
import { defineProperties } from "polyscript/exports";
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));
};
// 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",
};
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 = Promise.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,179 +0,0 @@
// PyScript py-terminal plugin
import { hooks } from "../../core.js";
import { defineProperties } from "polyscript/exports";
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",
});
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);
} 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,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,51 +0,0 @@
# Some notes about the naming conventions and the relationship between various
# similar-but-different names.
#
# import pyscript
# this 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 file)
#
# import _pyscript
# this is an internal module implemented in JS. It is used internally by
# the pyscript package, end users should not use it directly. For its
# implementation, grep for `interpreter.registerJsModule("_pyscript",
# ...)` in core.js
#
# import js
# this is the JS globalThis, as exported by pyodide and/or micropython's
# FFIs. As such, it contains different things in the main thread or in a
# worker.
#
# import pyscript.magic_js
# this submodule abstracts away some of the differences between the main
# thread and the worker. 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 the worker they are proxies which work
# thanks to coincident.
#
# from pyscript import window, document
# these are just the window and document objects as defined by
# pyscript.magic_js. 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.event_handling import when
from pyscript.display import HTML, display
from pyscript.fetch import fetch
from pyscript.magic_js import (
RUNNING_IN_WORKER,
PyWorker,
config,
current_target,
document,
js_import,
js_modules,
sync,
window,
)
from pyscript.storage import Storage, storage
from pyscript.websocket import WebSocket
if not RUNNING_IN_WORKER:
from pyscript.workers import create_named_worker, workers

View File

@@ -1,109 +0,0 @@
import inspect
try:
from pyodide.ffi.wrappers import add_event_listener
except ImportError:
def add_event_listener(el, event_type, func):
el.addEventListener(event_type, func)
from pyscript.magic_js import document
def when(target, *args, **kwargs):
"""
A decorator and function for attaching event handlers to DOM elements or
whenable objects.
When used as a decorator, the target is the object that will trigger the
event. The handler function is the decorated function. The handler function
will be called when the target is triggered.
When used as a function, the target is the object that will trigger the
event. The handler function is the next argument. The remaining arguments
and keyword arguments are passed to the target when it is triggered.
"""
# If "when" is called as a function, try to grab the handler from the
# arguments. If there's no handler, this must be a decorator based call.
handler = None
if args and callable(args[0]):
handler = args[0]
args = args[1:]
elif callable(kwargs.get("handler")):
handler = kwargs.pop("handler")
# Does the target implement the when protocol?
whenable = hasattr(target, "__when__")
# If not when-able, the DOM selector for the target event.
if not whenable:
# The target is an event linked to a DOM selector. Extract the
# selector from the arguments or keyword arguments.
if args:
selector = args[0]
elif kwargs:
selector = kwargs.get("selector")
if not selector:
# There must be a selector if the target is not when-able.
raise ValueError("No selector provided.")
# Grab the DOM elements to which the target event will be attached.
from pyscript.web import Element, ElementCollection
if isinstance(selector, str):
elements = document.querySelectorAll(selector)
# TODO: This is a hack that will be removed when pyscript becomes a package
# and we can better manage the imports without circular dependencies
elif isinstance(selector, Element):
elements = [selector._dom_element]
elif isinstance(selector, ElementCollection):
elements = [el._dom_element for el in selector]
else:
if isinstance(selector, list):
elements = selector
else:
elements = [selector]
def decorator(func):
try:
sig = inspect.signature(func)
# Function doesn't receive events
if not sig.parameters:
# Function is async: must be awaited
if inspect.iscoroutinefunction(func):
async def wrapper(*args, **kwargs):
await func()
else:
def wrapper(*args, **kwargs):
func()
else:
wrapper = func
except AttributeError:
# TODO: this is very ugly hack to get micropython working because inspect.signature
# doesn't exist, but we need to actually properly replace inspect.signature.
# It may be actually better to not try any magic for now and raise the error
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TypeError as e:
if "takes" in str(e) and "positional arguments" in str(e):
return func()
raise
if whenable:
target.__when__(wrapper, *args, **kwargs)
else:
for el in elements:
add_event_listener(el, target, wrapper)
return func
if handler:
decorator(handler)
else:
return decorator

View File

@@ -1,87 +0,0 @@
import json
import js
from pyscript.util import as_bytearray
### wrap the response to grant Pythonic results
class _Response:
def __init__(self, response):
self._response = response
# grant access to response.ok and other fields
def __getattr__(self, attr):
return getattr(self._response, attr)
# exposed methods with Pythonic results
async def arrayBuffer(self):
buffer = await self._response.arrayBuffer()
# works in Pyodide
if hasattr(buffer, "to_py"):
return buffer.to_py()
# shims in MicroPython
return memoryview(as_bytearray(buffer))
async def blob(self):
return await self._response.blob()
async def bytearray(self):
buffer = await self._response.arrayBuffer()
return as_bytearray(buffer)
async def json(self):
return json.loads(await self.text())
async def text(self):
return await self._response.text()
### allow direct await to _Response methods
class _DirectResponse:
@staticmethod
def setup(promise, response):
promise._response = _Response(response)
return promise._response
def __init__(self, promise):
self._promise = promise
promise._response = None
promise.arrayBuffer = self.arrayBuffer
promise.blob = self.blob
promise.bytearray = self.bytearray
promise.json = self.json
promise.text = self.text
async def _response(self):
if not self._promise._response:
await self._promise
return self._promise._response
async def arrayBuffer(self):
response = await self._response()
return await response.arrayBuffer()
async def blob(self):
response = await self._response()
return await response.blob()
async def bytearray(self):
response = await self._response()
return await response.bytearray()
async def json(self):
response = await self._response()
return await response.json()
async def text(self):
response = await self._response()
return await response.text()
def fetch(url, **kw):
# workaround Pyodide / MicroPython dict <-> js conversion
options = js.JSON.parse(json.dumps(kw))
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
promise = js.fetch(url, options).then(awaited)
_DirectResponse(promise)
return promise

View File

@@ -1,18 +0,0 @@
try:
import js
from pyodide.ffi import create_proxy as _cp
from pyodide.ffi import to_js as _py_tjs
from_entries = js.Object.fromEntries
def _tjs(value, **kw):
if not hasattr(kw, "dict_converter"):
kw["dict_converter"] = from_entries
return _py_tjs(value, **kw)
except:
from jsffi import create_proxy as _cp
from jsffi import to_js as _tjs
create_proxy = _cp
to_js = _tjs

View File

@@ -1,148 +0,0 @@
# https://www.npmjs.com/package/flatted
import json as _json
class _Known:
def __init__(self):
self.key = []
self.value = []
class _String:
def __init__(self, value):
self.value = value
def _array_keys(value):
keys = []
i = 0
for _ in value:
keys.append(i)
i += 1
return keys
def _object_keys(value):
keys = []
for key in value:
keys.append(key)
return keys
def _is_array(value):
return isinstance(value, list) or isinstance(value, tuple)
def _is_object(value):
return isinstance(value, dict)
def _is_string(value):
return isinstance(value, str)
def _index(known, input, value):
input.append(value)
index = str(len(input) - 1)
known.key.append(value)
known.value.append(index)
return index
def _loop(keys, input, known, output):
for key in keys:
value = output[key]
if isinstance(value, _String):
_ref(key, input[int(value.value)], input, known, output)
return output
def _ref(key, value, input, known, output):
if _is_array(value) and not value in known:
known.append(value)
value = _loop(_array_keys(value), input, known, value)
elif _is_object(value) and not value in known:
known.append(value)
value = _loop(_object_keys(value), input, known, value)
output[key] = value
def _relate(known, input, value):
if _is_string(value) or _is_array(value) or _is_object(value):
try:
return known.value[known.key.index(value)]
except:
return _index(known, input, value)
return value
def _transform(known, input, value):
if _is_array(value):
output = []
for val in value:
output.append(_relate(known, input, val))
return output
if _is_object(value):
obj = {}
for key in value:
obj[key] = _relate(known, input, value[key])
return obj
return value
def _wrap(value):
if _is_string(value):
return _String(value)
if _is_array(value):
i = 0
for val in value:
value[i] = _wrap(val)
i += 1
elif _is_object(value):
for key in value:
value[key] = _wrap(value[key])
return value
def parse(value, *args, **kwargs):
json = _json.loads(value, *args, **kwargs)
wrapped = []
for value in json:
wrapped.append(_wrap(value))
input = []
for value in wrapped:
if isinstance(value, _String):
input.append(value.value)
else:
input.append(value)
value = input[0]
if _is_array(value):
return _loop(_array_keys(value), input, [value], value)
if _is_object(value):
return _loop(_object_keys(value), input, [value], value)
return value
def stringify(value, *args, **kwargs):
known = _Known()
input = []
output = []
i = int(_index(known, input, value))
while i < len(input):
output.append(_transform(known, input, input[i]))
i += 1
return _json.dumps(output, *args, **kwargs)

View File

@@ -1,79 +0,0 @@
import json
import sys
import js as globalThis
from polyscript import config as _config
from polyscript import js_modules
from pyscript.util import NotSupported
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
config = json.loads(globalThis.JSON.stringify(_config))
if "MicroPython" in sys.version:
config["type"] = "mpy"
else:
config["type"] = "py"
# allow `from pyscript.js_modules.xxx import yyy`
class JSModule:
def __init__(self, name):
self.name = name
def __getattr__(self, field):
# avoid pyodide looking for non existent fields
if not field.startswith("_"):
return getattr(getattr(js_modules, self.name), field)
# generate N modules in the system that will proxy the real value
for name in globalThis.Reflect.ownKeys(js_modules):
sys.modules[f"pyscript.js_modules.{name}"] = JSModule(name)
sys.modules["pyscript.js_modules"] = js_modules
if RUNNING_IN_WORKER:
import polyscript
PyWorker = NotSupported(
"pyscript.PyWorker",
"pyscript.PyWorker works only when running in the main thread",
)
try:
import js
window = polyscript.xworker.window
document = window.document
js.document = document
# this is the same as js_import on main and it lands modules on main
js_import = window.Function(
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
)()
except:
message = "Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
globalThis.console.warn(message)
window = NotSupported("pyscript.window", message)
document = NotSupported("pyscript.document", message)
js_import = None
sync = polyscript.xworker.sync
# in workers the display does not have a default ID
# but there is a sync utility from xworker
def current_target():
return polyscript.target
else:
import _pyscript
from _pyscript import PyWorker, js_import
window = globalThis
document = globalThis.document
sync = NotSupported(
"pyscript.sync", "pyscript.sync works only when running in a worker"
)
# in MAIN the current element target exist, just use it
def current_target():
return _pyscript.target

View File

@@ -1,91 +0,0 @@
from pyscript import window
from pyscript.ffi import to_js
class Device:
"""Device represents a media input or output device, such as a microphone,
camera, or headset.
"""
def __init__(self, device):
self._dom_element = device
@property
def id(self):
return self._dom_element.deviceId
@property
def group(self):
return self._dom_element.groupId
@property
def kind(self):
return self._dom_element.kind
@property
def label(self):
return self._dom_element.label
def __getitem__(self, key):
return getattr(self, key)
@classmethod
async def load(cls, audio=False, video=True):
"""Load the device stream."""
options = window.Object.new()
options.audio = audio
if isinstance(video, bool):
options.video = video
else:
# TODO: Think this can be simplified but need to check it on the pyodide side
# TODO: this is pyodide specific. shouldn't be!
options.video = window.Object.new()
for k in video:
setattr(options.video, k, to_js(video[k]))
stream = await window.navigator.mediaDevices.getUserMedia(options)
return stream
async def get_stream(self):
key = self.kind.replace("input", "").replace("output", "")
options = {key: {"deviceId": {"exact": self.id}}}
return await self.load(**options)
async def list_devices() -> list[dict]:
"""
Return the list of the currently available media input and output devices,
such as microphones, cameras, headsets, and so forth.
Output:
list(dict) - list of dictionaries representing the available media devices.
Each dictionary has the following keys:
* deviceId: a string that is an identifier for the represented device
that is persisted across sessions. It is un-guessable by other
applications and unique to the origin of the calling application.
It is reset when the user clears cookies (for Private Browsing, a
different identifier is used that is not persisted across sessions).
* groupId: a string that is a group identifier. Two devices have the same
group identifier if they belong to the same physical device — for
example a monitor with both a built-in camera and a microphone.
* kind: an enumerated value that is either "videoinput", "audioinput"
or "audiooutput".
* label: a string describing this device (for example "External USB
Webcam").
Note: the returned list will omit any devices that are blocked by the document
Permission Policy: microphone, camera, speaker-selection (for output devices),
and so on. Access to particular non-default devices is also gated by the
Permissions API, and the list will omit devices for which the user has not
granted explicit permission.
"""
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
return [
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
]

View File

@@ -1,60 +0,0 @@
from polyscript import storage as _storage
from pyscript.flatted import parse as _parse
from pyscript.flatted import stringify as _stringify
# convert a Python value into an IndexedDB compatible entry
def _to_idb(value):
if value is None:
return _stringify(["null", 0])
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
return _stringify(["generic", value])
if isinstance(value, bytearray):
return _stringify(["bytearray", [v for v in value]])
if isinstance(value, memoryview):
return _stringify(["memoryview", [v for v in value]])
raise TypeError(f"Unexpected value: {value}")
# convert an IndexedDB compatible entry into a Python value
def _from_idb(value):
(
kind,
result,
) = _parse(value)
if kind == "null":
return None
if kind == "generic":
return result
if kind == "bytearray":
return bytearray(result)
if kind == "memoryview":
return memoryview(bytearray(result))
return value
class Storage(dict):
def __init__(self, store):
super().__init__({k: _from_idb(v) for k, v in store.entries()})
self.__store__ = store
def __delitem__(self, attr):
self.__store__.delete(attr)
super().__delitem__(attr)
def __setitem__(self, attr, value):
self.__store__.set(attr, _to_idb(value))
super().__setitem__(attr, value)
def clear(self):
self.__store__.clear()
super().clear()
async def sync(self):
await self.__store__.sync()
async def storage(name="", storage_class=Storage):
if not name:
raise ValueError("The storage name must be defined")
return storage_class(await _storage(f"@pyscript/{name}"))

View File

@@ -1,33 +0,0 @@
import js
def as_bytearray(buffer):
ui8a = js.Uint8Array.new(buffer)
size = ui8a.length
ba = bytearray(size)
for i in range(0, size):
ba[i] = ui8a[i]
return ba
class NotSupported:
"""
Small helper that raises exceptions if you try to get/set any attribute on
it.
"""
def __init__(self, name, error):
object.__setattr__(self, "name", name)
object.__setattr__(self, "error", error)
def __repr__(self):
return f"<NotSupported {self.name} [{self.error}]>"
def __getattr__(self, attr):
raise AttributeError(self.error)
def __setattr__(self, attr, value):
raise AttributeError(self.error)
def __call__(self, *args):
raise TypeError(self.error)

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +0,0 @@
import js
from pyscript.ffi import create_proxy
from pyscript.util import as_bytearray
code = "code"
protocols = "protocols"
reason = "reason"
methods = ["onclose", "onerror", "onmessage", "onopen"]
class EventMessage:
def __init__(self, event):
self._event = event
def __getattr__(self, attr):
value = getattr(self._event, attr)
if attr == "data" and not isinstance(value, str):
if hasattr(value, "to_py"):
return value.to_py()
# shims in MicroPython
return memoryview(as_bytearray(value))
return value
class WebSocket(object):
CONNECTING = 0
OPEN = 1
CLOSING = 2
CLOSED = 3
def __init__(self, **kw):
url = kw["url"]
if protocols in kw:
socket = js.WebSocket.new(url, kw[protocols])
else:
socket = js.WebSocket.new(url)
object.__setattr__(self, "_ws", socket)
for t in methods:
if t in kw:
# Pyodide fails at setting socket[t] directly
setattr(socket, t, create_proxy(kw[t]))
def __getattr__(self, attr):
return getattr(self._ws, attr)
def __setattr__(self, attr, value):
if attr in methods:
m = lambda e: value(EventMessage(e))
setattr(self._ws, attr, create_proxy(m))
else:
setattr(self._ws, attr, value)
def close(self, **kw):
if code in kw and reason in kw:
self._ws.close(kw[code], kw[reason])
elif code in kw:
self._ws.close(kw[code])
else:
self._ws.close()
def send(self, data):
if isinstance(data, str):
self._ws.send(data)
else:
buffer = js.Uint8Array.new(len(data))
for pos, b in enumerate(data):
buffer[pos] = b
self._ws.send(buffer)

View File

@@ -1,43 +0,0 @@
import js as _js
from polyscript import workers as _workers
_get = _js.Reflect.get
def _set(script, name, value=""):
script.setAttribute(name, value)
# this solves an inconsistency between Pyodide and MicroPython
# @see https://github.com/pyscript/pyscript/issues/2106
class _ReadOnlyProxy:
def __getitem__(self, name):
return _get(_workers, name)
def __getattr__(self, name):
return _get(_workers, name)
workers = _ReadOnlyProxy()
async def create_named_worker(src="", name="", config=None, type="py"):
from json import dumps
if not src:
raise ValueError("Named workers require src")
if not name:
raise ValueError("Named workers require a name")
s = _js.document.createElement("script")
s.type = type
s.src = src
_set(s, "worker")
_set(s, "name", name)
if config:
_set(s, "config", isinstance(config, str) and config or dumps(config))
_js.document.body.append(s)
return await workers[name]

View File

@@ -1,72 +0,0 @@
import { ArrayBuffer, TypedArray } from "sabayon/shared";
import IDBMapSync from "@webreflection/idb-map/sync";
import { parse, stringify } from "flatted";
const to_idb = (value) => {
if (value == null) return stringify(["null", 0]);
/* eslint-disable no-fallthrough */
switch (typeof value) {
case "object": {
if (value instanceof TypedArray)
return stringify(["memoryview", [...value]]);
if (value instanceof ArrayBuffer)
return stringify(["bytearray", [...new Uint8Array(value)]]);
}
case "string":
case "number":
case "boolean":
return stringify(["generic", value]);
default:
throw new TypeError(`Unexpected value: ${String(value)}`);
}
};
const from_idb = (value) => {
const [kind, result] = parse(value);
if (kind === "null") return null;
if (kind === "generic") return result;
if (kind === "bytearray") return new Uint8Array(value).buffer;
if (kind === "memoryview") return new Uint8Array(value);
return value;
};
// this export simulate pyscript.storage exposed in the Python world
export const storage = async (name) => {
if (!name) throw new SyntaxError("The storage name must be defined");
const store = new IDBMapSync(`@pyscript/${name}`);
const map = new Map();
await store.sync();
for (const [k, v] of store.entries()) map.set(k, from_idb(v));
const clear = () => {
map.clear();
store.clear();
};
const sync = async () => {
await store.sync();
};
return new Proxy(map, {
ownKeys: (map) => [...map.keys()],
has: (map, name) => map.has(name),
get: (map, name) => {
if (name === "clear") return clear;
if (name === "sync") return sync;
return map.get(name);
},
set: (map, name, value) => {
map.set(name, value);
store.set(name, to_idb(value));
return true;
},
deleteProperty: (map, name) => {
if (map.has(name)) {
map.delete(name);
store.delete(name);
}
return true;
},
});
};

View File

@@ -1,12 +0,0 @@
export default {
// allow pyterminal checks to bootstrap
is_pyterminal: () => false,
/**
* 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads.
* @param {number} seconds The number of seconds to sleep.
*/
sleep(seconds) {
return new Promise(($) => setTimeout($, seconds * 1000));
},
};

View File

@@ -1,4 +0,0 @@
export default new Map([
["py", "pyodide"],
["mpy", "micropython"],
]);

View File

@@ -1,49 +0,0 @@
# PyScript Test Suite
There are three aspects to our test suite. These are reflected in the layout of
the test directory:
1. `python` - contains the Python based test suite to exercise Python code
**within** PyScript. These tests are run four differeng ways to ensure all
combination of MicroPython/Pyodide and main thread/worker contexts are
checked.
2. `javascript` - contains JavaScript tests to exercise PyScript _itself_, in
the browser.
3. `manual` - contains tests to run manually in a browser, due to the complex
nature of the tests.
We use [Playwright](https://playwright.dev/) to automate the running of the
Python and JavaScript test suites. We use
[uPyTest](https://github.com/ntoll/upytest) as a test framework for the Python
test suite. uPyTest is a "PyTest inspired" framework for running tests in the
browser on both MicroPython and Pyodide.
The automated (Playwright) tests are specified in the `integration.spec.js`
file in this directory.
All automatic tests live in either the `python` or `javascript` folders. All
the tests in these folder are run by CI or locally run by `make test` in the
root of this project. Alternatively, run `npm run test:integration` in the
PyScript source directory.
Similarly, some tests can only be run manually (due to their nature or
underlying complexity). These are in the `manual` directory and are in the form
of separate directories (each containing an `index.html`) or individual `*.html`
files to which you point your browser. Each separate test may exercise
JavaScript or Python code (or both), and the context for each separate test is
kept carefully isolated.
Some rules of thumb:
* We don't test upstream projects: we assume they have their own test suites,
and if we find bugs, we file an issue upstream with an example of how to
recreate the problem.
* We don't test browser functionality, we just have to trust that browsers work
as advertised. Once again, if we find an issue, we report upstream.
* All test cases should include commentary describing the **intent** and
context of the test.
* Tests in Python use [uPyTest](https://github.com/ntoll/upytest) (see the
README for documentation), an "inspired by PyTest" test framework that works
with both MicroPython and Pyodide in the browser. This means that all
Python tests should work with both interpreters.
* Tests in JavaScript... (Andrea to explain). ;-)

View File

@@ -1,18 +0,0 @@
<!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><ul><li><strong><span>javascript</span></strong><ul><li><a href="./javascript/async-listener.html">async-listener<small>.html</small></a></li><li><a href="./javascript/config-url.html">config-url<small>.html</small></a></li><li><a href="./javascript/config_type.html">config_type<small>.html</small></a></li><li><strong><a href="./javascript/fetch/index.html">fetch</a></strong></li><li><a href="./javascript/ffi.html">ffi<small>.html</small></a></li><li><a href="./javascript/hooks.html">hooks<small>.html</small></a></li><li><strong><a href="./javascript/issue-2093/index.html">issue-2093</a></strong></li><li><a href="./javascript/js-storage.html">js-storage<small>.html</small></a></li><li><a href="./javascript/js_modules.html">js_modules<small>.html</small></a></li><li><strong><a href="./javascript/loader/index.html">loader</a></strong></li><li><a href="./javascript/mpy-error.html">mpy-error<small>.html</small></a></li><li><a href="./javascript/mpy-no-error.html">mpy-no-error<small>.html</small></a></li><li><a href="./javascript/mpy.html">mpy<small>.html</small></a></li><li><a href="./javascript/py-terminal-main.html">py-terminal-main<small>.html</small></a></li><li><a href="./javascript/py-terminal-worker.html">py-terminal-worker<small>.html</small></a></li><li><a href="./javascript/py-terminal.html">py-terminal<small>.html</small></a></li><li><a href="./javascript/py-terminals.html">py-terminals<small>.html</small></a></li><li><strong><a href="./javascript/pyodide-cache/index.html">pyodide-cache</a></strong></li><li><a href="./javascript/storage.html">storage<small>.html</small></a></li><li><strong><span>workers</span></strong><ul><li><strong><a href="./javascript/workers/create_named/index.html">create_named</a></strong></li><li><strong><a href="./javascript/workers/mpy/index.html">mpy</a></strong></li><li><strong><a href="./javascript/workers/py/index.html">py</a></strong></li></ul></li></ul></li><li><strong><a href="./manual/index.html">manual</a></strong><ul><li><a href="./manual/all-done.html">all-done<small>.html</small></a></li><li><a href="./manual/async.html">async<small>.html</small></a></li><li><a href="./manual/camera.html">camera<small>.html</small></a></li><li><a href="./manual/click.html">click<small>.html</small></a></li><li><a href="./manual/code-a-part.html">code-a-part<small>.html</small></a></li><li><a href="./manual/combo.html">combo<small>.html</small></a></li><li><a href="./manual/config.html">config<small>.html</small></a></li><li><a href="./manual/create-element.html">create-element<small>.html</small></a></li><li><a href="./manual/dialog.html">dialog<small>.html</small></a></li><li><a href="./manual/display.html">display<small>.html</small></a></li><li><strong><a href="./manual/donkey/index.html">donkey</a></strong></li><li><a href="./manual/error.html">error<small>.html</small></a></li><li><a href="./manual/html-decode.html">html-decode<small>.html</small></a></li><li><a href="./manual/input.html">input<small>.html</small></a></li><li><a href="./manual/interpreter.html">interpreter<small>.html</small></a></li><li><strong><a href="./manual/issue-7015/index.html">issue-7015</a></strong></li><li><a href="./manual/multi.html">multi<small>.html</small></a></li><li><a href="./manual/multiple-editors.html">multiple-editors<small>.html</small></a></li><li><a href="./manual/no-error.html">no-error<small>.html</small></a></li><li><strong><a href="./manual/no_sab/index.html">no_sab</a></strong></li><li><strong><a href="./manual/piratical/index.html">piratical</a></strong></li><li><a href="./manual/py-editor.html">py-editor<small>.html</small></a></li><li><a href="./manual/py-editor-failure.html">py-editor-failure<small>.html</small></a></li><li><strong><a href="./manual/py-terminals/index.html">py-terminals</a></strong><ul><li><a href="./manual/py-terminals/no-repl.html">no-repl<small>.html</small></a></li><li><a href="./manual/py-terminals/repl.html">repl<small>.html</small></a></li></ul></li><li><a href="./manual/py_modules.html">py_modules<small>.html</small></a></li><li><strong><a href="./manual/service-worker/index.html">service-worker</a></strong></li><li><a href="./manual/split-config.html">split-config<small>.html</small></a></li><li><a href="./manual/submit.html">submit<small>.html</small></a></li><li><a href="./manual/target.html">target<small>.html</small></a></li><li><a href="./manual/test_display_HTML.html">test_display_HTML<small>.html</small></a></li><li><a href="./manual/test_when.html">test_when<small>.html</small></a></li><li><a href="./manual/worker.html">worker<small>.html</small></a></li></ul></li><li><strong><a href="./python/index.html">python</a></strong></li></ul></body>
</html>

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy">
from pyscript import window, document, fetch, when
@when('click', '#click')
async def click(event):
text = await fetch(window.location.href).text()
document.getElementById('output').append(text)
document.documentElement.classList.add('ok')
document.getElementById('click').click()
</script>
<button id="click">click</button>
<pre id="output"></pre>
</body>
</html>

View File

@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin</title>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
<mpy-config src="config-url/config.json"></mpy-config>
<script type="mpy">
from pyscript import config
if config["files"]["{TO}"] != "./runtime":
raise Exception("wrong config tree")
from runtime import test
</script>
<script type="mpy" worker>
from pyscript import config
if config["files"]["{TO}"] != "./runtime":
raise Exception("wrong config tree")
from runtime import test
</script>
</head>
</html>

View File

@@ -1,7 +0,0 @@
{
"files":{
"{FROM}": "./src",
"{TO}": "./runtime",
"{FROM}/test.py": "{TO}/test.py"
}
}

View File

@@ -1,8 +0,0 @@
from pyscript import RUNNING_IN_WORKER, document
classList = document.documentElement.classList
if RUNNING_IN_WORKER:
classList.add("worker")
else:
classList.add("main")

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy">
from pyscript import config, document
if config["type"] is "mpy":
document.documentElement.classList.add("mpy")
</script>
<script type="py">
from pyscript import config, document
if config["type"] is "py":
document.documentElement.classList.add("py")
</script>
</body>
</html>

View File

@@ -1,95 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../../../dist/core.css">
</head>
<body>
<script type="module">
import fetch from 'https://esm.run/@webreflection/fetch';
globalThis.fetch_text = await fetch("config.json").text();
globalThis.fetch_json = JSON.stringify(await fetch("config.json").json());
globalThis.fetch_buffer = new Uint8Array((await fetch("config.json").arrayBuffer())).length;
document.head.appendChild(
Object.assign(
document.createElement('script'),
{
type: 'module',
src: '../../../dist/core.js'
}
)
);
</script>
<script type="mpy" async>
import js, json
from pyscript import document, fetch
fetch_text = await (await fetch("config.json")).text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_text = await fetch("config.json").text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_json = await (await fetch("config.json")).json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_json = await fetch("config.json").json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
fetch_buffer = await fetch("config.json").arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
print(await (await fetch("config.json")).bytearray())
print(await (await fetch("config.json")).blob())
if (await fetch("shenanigans.nope")).ok == False:
document.documentElement.classList.add('mpy')
</script>
<script type="py" async>
import js, json
from pyscript import document, fetch
fetch_text = await (await fetch("config.json")).text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_text = await fetch("config.json").text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_json = await (await fetch("config.json")).json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_json = await fetch("config.json").json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
fetch_buffer = await fetch("config.json").arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
print(await (await fetch("config.json")).bytearray())
print(await (await fetch("config.json")).blob())
if (await fetch("shenanigans.nope")).ok == False:
document.documentElement.classList.add('py')
</script>
</body>
</html>

View File

@@ -1,26 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript FFI</title>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy">
from pyscript import document
from pyscript.ffi import to_js
document.documentElement.classList.add(
to_js({"ok": "mpy"}).ok
)
</script>
<script type="py">
from pyscript import document
from pyscript.ffi import to_js
document.documentElement.classList.add(
to_js({"ok": "py"}).ok
)
</script>
</body>
</html>

View File

@@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin Bug?</title>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module">
addEventListener('mpy:done', () => {
document.documentElement.classList.add('done');
});
import { hooks } from "../../dist/core.js";
// Main
hooks.main.onReady.add((wrap, element) => {
console.log("main", "onReady");
if (location.search === '?debug') {
console.debug("main", "wrap", wrap);
console.debug("main", "element", element);
}
});
hooks.main.onBeforeRun.add(() => {
console.log("main", "onBeforeRun");
});
hooks.main.codeBeforeRun.add('print("main", "codeBeforeRun")');
hooks.main.codeAfterRun.add('print("main", "codeAfterRun")');
hooks.main.onAfterRun.add(() => {
console.log("main", "onAfterRun");
});
// Worker
hooks.worker.onReady.add((wrap, xworker) => {
console.log("worker", "onReady");
if (location.search === '?debug') {
console.debug("worker", "wrap", wrap);
console.debug("worker", "xworker", xworker);
}
});
hooks.worker.onBeforeRun.add(() => {
console.log("worker", "onBeforeRun");
});
hooks.worker.codeBeforeRun.add('print("worker", "codeBeforeRun")');
hooks.worker.codeAfterRun.add('print("worker", "codeAfterRun")');
hooks.worker.onAfterRun.add(() => {
console.log("worker", "onAfterRun");
});
</script>
</head>
<body>
<script type="mpy" async="false" worker>
from pyscript import document
print("actual code in worker")
document.documentElement.classList.add('worker')
</script>
<script type="mpy" async="false">
print("actual code in main")
</script>
</body>
</html>

View File

@@ -1,6 +0,0 @@
const { error } = console;
console.error = (...args) => {
error(...args);
document.documentElement.classList.add('errored');
};

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="./error.js"></script>
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="mpy-editor" setup>
print("Hello Editor")
raise Exception("failure")
</script>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module">
import { storage } from '../../dist/storage.js';
const store = await storage('js-storage');
store.test = [1, 2, 3].map(String);
await store.sync();
// be sure the store is not empty before bootstrap
import('../../dist/core.js');
</script>
</head>
<body>
<script type="mpy">
from pyscript import storage, document
store = await storage("js-storage")
if ",".join(store["test"]) == "1,2,3":
document.documentElement.classList.add("ok")
</script>
</body>
</html>

View File

@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<mpy-config>
[js_modules.main]
"./js_modules.js" = "random_js"
</mpy-config>
<mpy-script>
from pyscript.js_modules.random_js import default as value
from pyscript.js_modules import random_js
from pyscript import js_modules
print("mpy", value)
print("mpy", random_js.default)
print("mpy", js_modules.random_js.default)
</mpy-script>
<py-config>
[js_modules.main]
"./js_modules.js" = "random_js"
</py-config>
<py-script>
from pyscript.js_modules.random_js import default as value
from pyscript.js_modules import random_js
from pyscript import js_modules, document
print("py", value)
print("py", random_js.default)
print("py", js_modules.random_js.default)
document.documentElement.classList.add('done')
</py-script>
</body>
</html>

View File

@@ -1 +0,0 @@
export default Math.random();

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module">
import '../../../dist/core.js';
addEventListener('py:progress', ({ detail }) => {
document.body.append(
detail,
document.createElement('br'),
);
});
</script>
</head>
<body>
<py-config>
packages = ["matplotlib"]
</py-config>
<script type="py" worker>
from pyscript import document
from sys import version
document.body.append(document.createElement('hr'), version)
document.documentElement.classList.add('ok')
</script>
</body>
</html>

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="../../dist/core.js"></script>
<script type="mpy">
from pyscript import document
import sys
print("This is an error", file=sys.stderr)
document.documentElement.classList.add("ok");
</script>
</head>
</html>

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="../../dist/core.js"></script>
<script type="mpy" config="mpy-no-error.toml">
from pyscript import document
import sys
print("This is an error", file=sys.stderr)
document.documentElement.classList.add("ok");
</script>
</head>
</html>

View File

@@ -1 +0,0 @@
plugins = ["!error"]

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