mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Compare commits
89 Commits
2024.8.2
...
event-refi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00f6cfbd28 | ||
|
|
b911ea99fb | ||
|
|
46ca9154c4 | ||
|
|
afd7a8eb00 | ||
|
|
b22f384d73 | ||
|
|
caeab77a8e | ||
|
|
f2bbc6ed5f | ||
|
|
1d666b92a2 | ||
|
|
290eb03388 | ||
|
|
55031f2347 | ||
|
|
8168383653 | ||
|
|
3ff2c171bc | ||
|
|
edbac13713 | ||
|
|
46239caa19 | ||
|
|
0366e48fad | ||
|
|
b13317d32f | ||
|
|
57b1440a10 | ||
|
|
fc53356a1d | ||
|
|
5be99456f0 | ||
|
|
7adedcc704 | ||
|
|
d143b229ed | ||
|
|
0d74a60227 | ||
|
|
ce923a354f | ||
|
|
7e65836423 | ||
|
|
796373cfa6 | ||
|
|
0a1d3bb678 | ||
|
|
4e43d3e92d | ||
|
|
5acc2afaf3 | ||
|
|
56c64cbee7 | ||
|
|
4ff02a24d1 | ||
|
|
a5dc94792b | ||
|
|
0db79e0f02 | ||
|
|
283eabdb30 | ||
|
|
3f19e00410 | ||
|
|
9233d5e45a | ||
|
|
fe580cd90b | ||
|
|
00e6cfed29 | ||
|
|
6b1330d28a | ||
|
|
5d751493f6 | ||
|
|
c3517f7973 | ||
|
|
b1c33b7f79 | ||
|
|
722abda895 | ||
|
|
8061bc0143 | ||
|
|
993e812e7b | ||
|
|
a6b6dd8479 | ||
|
|
febbb031ac | ||
|
|
f827efe2fc | ||
|
|
8c6bfecbff | ||
|
|
f562860e49 | ||
|
|
62c78b0f42 | ||
|
|
4fe6ad80af | ||
|
|
89a0a556f3 | ||
|
|
af06bc4826 | ||
|
|
6fab9a1c26 | ||
|
|
386966ebd2 | ||
|
|
c257b704e6 | ||
|
|
fc599d96a2 | ||
|
|
8de97a7e7b | ||
|
|
2642be863b | ||
|
|
9dad29ec17 | ||
|
|
abb1eb28fe | ||
|
|
532ac0c80f | ||
|
|
c1849d28e0 | ||
|
|
ad2af2392b | ||
|
|
957ab69c21 | ||
|
|
f5d49ee52c | ||
|
|
06138bbb48 | ||
|
|
ae66d13d57 | ||
|
|
5aaeebf32c | ||
|
|
a6b0964185 | ||
|
|
dd86169f2a | ||
|
|
84c7d69db9 | ||
|
|
ca9b565adc | ||
|
|
b2d1018db1 | ||
|
|
c4e25d879e | ||
|
|
c82dbb755e | ||
|
|
1ed77321a5 | ||
|
|
e36a57eb06 | ||
|
|
ee3cd76022 | ||
|
|
eb31e51a45 | ||
|
|
c8c2dd0806 | ||
|
|
e525d54be0 | ||
|
|
7b9f7c13f5 | ||
|
|
7582cbef9c | ||
|
|
b395cde49c | ||
|
|
9f46234f71 | ||
|
|
f4c4edeb29 | ||
|
|
7166c32384 | ||
|
|
ed126889ae |
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
<!-- 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 -->
|
<!-- List the technical changes done to fix a bug or introduce a new feature. -->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- 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. -->
|
- [ ] I have checked `make build` works locally.
|
||||||
|
- [ ] I have created / updated documentation for this change (if applicable).
|
||||||
- [ ] All tests pass locally
|
|
||||||
- [ ] I have updated `CHANGELOG.md`
|
|
||||||
- [ ] I have created documentation for this(if applicable)
|
|
||||||
|
|||||||
23
.github/workflows/prepare-release.yml
vendored
23
.github/workflows/prepare-release.yml
vendored
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-release:
|
prepare-release:
|
||||||
@@ -19,7 +19,22 @@ jobs:
|
|||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
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
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -35,14 +50,14 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: NPM Install
|
- name: NPM Install
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Generate index.html
|
- name: Generate index.html
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#./#' ./public/index.html > ./pyscript.core/dist/index.html
|
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: Zip dist folder
|
- name: Zip dist folder
|
||||||
run: zip -r -q ./build.zip ./dist
|
run: zip -r -q ./build.zip ./dist
|
||||||
|
|||||||
23
.github/workflows/publish-release.yml
vendored
23
.github/workflows/publish-release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-release:
|
publish-release:
|
||||||
@@ -21,7 +21,22 @@ jobs:
|
|||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
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
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -37,14 +52,14 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Generate index.html in snapshot
|
- name: Generate index.html in snapshot
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
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/
|
- name: Generate release.tar from snapshot and put it in dist/
|
||||||
working-directory: .
|
working-directory: .
|
||||||
|
|||||||
23
.github/workflows/publish-snapshot.yml
vendored
23
.github/workflows/publish-snapshot.yml
vendored
@@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-snapshot:
|
publish-snapshot:
|
||||||
@@ -25,7 +25,22 @@ jobs:
|
|||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
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
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -41,7 +56,7 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Build Pyscript.core
|
- name: Build Pyscript.core
|
||||||
run: npm run build
|
run: npm run build
|
||||||
@@ -54,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate index.html in snapshot
|
- name: Generate index.html in snapshot
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
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: Copy to Snapshot
|
||||||
run: >
|
run: >
|
||||||
|
|||||||
27
.github/workflows/publish-unstable.yml
vendored
27
.github/workflows/publish-unstable.yml
vendored
@@ -1,11 +1,11 @@
|
|||||||
name: "Publish Unstable"
|
name: "Publish Unstable"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push: # Only run on merges into main that modify files under pyscript.core/ and examples/
|
push: # Only run on merges into main that modify files under core/ and examples/
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- pyscript.core/**
|
- core/**
|
||||||
- examples/**
|
- examples/**
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -26,7 +26,22 @@ jobs:
|
|||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
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
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -42,14 +57,14 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: NPM Install
|
- name: NPM Install
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Generate index.html in snapshot
|
- name: Generate index.html in snapshot
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/unstable/#' ./public/index.html > ./pyscript.core/dist/index.html
|
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
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
|||||||
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -5,20 +5,20 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- pyscript.core/**
|
- core/**
|
||||||
- .github/workflows/test.yml
|
- .github/workflows/test.yml
|
||||||
|
|
||||||
pull_request: # Only run on merges into main that modify certain files
|
pull_request: # Only run on merges into main that modify certain files
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- pyscript.core/**
|
- core/**
|
||||||
- .github/workflows/test.yml
|
- .github/workflows/test.yml
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
BuildAndTest:
|
BuildAndTest:
|
||||||
runs-on: ubuntu-latest-8core
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
MINICONDA_PYTHON_VERSION: py38
|
MINICONDA_PYTHON_VERSION: py38
|
||||||
MINICONDA_VERSION: 4.11.0
|
MINICONDA_VERSION: 4.11.0
|
||||||
@@ -69,24 +69,12 @@ jobs:
|
|||||||
make setup
|
make setup
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build
|
run: make build # Integration tests run in the build step.
|
||||||
|
|
||||||
- name: Integration Tests
|
|
||||||
#run: make test-integration-parallel
|
|
||||||
run: |
|
|
||||||
make test-integration
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pyscript
|
name: pyscript
|
||||||
path: |
|
path: |
|
||||||
pyscript.core/dist/
|
core/dist/
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: success() || failure()
|
|
||||||
with:
|
|
||||||
name: test_results
|
|
||||||
path: test_results/
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|||||||
16
.github/workflows/test_report.yml
vendored
16
.github/workflows/test_report.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
name: Test Report
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ['\[CI\] Test']
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
jobs:
|
|
||||||
report:
|
|
||||||
runs-on: ubuntu-latest-8core
|
|
||||||
steps:
|
|
||||||
- uses: dorny/test-reporter@v1.9.0
|
|
||||||
with:
|
|
||||||
artifact: test_results
|
|
||||||
name: Test reports
|
|
||||||
path: "*.xml"
|
|
||||||
reporter: java-junit
|
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -142,11 +142,11 @@ coverage/
|
|||||||
test_results
|
test_results
|
||||||
|
|
||||||
# @pyscript/core npm artifacts
|
# @pyscript/core npm artifacts
|
||||||
pyscript.core/test-results/*
|
core/test-results/*
|
||||||
pyscript.core/core.*
|
core/core.*
|
||||||
pyscript.core/dist
|
core/dist
|
||||||
pyscript.core/dist.zip
|
core/dist.zip
|
||||||
pyscript.core/src/plugins.js
|
core/src/plugins.js
|
||||||
pyscript.core/src/stdlib/pyscript.js
|
core/src/stdlib/pyscript.js
|
||||||
pyscript.core/src/3rd-party/*
|
core/src/3rd-party/*
|
||||||
!pyscript.core/src/3rd-party/READMEmd
|
!core/src/3rd-party/READMEmd
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ ci:
|
|||||||
#skip: [eslint]
|
#skip: [eslint]
|
||||||
autoupdate_schedule: monthly
|
autoupdate_schedule: monthly
|
||||||
|
|
||||||
default_stages: [commit]
|
default_stages: [pre-commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
@@ -21,33 +21,33 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: pyscript\.core/dist|\.min\.js$
|
exclude: core/dist|\.min\.js$
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 24.4.2
|
rev: 25.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
exclude: core/tests
|
||||||
|
args: ["-l", "88", "--skip-string-normalization"]
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.3.0
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'pyproject.toml' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
exclude: \.js\.map$
|
exclude: fs\.py|\.js\.map$
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- tomli
|
- tomli
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.9.6
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
exclude: core/tests
|
||||||
|
|
||||||
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
||||||
rev: "v3.0.0-alpha.6"
|
rev: "v3.0.0-alpha.6"
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
exclude: pyscript\.core/test|pyscript\.core/dist|pyscript\.core/types|pyscript.core/src/stdlib/pyscript.js|pyscript\.sw/|pyscript.core/src/3rd-party
|
exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
|
||||||
args: [--tab-width, "4"]
|
args: [--tab-width, "4"]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: 5.13.2
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
name: isort (python)
|
|
||||||
args: [--profile, black]
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
ISSUE_TEMPLATE
|
ISSUE_TEMPLATE
|
||||||
*.min.*
|
*.min.*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
docs
|
|
||||||
examples/panel.html
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# .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
|
|
||||||
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,97 +0,0 @@
|
|||||||
# Release Notes
|
|
||||||
|
|
||||||
## 2024.05.21
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
- `py-editor` run buttons now display a spinner when disabled, which occurs when the editor is running code.
|
|
||||||
|
|
||||||
## 2023.05.01
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Added the `xterm` attribute to `py-config`. When set to `True` or `xterm`, an (output-only) [xterm.js](http://xtermjs.org/) terminal will be used in place of the default py-terminal.
|
|
||||||
- The default version of Pyodide is now `0.23.2`. See the [Pyodide Changelog](https://pyodide.org/en/stable/project/changelog.html#version-0-23-2) for a detailed list of changes.
|
|
||||||
- Added the `@when` decorator for attaching Python functions as event handlers
|
|
||||||
- The `py-mount` attribute on HTML elements has been deprecated, and will be removed in a future release.
|
|
||||||
|
|
||||||
#### Runtime py- attributes
|
|
||||||
|
|
||||||
- Added logic to react to `py-*` attributes changes, removal, `py-*` attributes added to already live nodes but also `py-*` attributes added or defined via injected nodes (either appended or via `innerHTML` operations). ([#1435](https://github.com/pyscript/pyscript/pull/1435))
|
|
||||||
|
|
||||||
#### <script type="py">
|
|
||||||
|
|
||||||
- Added the ability to optionally use `<script type="py">`, `<script type="pyscript">` or `<script type="py-script">` instead of a `<py-script>` custom element, in order to tackle cases where the content of the `<py-script>` tag, inevitably parsed by browsers, could accidentally contain _HTML_ able to break the surrounding page layout. ([#1396](https://github.com/pyscript/pyscript/pull/1396))
|
|
||||||
|
|
||||||
#### <py-terminal>
|
|
||||||
|
|
||||||
- Added a `docked` field and attribute for the `<py-terminal>` custom element, enabled by default when the terminal is in `auto` mode, and able to dock the terminal at the bottom of the page with auto scroll on new code execution.
|
|
||||||
|
|
||||||
#### <py-script>
|
|
||||||
|
|
||||||
- Restored the `output` attribute of `py-script` tags to route `sys.stdout` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
|
|
||||||
- Added a `stderr` attribute of `py-script` tags to route `sys.stderr` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
|
|
||||||
|
|
||||||
#### <py-repl>
|
|
||||||
|
|
||||||
- The `output` attribute of `py-repl` tags now specifies the id of the DOM element that `sys.stdout`, `sys.stderr`, and the results of a REPL execution are written to. It no longer affects the location of calls to `display()`
|
|
||||||
- Added a `stderr` attribute of `py-repl` tags to route `sys.stderr` to a DOM element with the given ID. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
|
|
||||||
- Resored the `output-mode` attribute of `py-repl` tags. If `output-mode` == 'append', the DOM element where output is printed is _not_ cleared before writing new results.
|
|
||||||
- Load code from the attribute src of py-repl and preload it into the corresponding py-repl tag by use the attribute `str` in your `py-repl` tag([#1292](https://github.com/pyscript/pyscript/pull/1292))
|
|
||||||
- <py-repl> elements now have a `getPySrc()` method, which returns the code inside the REPL as a string.([#1516](https://github.com/pyscript/pyscript/pull/1292))
|
|
||||||
|
|
||||||
#### Plugins
|
|
||||||
|
|
||||||
- Plugins may now implement the `beforePyReplExec()` and `afterPyReplExec()` hooks, which are called immediately before and after code in a `py-repl` tag is executed. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
|
|
||||||
|
|
||||||
#### Web worker support
|
|
||||||
|
|
||||||
- introduced the new experimental `execution_thread` config option: if you set `execution_thread = "worker"`, the python interpreter runs inside a web worker
|
|
||||||
- worker support is still **very** experimental: not everything works, use it at your own risk
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
- Fixes [#1280](https://github.com/pyscript/pyscript/issues/1280), which describes the errors on the PyRepl tests related to having auto-gen tags that shouldn't be there.
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
- Py-REPL tests now run on both osx and non osx OSs
|
|
||||||
- migrated from _rollup_ to _esbuild_ to create artifacts
|
|
||||||
- updated `@codemirror` dependency to its latest
|
|
||||||
|
|
||||||
### Docs
|
|
||||||
|
|
||||||
- Add docs for event handlers
|
|
||||||
|
|
||||||
## 2023.03.1
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
- Fixed an issue where `pyscript` would not be available when using the minified version of PyScript. ([#1054](https://github.com/pyscript/pyscript/pull/1054))
|
|
||||||
- Fixed missing closing tag when rendering an image with `display`. ([#1058](https://github.com/pyscript/pyscript/pull/1058))
|
|
||||||
- Fixed a bug where Python plugins methods were being executed twice. ([#1064](https://github.com/pyscript/pyscript/pull/1064))
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
- When adding a `py-` attribute to an element but didn't added an `id` attribute, PyScript will now generate a random ID for the element instead of throwing an error which caused the splash screen to not shutdown. ([#1122](https://github.com/pyscript/pyscript/pull/1122))
|
|
||||||
- You can now disable the splashscreen by setting `enabled = false` in your `py-config` under the `[splashscreen]` configuration section. ([#1138](https://github.com/pyscript/pyscript/pull/1138))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- Fixed 'Direct usage of document is deprecated' warning in the getting started guide. ([#1052](https://github.com/pyscript/pyscript/pull/1052))
|
|
||||||
- Added reference documentation for the `py-splashscreen` plugin ([#1138](https://github.com/pyscript/pyscript/pull/1138))
|
|
||||||
- Adds doc for installing tests ([#1156](https://github.com/pyscript/pyscript/pull/1156))
|
|
||||||
- Adds docs for custom Pyscript attributes (`py-*`) that allow you to add event listeners to an element ([#1125](https://github.com/pyscript/pyscript/pull/1125))
|
|
||||||
|
|
||||||
### Deprecations and Removals
|
|
||||||
|
|
||||||
- The py-config `runtimes` to specify an interpreter has been deprecated. The `interpreters` config should be used instead. ([#1082](https://github.com/pyscript/pyscript/pull/1082))
|
|
||||||
- The attributes `pys-onClick` and `pys-onKeyDown` have been deprecated, but the warning was only shown in the console. An alert banner will now be shown on the page if the attributes are used. They will be removed in the next release. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
|
|
||||||
- The pyscript elements `py-button`, `py-inputbox`, `py-box` and `py-title` have now completed their deprecation cycle and have been removed. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
|
|
||||||
- The attributes `pys-onClick` and `pys-onKeyDown` have been removed. Use `py-click` and `py-keydown` instead ([#1361](https://github.com/pyscript/pyscript/pull/1361))
|
|
||||||
181
CONTRIBUTING.md
181
CONTRIBUTING.md
@@ -1,181 +1,4 @@
|
|||||||
# Contributing to PyScript
|
# Contributing to PyScript
|
||||||
|
|
||||||
Thank you for wanting to contribute to the PyScript project!
|
Please see our guide to contributing to PyScript
|
||||||
|
[in our documentation](https://docs.pyscript.net/latest/contributing/).
|
||||||
## 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/contributing/#set-up-your-development-environment) will help you get started.
|
|
||||||
|
|
||||||
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/developers/) 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/).
|
|
||||||
|
|
||||||
# Quick guide to pytest
|
|
||||||
|
|
||||||
We make heavy usage of pytest. Here is a quick guide and collection of useful options:
|
|
||||||
|
|
||||||
- To run all tests in the current directory and subdirectories: pytest
|
|
||||||
|
|
||||||
- To run tests in a specific directory or file: pytest path/to/dir/test_foo.py
|
|
||||||
|
|
||||||
- -s: disables output capturing
|
|
||||||
|
|
||||||
- --pdb: in case of exception, enter a (Pdb) prompt so that you can inspect what went wrong.
|
|
||||||
|
|
||||||
- -v: verbose mode
|
|
||||||
|
|
||||||
- -x: stop the execution as soon as one test fails
|
|
||||||
|
|
||||||
- -k foo: run only the tests whose full name contains foo
|
|
||||||
|
|
||||||
- -k 'foo and bar'
|
|
||||||
|
|
||||||
- -k 'foo and not bar'
|
|
||||||
|
|
||||||
## Running integration tests under pytest
|
|
||||||
|
|
||||||
make test is useful to run all the tests, but during the development is useful to have more control on how tests are run. The following guide assumes that you are in the directory pyscriptjs/tests/integration/.
|
|
||||||
|
|
||||||
### To run all the integration tests, single or multi core
|
|
||||||
|
|
||||||
$ pytest -xv
|
|
||||||
...
|
|
||||||
|
|
||||||
test_00_support.py::TestSupport::test_basic[chromium] PASSED [ 0%]
|
|
||||||
test_00_support.py::TestSupport::test_console[chromium] PASSED [ 1%]
|
|
||||||
test_00_support.py::TestSupport::test_check_js_errors_simple[chromium] PASSED [ 2%]
|
|
||||||
test_00_support.py::TestSupport::test_check_js_errors_expected[chromium] PASSED [ 3%]
|
|
||||||
test_00_support.py::TestSupport::test_check_js_errors_expected_but_didnt_raise[chromium] PASSED [ 4%]
|
|
||||||
test_00_support.py::TestSupport::test_check_js_errors_multiple[chromium] PASSED [ 5%]
|
|
||||||
...
|
|
||||||
|
|
||||||
-x means "stop at the first failure". -v means "verbose", so that you can see all the test names one by one. We try to keep tests in a reasonable order, from most basic to most complex. This way, if you introduced some bug in very basic things, you will notice immediately.
|
|
||||||
|
|
||||||
If you have the pytest-xdist plugin installed, you can run all the integration tests on 4 cores in parallel:
|
|
||||||
|
|
||||||
$ pytest -n 4
|
|
||||||
|
|
||||||
### To run a single test, headless
|
|
||||||
|
|
||||||
$ pytest test_01_basic.py -k test_pyscript_hello -s
|
|
||||||
...
|
|
||||||
[ 0.00 page.goto ] pyscript_hello.html
|
|
||||||
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
|
|
||||||
...
|
|
||||||
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
|
|
||||||
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
|
|
||||||
...
|
|
||||||
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
|
|
||||||
[ 3.60 console.log ] hello pyscript
|
|
||||||
|
|
||||||
-k selects tests by pattern matching as described above. -s instructs pytest to show the output to the terminal instead of capturing it. In the output you can see various useful things, including network requests and JS console messages.
|
|
||||||
|
|
||||||
### To run a single test, headed
|
|
||||||
|
|
||||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed
|
|
||||||
...
|
|
||||||
|
|
||||||
Same as above, but with --headed the browser is shown in a window, and you can interact with it. The browser uses a fake server, which means that HTTP requests are cached.
|
|
||||||
|
|
||||||
Unfortunately, in this mode source maps does not seem to work, and you cannot debug the original typescript source code. This seems to be a bug in playwright, for which we have a workaround:
|
|
||||||
|
|
||||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed --no-fake-server
|
|
||||||
...
|
|
||||||
|
|
||||||
As the name implies, -no-fake-server disables the fake server: HTTP requests are not cached, but source-level debugging works.
|
|
||||||
|
|
||||||
Finally:
|
|
||||||
|
|
||||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --dev
|
|
||||||
...
|
|
||||||
|
|
||||||
--dev implies --headed --no-fake-server. In addition, it also automatically open chrome dev tools.
|
|
||||||
|
|
||||||
### To run only main thread or worker tests
|
|
||||||
|
|
||||||
By default, we run each test twice: one with execution_thread = "main" and one with execution_thread = "worker". If you want to run only half of them, you can use -m:
|
|
||||||
|
|
||||||
$ pytest -m main # run only the tests in the main thread
|
|
||||||
$ pytest -m worker # ron only the tests in the web worker
|
|
||||||
|
|
||||||
## Fake server, HTTP cache
|
|
||||||
|
|
||||||
By default, our test machinery uses a playwright router which intercepts and cache HTTP requests, so that for example you don't have to download pyodide again and again. This also enables the possibility of running tests in parallel on multiple cores.
|
|
||||||
|
|
||||||
The cache is stored using the pytest-cache plugin, which means that it survives across sessions.
|
|
||||||
|
|
||||||
If you want to temporarily disable the cache, the easiest thing is to use --no-fake-server, which bypasses it completely.
|
|
||||||
|
|
||||||
If you want to clear the cache, you can use the special option --clear-http-cache:
|
|
||||||
|
|
||||||
NOTE: this works only if you are inside tests/integration, or if you explicitly specify tests/integration from the command line. This is due to how pytest decides to search for and load the various conftest.py.
|
|
||||||
|
|||||||
33
Makefile
33
Makefile
@@ -9,10 +9,11 @@ PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
|
|||||||
all:
|
all:
|
||||||
@echo "\nThere is no default Makefile target right now. Try:\n"
|
@echo "\nThere is no default Makefile target right now. Try:\n"
|
||||||
@echo "make setup - check your environment and install the dependencies."
|
@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 clean - clean up auto-generated assets."
|
||||||
@echo "make build - build PyScript."
|
@echo "make build - build PyScript."
|
||||||
@echo "make precommit-check - run the precommit checks (run eslint)."
|
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||||
@echo "make test-integration - run all integration tests sequentially."
|
@echo "make test - run all automated tests in playwright."
|
||||||
@echo "make fmt - format the code."
|
@echo "make fmt - format the code."
|
||||||
@echo "make fmt-check - check the code formatting.\n"
|
@echo "make fmt-check - check the code formatting.\n"
|
||||||
|
|
||||||
@@ -39,13 +40,12 @@ check-python:
|
|||||||
|
|
||||||
# Check the environment, install the dependencies.
|
# Check the environment, install the dependencies.
|
||||||
setup: check-node check-npm check-python
|
setup: check-node check-npm check-python
|
||||||
cd pyscript.core && npm install && cd ..
|
cd core && npm ci && cd ..
|
||||||
ifeq ($(VIRTUAL_ENV),)
|
ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
|
||||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
python -m pip install -r requirements.txt
|
python -m pip install -r requirements.txt
|
||||||
playwright install
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Clean up generated assets.
|
# Clean up generated assets.
|
||||||
@@ -55,22 +55,24 @@ clean:
|
|||||||
rm -rf .pytest_cache .coverage coverage.xml
|
rm -rf .pytest_cache .coverage coverage.xml
|
||||||
|
|
||||||
# Build PyScript.
|
# Build PyScript.
|
||||||
build:
|
build: precommit-check
|
||||||
cd pyscript.core && npx playwright install && npm run build
|
cd core && npx playwright install chromium && npm run build
|
||||||
|
|
||||||
|
# Update the dependencies.
|
||||||
|
update:
|
||||||
|
python -m pip install -r requirements.txt --upgrade
|
||||||
|
|
||||||
# Run the precommit checks (run eslint).
|
# Run the precommit checks (run eslint).
|
||||||
precommit-check:
|
precommit-check:
|
||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
|
|
||||||
# Run all integration tests sequentially.
|
# Run all automated tests in playwright.
|
||||||
test-integration:
|
test:
|
||||||
mkdir -p test_results
|
cd core && npm run test:integration
|
||||||
pytest -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
|
||||||
|
|
||||||
# Run all integration tests in parallel.
|
# Serve the repository with the correct headers.
|
||||||
test-integration-parallel:
|
serve:
|
||||||
mkdir -p test_results
|
npx mini-coi .
|
||||||
pytest --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
|
||||||
|
|
||||||
# Format the code.
|
# Format the code.
|
||||||
fmt: fmt-py
|
fmt: fmt-py
|
||||||
@@ -83,7 +85,6 @@ fmt-check: fmt-py-check
|
|||||||
# Format Python code.
|
# Format Python code.
|
||||||
fmt-py:
|
fmt-py:
|
||||||
black -l 88 --skip-string-normalization .
|
black -l 88 --skip-string-normalization .
|
||||||
isort --profile black .
|
|
||||||
|
|
||||||
# Check the format of Python code.
|
# Check the format of Python code.
|
||||||
fmt-py-check:
|
fmt-py-check:
|
||||||
|
|||||||
137
README.md
137
README.md
@@ -1,95 +1,94 @@
|
|||||||
# PyScript
|
# PyScript
|
||||||
|
|
||||||
## What is PyScript
|
## PyScript is an open source platform for Python in the browser.
|
||||||
|
|
||||||
### Summary
|
Using PyScript is as simple as:
|
||||||
|
|
||||||
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/), [MicroPython](https://micropython.org/) and [WASM](https://webassembly.org/), and modern web technologies.
|
```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>
|
||||||
|
```
|
||||||
|
|
||||||
To get started see the [Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
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.
|
||||||
|
|
||||||
For examples see [here](https://pyscript.com/@examples).
|
To start building, read the
|
||||||
|
[Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||||
|
|
||||||
|
For example applications, see [here](https://pyscript.com/@examples).
|
||||||
|
|
||||||
Other useful resources:
|
Other useful resources:
|
||||||
|
|
||||||
|
- Our [Home Page](https://pyscript.net/) as an open source project.
|
||||||
- The [official technical docs](https://docs.pyscript.net/).
|
- The [official technical docs](https://docs.pyscript.net/).
|
||||||
- Our current [Home Page](https://pyscript.net/) on the web.
|
- A [YouTube channel](https://www.youtube.com/@PyScriptTV) with helpful videos
|
||||||
- A free-to-use [online editor](https://pyscript.com/) for trying PyScript.
|
and community content.
|
||||||
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in touch .
|
- A free-to-use [online IDE](https://pyscript.com/) for trying PyScript.
|
||||||
|
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in
|
||||||
|
touch .
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: this is a call in which everybody is encouraged to show what they did with PyScript.
|
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.
|
||||||
|
|
||||||
For more details on how to join the calls and up to date schedule, consult the official calendar:
|
For more details on how to join the calls and up to date schedule, consult the
|
||||||
|
official calendar:
|
||||||
|
|
||||||
- [Google calendar](https://calendar.google.com/calendar/u/0/embed?src=d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0@group.calendar.google.com&ctz=UTC) in UTC time;
|
- [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).
|
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
|
||||||
|
|
||||||
### Longer Version
|
## Contribute
|
||||||
|
|
||||||
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.
|
For technical details of the code, please see the [README](core/README.md) in
|
||||||
|
the `core` directory.
|
||||||
|
|
||||||
## Try PyScript
|
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.
|
||||||
|
|
||||||
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page:
|
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||||
|
documentation for more information on how to setup your development environment.
|
||||||
```html
|
|
||||||
<head>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://pyscript.net/releases/2024.6.2/core.css"
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
type="module"
|
|
||||||
src="https://pyscript.net/releases/2024.6.2/core.js"
|
|
||||||
></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="py" terminal>
|
|
||||||
from pyscript import display
|
|
||||||
display("Hello World!") # this goes to the DOM
|
|
||||||
print("Hello terminal") # this goes to the terminal
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can then use PyScript components in your html page. PyScript currently offers various ways of running Python code:
|
|
||||||
|
|
||||||
- `<script type="py">`: can be used to define python code that is executable within the web page.
|
|
||||||
- `<script type="py" src="hello.py">`: same as above, but the python source is fetched from the given URL.
|
|
||||||
- `<script type="py" terminal>`: same as above, but also creates a terminal where to display stdout and stderr (e.g., the output of `print()`); `input()` does not work.
|
|
||||||
- `<script type="py" terminal worker>`: run Python inside a web worker: the terminal is fully functional and `input()` works.
|
|
||||||
- `<py-script>`: same as `<script type="py">`, but it is not recommended because if the code contains HTML tags, they could be parsed wrongly.
|
|
||||||
- `<script type="mpy">`: same as above but use MicroPython instead of Python.
|
|
||||||
|
|
||||||
Check out the [official docs](https://docs.pyscript.net/) for more detailed documentation.
|
|
||||||
|
|
||||||
## How to Contribute
|
|
||||||
|
|
||||||
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 [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
|
|
||||||
|
|
||||||
## Governance
|
## 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.
|
||||||
|
|
||||||
## Release
|
## Supporters
|
||||||
|
|
||||||
To cut a new release of PyScript simply
|
PyScript is an independent open source project.
|
||||||
[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:
|
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
|
||||||
|
core contributors are currently employed by Anaconda to work on PyScript. We
|
||||||
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
would like to acknowledge and celebrate Anaconda's continued support of this
|
||||||
file in the root of the directory and then `node version-update.js`.
|
project. Thank you [Anaconda Inc](https://anaconda.com/)!
|
||||||
- [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.
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
168
core/README.md
Normal file
168
core/README.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# @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.
|
||||||
1931
pyscript.core/package-lock.json → core/package-lock.json
generated
1931
pyscript.core/package-lock.json → core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.5.3",
|
"version": "0.6.39",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"module": "./index.js",
|
"module": "./index.js",
|
||||||
@@ -8,27 +8,47 @@
|
|||||||
"jsdelivr": "./jsdelivr.js",
|
"jsdelivr": "./jsdelivr.js",
|
||||||
"browser": "./index.js",
|
"browser": "./index.js",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./dist/",
|
||||||
|
"./src/",
|
||||||
|
"./types/",
|
||||||
|
"./index.js",
|
||||||
|
"./jsdelivr.js",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./types/core.d.ts",
|
"types": "./types/core.d.ts",
|
||||||
"import": "./src/core.js"
|
"import": "./src/core.js"
|
||||||
},
|
},
|
||||||
|
"./js": {
|
||||||
|
"types": "./types/core.d.ts",
|
||||||
|
"import": "./dist/core.js"
|
||||||
|
},
|
||||||
"./css": {
|
"./css": {
|
||||||
"import": "./dist/core.css"
|
"import": "./dist/core.css"
|
||||||
},
|
},
|
||||||
|
"./storage": {
|
||||||
|
"import": "./dist/storage.js"
|
||||||
|
},
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "npx static-handler --coi .",
|
"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 && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:mpy; fi",
|
"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:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
"build:flatted": "node rollup/flatted.cjs",
|
"build:flatted": "node rollup/flatted.cjs",
|
||||||
"build:plugins": "node rollup/plugins.cjs",
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
"build:stdlib": "node rollup/stdlib.cjs",
|
"build:stdlib": "node rollup/stdlib.cjs",
|
||||||
"build:3rd-party": "node rollup/3rd-party.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",
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/mpy.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
"test:ws": "bun test/ws/index.js & playwright test test/ws.spec.js",
|
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
|
||||||
"dev": "node dev.cjs",
|
"dev": "node dev.cjs",
|
||||||
"release": "npm run build && npm run zip",
|
"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",
|
"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",
|
||||||
@@ -43,37 +63,42 @@
|
|||||||
"license": "APACHE-2.0",
|
"license": "APACHE-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ungap/with-resolvers": "^0.1.0",
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
|
"@webreflection/idb-map": "^0.3.2",
|
||||||
|
"add-promise-listener": "^0.1.3",
|
||||||
"basic-devtools": "^0.1.6",
|
"basic-devtools": "^0.1.6",
|
||||||
"polyscript": "^0.15.0",
|
"polyscript": "^0.16.22",
|
||||||
|
"sabayon": "^0.6.6",
|
||||||
"sticky-module": "^0.1.1",
|
"sticky-module": "^0.1.1",
|
||||||
"to-json-callback": "^0.1.1",
|
"to-json-callback": "^0.1.1",
|
||||||
"type-checked-collections": "^0.1.7"
|
"type-checked-collections": "^0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/commands": "^6.6.0",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.7",
|
||||||
"@codemirror/language": "^6.10.2",
|
"@codemirror/language": "^6.11.0",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.30.0",
|
"@codemirror/view": "^6.36.4",
|
||||||
"@playwright/test": "^1.45.3",
|
"@playwright/test": "^1.51.1",
|
||||||
"@rollup/plugin-commonjs": "^26.0.1",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@webreflection/toml-j0.4": "^1.1.3",
|
"@webreflection/toml-j0.4": "^1.1.3",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"bun": "^1.1.21",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"chokidar": "^3.6.0",
|
"bun": "^1.2.5",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"codedent": "^0.1.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.22.0",
|
||||||
"flatted": "^3.3.1",
|
"flatted": "^3.3.3",
|
||||||
"rollup": "^4.20.0",
|
"rollup": "^4.36.0",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.4.3",
|
"static-handler": "^0.5.3",
|
||||||
"typescript": "^5.5.4",
|
"string-width": "^7.2.0",
|
||||||
"xterm": "^5.3.0",
|
"typescript": "^5.8.2",
|
||||||
"xterm-readline": "^1.1.1"
|
"xterm-readline": "^1.1.2"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -46,7 +46,7 @@ const modules = {
|
|||||||
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||||
|
|
||||||
// xterm
|
// xterm
|
||||||
"xterm.js": resolve("xterm"),
|
"xterm.js": resolve("@xterm/xterm"),
|
||||||
"xterm-readline.js": resolve("xterm-readline"),
|
"xterm-readline.js": resolve("xterm-readline"),
|
||||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
b.text(),
|
b.text(),
|
||||||
@@ -54,9 +54,9 @@ const modules = {
|
|||||||
"xterm_addon-web-links.js": fetch(
|
"xterm_addon-web-links.js": fetch(
|
||||||
`${CDN}/@xterm/addon-web-links/+esm`,
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
).then((b) => b.text()),
|
).then((b) => b.text()),
|
||||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
"xterm.css": fetch(
|
||||||
(b) => b.text(),
|
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
||||||
),
|
).then((b) => b.text()),
|
||||||
|
|
||||||
// codemirror
|
// codemirror
|
||||||
"codemirror.js": reBundle("codemirror"),
|
"codemirror.js": reBundle("codemirror"),
|
||||||
73
core/rollup/build_test_index.cjs
Normal file
73
core/rollup/build_test_index.cjs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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>
|
||||||
|
`,
|
||||||
|
);
|
||||||
@@ -40,4 +40,17 @@ export default [
|
|||||||
warn(warning);
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
@@ -13,7 +13,12 @@ for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
|
|||||||
plugins.push(
|
plugins.push(
|
||||||
// this comment is needed to avoid bundlers eagerly embedding lazy
|
// this comment is needed to avoid bundlers eagerly embedding lazy
|
||||||
// dependencies, causing all sort of issues once in production
|
// dependencies, causing all sort of issues once in production
|
||||||
` ${key}: () => import(/* webpackIgnore: true */ ${value}),`,
|
// ⚠️ THIS HAS TO BE LIKE THIS or prettier changes it every single time
|
||||||
|
` ${key}: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
${value}
|
||||||
|
),`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
66
core/rollup/stdlib.cjs
Normal file
66
core/rollup/stdlib.cjs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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`,
|
||||||
|
);
|
||||||
3
core/src/3rd-party/toml.js
vendored
Normal file
3
core/src/3rd-party/toml.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm-readline.js
vendored
Normal file
7
core/src/3rd-party/xterm-readline.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm.css
vendored
Normal file
7
core/src/3rd-party/xterm.css
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Minified by jsDelivr using clean-css v5.3.2.
|
||||||
|
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||||
7
core/src/3rd-party/xterm.js
vendored
Normal file
7
core/src/3rd-party/xterm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm_addon-fit.js
vendored
Normal file
7
core/src/3rd-party/xterm_addon-fit.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.37.0.
|
||||||
|
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var e,t,r={exports:{}};self;var s=r.exports=(e=t={},Object.defineProperty(e,"__esModule",{value:!0}),e.FitAddon=void 0,e.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(s.getPropertyValue("height")),o=Math.max(0,parseInt(s.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=i-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=o-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}},t),i=r.exports.FitAddon,o=r.exports.__esModule;export{i as FitAddon,o as __esModule,s as default};
|
||||||
7
core/src/3rd-party/xterm_addon-web-links.js
vendored
Normal file
7
core/src/3rd-party/xterm_addon-web-links.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.37.0.
|
||||||
|
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var e={exports:{}};self;var t=e.exports=(()=>{var e={6:(e,t)=>{function r(e){try{const t=new URL(e),r=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(r.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,r,n={}){this._terminal=e,this._regex=t,this._handler=r,this._options=n}provideLinks(e,t){const r=n.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(r))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,r)=>{if(this._options.hover){const{range:n}=e;this._options.hover(t,r,n)}},e)))}};class n{static computeLink(e,t,o,s){const i=new RegExp(t.source,(t.flags||"")+"g"),[a,l]=n._getWindowedLineStrings(e-1,o),c=a.join("");let d;const p=[];for(;d=i.exec(c);){const e=d[0];if(!r(e))continue;const[t,i]=n._mapStrIdx(o,l,0,d.index),[a,c]=n._mapStrIdx(o,t,i,e.length);if(-1===t||-1===i||-1===a||-1===c)continue;const h={start:{x:i+1,y:t+1},end:{x:c,y:a+1}};p.push({range:h,text:e,activate:s})}return p}static _getWindowedLineStrings(e,t){let r,n=e,o=e,s=0,i="";const a=[];if(r=t.buffer.active.getLine(e)){const e=r.translateToString(!0);if(r.isWrapped&&" "!==e[0]){for(s=0;(r=t.buffer.active.getLine(--n))&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),r.isWrapped&&-1===i.indexOf(" ")););a.reverse()}for(a.push(e),s=0;(r=t.buffer.active.getLine(++o))&&r.isWrapped&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),-1===i.indexOf(" ")););}return[a,n]}static _mapStrIdx(e,t,r,n){const o=e.buffer.active,s=o.getNullCell();let i=r;for(;n;){const e=o.getLine(t);if(!e)return[-1,-1];for(let r=i;r<e.length;++r){e.getCell(r,s);const i=s.getChars();if(s.getWidth()&&(n-=i.length||1,r===e.length-1&&""===i)){const e=o.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,s),2===s.getWidth()&&(n+=1))}if(n<0)return[t,r]}t++,i=0}return[t,i]}}t.LinkComputer=n}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=r(6),o=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function s(e,t){const r=window.open();if(r){try{r.opener=null}catch{}r.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=s,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const r=this._options,n=r.urlRegex||o;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,n,this._handler,r))}dispose(){this._linkProvider?.dispose()}}})(),n})(),r=e.exports.WebLinksAddon,n=e.exports.__esModule;export{r as WebLinksAddon,n as __esModule,t as default};
|
||||||
@@ -25,7 +25,7 @@ const badURL = (url, expected = "") => {
|
|||||||
* @param {string?} type the optional type to enforce
|
* @param {string?} type the optional type to enforce
|
||||||
* @returns {{json: boolean, toml: boolean, text: string}}
|
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||||
*/
|
*/
|
||||||
const configDetails = async (config, type) => {
|
export const configDetails = async (config, type) => {
|
||||||
let text = config?.trim();
|
let text = config?.trim();
|
||||||
// we only support an object as root config
|
// we only support an object as root config
|
||||||
let url = "",
|
let url = "",
|
||||||
@@ -56,7 +56,7 @@ const syntaxError = (type, url, { message }) => {
|
|||||||
const configs = new Map();
|
const configs = new Map();
|
||||||
|
|
||||||
for (const [TYPE] of TYPES) {
|
for (const [TYPE] of TYPES) {
|
||||||
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
/** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||||
let plugins;
|
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} ) */
|
/** @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} ) */
|
||||||
@@ -135,6 +135,7 @@ for (const [TYPE] of TYPES) {
|
|||||||
|
|
||||||
// parse all plugins and optionally ignore only
|
// parse all plugins and optionally ignore only
|
||||||
// those flagged as "undesired" via `!` prefix
|
// those flagged as "undesired" via `!` prefix
|
||||||
|
plugins = async () => {
|
||||||
const toBeAwaited = [];
|
const toBeAwaited = [];
|
||||||
for (const [key, value] of Object.entries(allPlugins)) {
|
for (const [key, value] of Object.entries(allPlugins)) {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -146,11 +147,12 @@ for (const [TYPE] of TYPES) {
|
|||||||
}
|
}
|
||||||
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||||
toBeAwaited.push(value().then(({ default: p }) => p));
|
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||||
|
} else if (key === "error") {
|
||||||
|
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return await Promise.all(toBeAwaited);
|
||||||
// assign plugins as Promise.all only if needed
|
};
|
||||||
plugins = Promise.all(toBeAwaited);
|
|
||||||
|
|
||||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||||
}
|
}
|
||||||
61
core/src/core.css
Normal file
61
core/src/core.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
py-script,
|
||||||
|
py-config,
|
||||||
|
mpy-script,
|
||||||
|
mpy-config {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PyEditor */
|
||||||
|
.py-editor-box,
|
||||||
|
.mpy-editor-box {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.py-editor-input,
|
||||||
|
.mpy-editor-input {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.py-editor-box::before,
|
||||||
|
.mpy-editor-box::before {
|
||||||
|
content: attr(data-env);
|
||||||
|
display: block;
|
||||||
|
font-size: x-small;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
.py-editor-output,
|
||||||
|
.mpy-editor-output {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.py-editor-run-button,
|
||||||
|
.mpy-editor-run-button {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.py-editor-box:hover .py-editor-run-button,
|
||||||
|
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||||
|
.py-editor-run-button:focus,
|
||||||
|
.py-editor-run-button.running,
|
||||||
|
.mpy-editor-run-button:focus,
|
||||||
|
.mpy-editor-run-button.running {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
py-terminal span,
|
||||||
|
mpy-terminal span {
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.pyscript-fs {
|
||||||
|
border-radius: 8px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.pyscript-fs > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
@@ -33,10 +33,19 @@ import {
|
|||||||
createFunction,
|
createFunction,
|
||||||
inputFailure,
|
inputFailure,
|
||||||
} from "./hooks.js";
|
} from "./hooks.js";
|
||||||
|
import * as fs from "./fs.js";
|
||||||
|
|
||||||
|
import codemirror from "./plugins/codemirror.js";
|
||||||
|
export { codemirror };
|
||||||
|
|
||||||
import { stdlib, optional } from "./stdlib.js";
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
export { stdlib, optional, inputFailure };
|
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
|
// generic helper to disambiguate between custom element and script
|
||||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||||
|
|
||||||
@@ -159,6 +168,8 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// enrich the Python env with some JS utility for main
|
// enrich the Python env with some JS utility for main
|
||||||
interpreter.registerJsModule("_pyscript", {
|
interpreter.registerJsModule("_pyscript", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
|
fs,
|
||||||
|
interpreter,
|
||||||
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||||
get target() {
|
get target() {
|
||||||
return isScript(currentElement)
|
return isScript(currentElement)
|
||||||
@@ -174,7 +185,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// ensure plugins are bootstrapped already before custom type definition
|
// ensure plugins are bootstrapped already before custom type definition
|
||||||
// NOTE: we cannot top-level await in here as plugins import other utilities
|
// 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.
|
// from core.js itself so that custom definition should not be blocking.
|
||||||
plugins.then(() => {
|
plugins().then(() => {
|
||||||
// possible early errors sent by polyscript
|
// possible early errors sent by polyscript
|
||||||
const errors = new Map();
|
const errors = new Map();
|
||||||
|
|
||||||
81
core/src/fs.js
Normal file
81
core/src/fs.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import IDBMap from "@webreflection/idb-map";
|
||||||
|
import { assign } from "polyscript/exports";
|
||||||
|
import { $$ } from "basic-devtools";
|
||||||
|
|
||||||
|
const stop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚠️ these two constants MUST be passed as `fs`
|
||||||
|
// within the worker onBeforeRunAsync hook!
|
||||||
|
export const NAMESPACE = "@pyscript.fs";
|
||||||
|
export const ERROR = "storage permissions not granted";
|
||||||
|
|
||||||
|
export const idb = new IDBMap(NAMESPACE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask a user action via dialog and returns the directory handler once granted.
|
||||||
|
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||||
|
* @returns {Promise<FileSystemDirectoryHandle>}
|
||||||
|
*/
|
||||||
|
export const getFileSystemDirectoryHandle = async (options) => {
|
||||||
|
if (!("showDirectoryPicker" in globalThis)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("showDirectoryPicker is not supported"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers();
|
||||||
|
|
||||||
|
const how = { id: "pyscript", mode: "readwrite", ...options };
|
||||||
|
if (options.hint) how.startIn = options.hint;
|
||||||
|
|
||||||
|
const transient = async () => {
|
||||||
|
try {
|
||||||
|
/* eslint-disable */
|
||||||
|
const handler = await showDirectoryPicker(how);
|
||||||
|
/* eslint-enable */
|
||||||
|
if ((await handler.requestPermission(how)) === "granted") {
|
||||||
|
resolve(handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch ({ message }) {
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// in case the user decided to attach the event itself
|
||||||
|
// as opposite of relying our dialog walkthrough
|
||||||
|
if (navigator.userActivation?.isActive) {
|
||||||
|
if (!(await transient())) reject(new Error(ERROR));
|
||||||
|
} else {
|
||||||
|
const dialog = assign(document.createElement("dialog"), {
|
||||||
|
className: "pyscript-fs",
|
||||||
|
innerHTML: [
|
||||||
|
"<strong>ℹ️ Persistent FileSystem</strong><hr>",
|
||||||
|
"<p><small>PyScript would like to access a local folder.</small></p>",
|
||||||
|
"<div><button title='ok'>✅ Authorize</button>",
|
||||||
|
"<button title='cancel'>❌</button></div>",
|
||||||
|
].join(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ok, cancel] = $$("button", dialog);
|
||||||
|
|
||||||
|
ok.addEventListener("click", async (event) => {
|
||||||
|
stop(event);
|
||||||
|
if (await transient()) dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancel.addEventListener("click", async (event) => {
|
||||||
|
stop(event);
|
||||||
|
reject(new Error(ERROR));
|
||||||
|
dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(dialog).showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
@@ -88,7 +88,19 @@ export const hooks = {
|
|||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onBeforeRun: new SetFunction(),
|
onBeforeRun: new SetFunction(),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onBeforeRunAsync: new SetFunction(),
|
onBeforeRunAsync: new SetFunction([
|
||||||
|
({ interpreter }) => {
|
||||||
|
interpreter.registerJsModule("_pyscript", {
|
||||||
|
// cannot be imported from fs.js
|
||||||
|
// because this code is stringified
|
||||||
|
fs: {
|
||||||
|
ERROR: "storage permissions not granted",
|
||||||
|
NAMESPACE: "@pyscript.fs",
|
||||||
|
},
|
||||||
|
interpreter,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
]),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onAfterRun: new SetFunction(),
|
onAfterRun: new SetFunction(),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
38
core/src/plugins.js
Normal file
38
core/src/plugins.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||||
|
export default {
|
||||||
|
codemirror: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/codemirror.js"
|
||||||
|
),
|
||||||
|
["deprecations-manager"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/deprecations-manager.js"
|
||||||
|
),
|
||||||
|
donkey: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/donkey.js"
|
||||||
|
),
|
||||||
|
error: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/error.js"
|
||||||
|
),
|
||||||
|
["py-editor"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-editor.js"
|
||||||
|
),
|
||||||
|
["py-game"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-game.js"
|
||||||
|
),
|
||||||
|
["py-terminal"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-terminal.js"
|
||||||
|
),
|
||||||
|
};
|
||||||
31
core/src/plugins/codemirror.js
Normal file
31
core/src/plugins/codemirror.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// lazy loaded on-demand codemirror related files
|
||||||
|
export default {
|
||||||
|
get core() {
|
||||||
|
return import(/* webpackIgnore: true */ "../3rd-party/codemirror.js");
|
||||||
|
},
|
||||||
|
get state() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get python() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get language() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get view() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get commands() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript Derepcations Plugin
|
// PyScript Derepcations Plugin
|
||||||
import { hooks } from "../core.js";
|
|
||||||
import { notify } from "./error.js";
|
import { notify } from "./error.js";
|
||||||
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
// react lazily on PyScript bootstrap
|
// react lazily on PyScript bootstrap
|
||||||
hooks.main.onReady.add(checkDeprecations);
|
hooks.main.onReady.add(checkDeprecations);
|
||||||
121
core/src/plugins/donkey.js
Normal file
121
core/src/plugins/donkey.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import addPromiseListener from "add-promise-listener";
|
||||||
|
import { assign, dedent } from "polyscript/exports";
|
||||||
|
|
||||||
|
const { stringify } = JSON;
|
||||||
|
|
||||||
|
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||||
|
|
||||||
|
const donkey = ({
|
||||||
|
type = "py",
|
||||||
|
persistent,
|
||||||
|
terminal,
|
||||||
|
config,
|
||||||
|
serviceWorker,
|
||||||
|
}) => {
|
||||||
|
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
|
||||||
|
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
|
||||||
|
|
||||||
|
const src = URL.createObjectURL(
|
||||||
|
new Blob([
|
||||||
|
[
|
||||||
|
// this array is to better minify this code once in production
|
||||||
|
"from pyscript import sync, config",
|
||||||
|
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
|
||||||
|
"__locals__ = {}",
|
||||||
|
'if config["type"] == "py":',
|
||||||
|
" import sys",
|
||||||
|
" def __error__(_):",
|
||||||
|
" info = sys.exc_info()",
|
||||||
|
" return __message__(info[0], info[1])",
|
||||||
|
"else:",
|
||||||
|
" __error__ = lambda e: __message__(e.__class__, e.value)",
|
||||||
|
"def execute(code):",
|
||||||
|
` try: return ${invoke("exec", args)};`,
|
||||||
|
" except Exception as e: print(__error__(e));",
|
||||||
|
"def evaluate(code):",
|
||||||
|
` try: return ${invoke("eval", args)};`,
|
||||||
|
" except Exception as e: print(__error__(e));",
|
||||||
|
"sync.execute = execute",
|
||||||
|
"sync.evaluate = evaluate",
|
||||||
|
].join("\n"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// create the script that exposes the code to execute or evaluate
|
||||||
|
const script = assign(document.createElement("script"), { type, src });
|
||||||
|
script.toggleAttribute("worker", true);
|
||||||
|
script.toggleAttribute("terminal", true);
|
||||||
|
if (terminal) script.setAttribute("target", terminal);
|
||||||
|
if (config) {
|
||||||
|
script.setAttribute(
|
||||||
|
"config",
|
||||||
|
typeof config === "string" ? config : stringify(config),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
||||||
|
|
||||||
|
return addPromiseListener(
|
||||||
|
document.body.appendChild(script),
|
||||||
|
`${type}:done`,
|
||||||
|
{ stopPropagation: true },
|
||||||
|
).then(() => {
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
return script;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const utils = async (options) => {
|
||||||
|
const script = await donkey(options);
|
||||||
|
const { xworker, process, terminal } = script;
|
||||||
|
const { execute, evaluate } = xworker.sync;
|
||||||
|
script.remove();
|
||||||
|
return {
|
||||||
|
xworker,
|
||||||
|
process,
|
||||||
|
terminal,
|
||||||
|
execute,
|
||||||
|
evaluate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (options = {}) => {
|
||||||
|
let farmer = await utils(options);
|
||||||
|
let working = false;
|
||||||
|
const kill = () => {
|
||||||
|
if (farmer) {
|
||||||
|
farmer.xworker.terminate();
|
||||||
|
farmer.terminal.dispose();
|
||||||
|
farmer = null;
|
||||||
|
}
|
||||||
|
working = false;
|
||||||
|
};
|
||||||
|
const reload = async () => {
|
||||||
|
kill();
|
||||||
|
farmer = await utils(options);
|
||||||
|
};
|
||||||
|
const asyncTask = (method) => async (code) => {
|
||||||
|
// race condition ... a new task has been
|
||||||
|
// assigned while the previous one didn't finish
|
||||||
|
if (working) await reload();
|
||||||
|
working = true;
|
||||||
|
try {
|
||||||
|
return await farmer[method](dedent(code));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
working = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const asyncMethod = (method) => async () => {
|
||||||
|
if (working) await reload();
|
||||||
|
else farmer?.terminal[method]();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
process: asyncTask("process"),
|
||||||
|
execute: asyncTask("execute"),
|
||||||
|
evaluate: asyncTask("evaluate"),
|
||||||
|
clear: asyncMethod("clear"),
|
||||||
|
reset: asyncMethod("reset"),
|
||||||
|
kill,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
// PyScript Error Plugin
|
// PyScript Error Plugin
|
||||||
|
import { buffered } from "polyscript/exports";
|
||||||
import { hooks } from "../core.js";
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
|
let dontBotherDOM = false;
|
||||||
|
export function notOnDOM() {
|
||||||
|
dontBotherDOM = true;
|
||||||
|
}
|
||||||
|
|
||||||
hooks.main.onReady.add(function override(pyScript) {
|
hooks.main.onReady.add(function override(pyScript) {
|
||||||
// be sure this override happens only once
|
// be sure this override happens only once
|
||||||
hooks.main.onReady.delete(override);
|
hooks.main.onReady.delete(override);
|
||||||
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
|
|||||||
// trap generic `stderr` to propagate to it regardless
|
// trap generic `stderr` to propagate to it regardless
|
||||||
const { stderr } = pyScript.io;
|
const { stderr } = pyScript.io;
|
||||||
|
|
||||||
// override it with our own logic
|
const cb = (error, ...rest) => {
|
||||||
pyScript.io.stderr = (error, ...rest) => {
|
|
||||||
notify(error.message || error);
|
notify(error.message || error);
|
||||||
// let other plugins or stderr hook, if any, do the rest
|
// let other plugins or stderr hook, if any, do the rest
|
||||||
return stderr(error, ...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
|
// be sure uncaught Python errors are also visible
|
||||||
addEventListener("error", ({ message }) => {
|
addEventListener("error", ({ message }) => {
|
||||||
if (message.startsWith("Uncaught PythonError")) notify(message);
|
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||||
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
|
|||||||
* @param {string} message
|
* @param {string} message
|
||||||
*/
|
*/
|
||||||
export function notify(message) {
|
export function notify(message) {
|
||||||
|
if (dontBotherDOM) return;
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "py-error";
|
div.className = "py-error";
|
||||||
div.textContent = message;
|
div.textContent = message;
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
// PyScript py-editor plugin
|
// PyScript py-editor plugin
|
||||||
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import codemirror from "./codemirror.js";
|
||||||
|
|
||||||
const RUN_BUTTON = `<svg style="height: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>`;
|
const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
|
||||||
|
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
const getID = (type) => `${type}-editor-${id++}`;
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
const envs = new Map();
|
const envs = new Map();
|
||||||
const configs = new Map();
|
const configs = new Map();
|
||||||
|
const editors = new WeakMap();
|
||||||
|
|
||||||
const hooks = {
|
const hooks = {
|
||||||
worker: {
|
worker: {
|
||||||
@@ -23,12 +27,23 @@ const hooks = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validate = (config, result) => {
|
||||||
|
if (typeof result === "boolean") throw `Invalid source: ${config}`;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelatedScript = (target, type) => {
|
||||||
|
const editor = target.closest(`.${type}-editor-box`);
|
||||||
|
return editor?.parentNode?.previousElementSibling;
|
||||||
|
};
|
||||||
|
|
||||||
async function execute({ currentTarget }) {
|
async function execute({ currentTarget }) {
|
||||||
const { env, pySrc, outDiv } = this;
|
const { env, pySrc, outDiv } = this;
|
||||||
const hasRunButton = !!currentTarget;
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
if (hasRunButton) {
|
if (hasRunButton) {
|
||||||
currentTarget.disabled = true;
|
currentTarget.classList.add("running");
|
||||||
|
currentTarget.innerHTML = STOP_BUTTON;
|
||||||
outDiv.innerHTML = "";
|
outDiv.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,26 +55,49 @@ async function execute({ currentTarget }) {
|
|||||||
};
|
};
|
||||||
const { config } = this;
|
const { config } = this;
|
||||||
if (config) {
|
if (config) {
|
||||||
|
// verify that config can be parsed and used
|
||||||
|
try {
|
||||||
details.configURL = relative_url(config);
|
details.configURL = relative_url(config);
|
||||||
if (config.endsWith(".toml")) {
|
if (config.endsWith(".toml")) {
|
||||||
const [{ parse }, toml] = await Promise.all([
|
const [{ parse }, toml] = await Promise.all([
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/toml.js"),
|
import(
|
||||||
fetch(config).then((r) => r.text()),
|
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||||
|
),
|
||||||
|
fetch(config).then((r) => r.ok && r.text()),
|
||||||
]);
|
]);
|
||||||
details.config = parse(toml);
|
details.config = parse(validate(config, toml));
|
||||||
} else if (config.endsWith(".json")) {
|
} else if (config.endsWith(".json")) {
|
||||||
details.config = await fetch(config).then((r) => r.json());
|
const json = await fetch(config).then(
|
||||||
|
(r) => r.ok && r.json(),
|
||||||
|
);
|
||||||
|
details.config = validate(config, json);
|
||||||
} else {
|
} else {
|
||||||
details.configURL = relative_url("./config.txt");
|
details.configURL = relative_url("./config.txt");
|
||||||
details.config = JSON.parse(config);
|
details.config = JSON.parse(config);
|
||||||
}
|
}
|
||||||
details.version = offline_interpreter(details.config);
|
details.version = offline_interpreter(details.config);
|
||||||
|
} catch (error) {
|
||||||
|
notify(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
details.config = {};
|
details.config = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
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 script = getRelatedScript(currentTarget, type);
|
||||||
|
if (script) {
|
||||||
|
defineProperties(script, { xworker: { value: xworker } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
const { promise, resolve } = Promise.withResolvers();
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
envs.set(env, promise);
|
envs.set(env, promise);
|
||||||
@@ -74,23 +112,36 @@ async function execute({ currentTarget }) {
|
|||||||
return envs.get(env).then((xworker) => {
|
return envs.get(env).then((xworker) => {
|
||||||
xworker.onerror = ({ error }) => {
|
xworker.onerror = ({ error }) => {
|
||||||
if (hasRunButton) {
|
if (hasRunButton) {
|
||||||
outDiv.innerHTML += `<span style='color:red'>${
|
outDiv.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`<span style='color:red'>${
|
||||||
error.message || error
|
error.message || error
|
||||||
}</span>\n`;
|
}</span>\n`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enable = () => {
|
const enable = () => {
|
||||||
if (hasRunButton) currentTarget.disabled = false;
|
if (hasRunButton) {
|
||||||
|
currentTarget.classList.remove("running");
|
||||||
|
currentTarget.innerHTML = RUN_BUTTON;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
sync.write = (str) => {
|
sync.write = (str) => {
|
||||||
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||||
|
else console.log(str);
|
||||||
};
|
};
|
||||||
sync.writeErr = (str) => {
|
sync.writeErr = (str) => {
|
||||||
if (hasRunButton) {
|
if (hasRunButton) {
|
||||||
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
outDiv.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`<span style='color:red'>${str}</span>\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notify(str);
|
||||||
|
console.error(str);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
sync.runAsync(pySrc).then(enable, enable);
|
sync.runAsync(pySrc).then(enable, enable);
|
||||||
@@ -103,6 +154,24 @@ const makeRunButton = (handler, type) => {
|
|||||||
runButton.innerHTML = RUN_BUTTON;
|
runButton.innerHTML = RUN_BUTTON;
|
||||||
runButton.setAttribute("aria-label", "Python Script Run Button");
|
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||||
runButton.addEventListener("click", async (event) => {
|
runButton.addEventListener("click", async (event) => {
|
||||||
|
if (
|
||||||
|
runButton.classList.contains("running") &&
|
||||||
|
confirm("Stop evaluating this code?")
|
||||||
|
) {
|
||||||
|
const script = getRelatedScript(runButton, type);
|
||||||
|
if (script) {
|
||||||
|
const editor = editors.get(script);
|
||||||
|
const content = editor.state.doc.toString();
|
||||||
|
const clone = script.cloneNode(true);
|
||||||
|
clone.type = `${type}-editor`;
|
||||||
|
clone.textContent = content;
|
||||||
|
script.xworker.terminate();
|
||||||
|
script.nextElementSibling.remove();
|
||||||
|
script.replaceWith(clone);
|
||||||
|
editors.delete(script);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
runButton.blur();
|
runButton.blur();
|
||||||
await handler.handleEvent(event);
|
await handler.handleEvent(event);
|
||||||
});
|
});
|
||||||
@@ -154,14 +223,12 @@ const init = async (script, type, interpreter) => {
|
|||||||
{ keymap },
|
{ keymap },
|
||||||
{ defaultKeymap, indentWithTab },
|
{ defaultKeymap, indentWithTab },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
codemirror.core,
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
codemirror.state,
|
||||||
import(
|
codemirror.python,
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
codemirror.language,
|
||||||
),
|
codemirror.view,
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
codemirror.commands,
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
|
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let isSetup = script.hasAttribute("setup");
|
let isSetup = script.hasAttribute("setup");
|
||||||
@@ -187,9 +254,22 @@ const init = async (script, type, interpreter) => {
|
|||||||
|
|
||||||
configs.set(env, hasConfig);
|
configs.set(env, hasConfig);
|
||||||
|
|
||||||
let source = script.src
|
let source = script.textContent;
|
||||||
? await fetch(script.src).then((b) => b.text())
|
|
||||||
: 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 = {
|
const context = {
|
||||||
// allow the listener to be overridden at distance
|
// allow the listener to be overridden at distance
|
||||||
handleEvent: execute,
|
handleEvent: execute,
|
||||||
@@ -250,9 +330,11 @@ const init = async (script, type, interpreter) => {
|
|||||||
/**
|
/**
|
||||||
* Simulate a setup node overriding the source to evaluate.
|
* Simulate a setup node overriding the source to evaluate.
|
||||||
* @param {string} code the Python code 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.
|
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||||
*/
|
*/
|
||||||
value(code) {
|
value(code, asRunButtonAction = false) {
|
||||||
|
if (asRunButtonAction) return listener();
|
||||||
const wasSetup = isSetup;
|
const wasSetup = isSetup;
|
||||||
const wasSource = source;
|
const wasSource = source;
|
||||||
isSetup = true;
|
isSetup = true;
|
||||||
@@ -268,14 +350,14 @@ const init = async (script, type, interpreter) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const notify = () => {
|
const notifyEditor = () => {
|
||||||
const event = new Event(`${type}-editor`, { bubbles: true });
|
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||||
script.dispatchEvent(event);
|
script.dispatchEvent(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSetup) {
|
if (isSetup) {
|
||||||
await context.handleEvent({ currentTarget: null });
|
await context.handleEvent({ currentTarget: null });
|
||||||
notify();
|
notifyEditor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,8 +415,9 @@ const init = async (script, type, interpreter) => {
|
|||||||
doc,
|
doc,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
editors.set(script, editor);
|
||||||
editor.focus();
|
editor.focus();
|
||||||
notify();
|
notifyEditor();
|
||||||
};
|
};
|
||||||
|
|
||||||
// avoid too greedy MutationObserver operations at distance
|
// avoid too greedy MutationObserver operations at distance
|
||||||
112
core/src/plugins/py-game.js
Normal file
112
core/src/plugins/py-game.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
dedent,
|
||||||
|
define,
|
||||||
|
createProgress,
|
||||||
|
loadProgress,
|
||||||
|
} from "polyscript/exports";
|
||||||
|
|
||||||
|
import { stdlib } from "../core.js";
|
||||||
|
import { configDetails } from "../config.js";
|
||||||
|
import { getText } from "../fetch.js";
|
||||||
|
|
||||||
|
const progress = createProgress("py-game");
|
||||||
|
|
||||||
|
const inputPatch = `
|
||||||
|
import builtins
|
||||||
|
def input(prompt=""):
|
||||||
|
import js
|
||||||
|
return js.prompt(prompt)
|
||||||
|
|
||||||
|
builtins.input = input
|
||||||
|
del builtins
|
||||||
|
del input
|
||||||
|
`;
|
||||||
|
|
||||||
|
let toBeWarned = true;
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
main: {
|
||||||
|
onReady: async (wrap, script) => {
|
||||||
|
if (toBeWarned) {
|
||||||
|
toBeWarned = false;
|
||||||
|
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
if (script.hasAttribute("config")) {
|
||||||
|
const value = script.getAttribute("config");
|
||||||
|
const { json, toml, text, url } = await configDetails(value);
|
||||||
|
if (json) config = JSON.parse(text);
|
||||||
|
else if (toml) {
|
||||||
|
const { parse } = await import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
config = parse(text);
|
||||||
|
}
|
||||||
|
if (config.packages) {
|
||||||
|
await wrap.interpreter.loadPackage("micropip");
|
||||||
|
const micropip = wrap.interpreter.pyimport("micropip");
|
||||||
|
await micropip.install(config.packages, {
|
||||||
|
keep_going: true,
|
||||||
|
});
|
||||||
|
micropip.destroy();
|
||||||
|
}
|
||||||
|
await loadProgress(
|
||||||
|
"py-game",
|
||||||
|
progress,
|
||||||
|
wrap.interpreter,
|
||||||
|
config,
|
||||||
|
url ? new URL(url, location.href).href : location.href,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap.interpreter.registerJsModule("_pyscript", {
|
||||||
|
PyWorker() {
|
||||||
|
throw new Error(
|
||||||
|
"Unable to use PyWorker in py-game scripts",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
js_import: (...urls) =>
|
||||||
|
Promise.all(urls.map((url) => import(url))),
|
||||||
|
get target() {
|
||||||
|
return script.id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrap.interpreter.runPythonAsync(stdlib);
|
||||||
|
wrap.interpreter.runPython(inputPatch);
|
||||||
|
|
||||||
|
let code = dedent(script.textContent);
|
||||||
|
if (script.src) code = await fetch(script.src).then(getText);
|
||||||
|
|
||||||
|
const target = script.getAttribute("target") || "canvas";
|
||||||
|
const canvas = document.getElementById(target);
|
||||||
|
wrap.interpreter.canvas.setCanvas2D(canvas);
|
||||||
|
|
||||||
|
// allow 3rd party to hook themselves right before
|
||||||
|
// the code gets executed
|
||||||
|
const event = new CustomEvent("py-game", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
detail: {
|
||||||
|
canvas,
|
||||||
|
code,
|
||||||
|
config,
|
||||||
|
wrap,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
// run only if the default was not prevented
|
||||||
|
if (!event.defaultPrevented)
|
||||||
|
await wrap.interpreter.runPythonAsync(code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
define("py-game", {
|
||||||
|
config: { packages: ["pygame-ce"] },
|
||||||
|
configURL: new URL("./config.txt", location.href).href,
|
||||||
|
interpreter: "pyodide",
|
||||||
|
env: "py-game",
|
||||||
|
hooks,
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript pyodide terminal plugin
|
// PyScript pyodide terminal plugin
|
||||||
import { hooks, inputFailure } from "../../core.js";
|
|
||||||
import { defineProperties } from "polyscript/exports";
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
import { hooks, inputFailure } from "../../core.js";
|
||||||
|
|
||||||
const bootstrapped = new WeakSet();
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
|||||||
pyterminal_write(String(error.message || error));
|
pyterminal_write(String(error.message || error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sync.pyterminal_stream_write = () => {};
|
||||||
|
|
||||||
// tiny shim of the code module with only interact
|
// tiny shim of the code module with only interact
|
||||||
// to bootstrap a REPL like environment
|
// to bootstrap a REPL like environment
|
||||||
interpreter.registerJsModule("code", {
|
interpreter.registerJsModule("code", {
|
||||||
@@ -71,6 +73,7 @@ export default async (element) => {
|
|||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript py-terminal plugin
|
// PyScript py-terminal plugin
|
||||||
import { hooks } from "../../core.js";
|
|
||||||
import { defineProperties } from "polyscript/exports";
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
import { hooks } from "../../core.js";
|
||||||
|
|
||||||
const bootstrapped = new WeakSet();
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
@@ -126,6 +126,7 @@ export default async (element) => {
|
|||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
});
|
});
|
||||||
|
|
||||||
xworker.sync.is_pyterminal = () => true;
|
xworker.sync.is_pyterminal = () => true;
|
||||||
@@ -136,6 +137,18 @@ export default async (element) => {
|
|||||||
// setup remote thread JS/Python code for whenever the
|
// setup remote thread JS/Python code for whenever the
|
||||||
// worker is ready to become a terminal
|
// worker is ready to become a terminal
|
||||||
hooks.worker.onReady.add(workerReady);
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
|
||||||
|
// @see https://github.com/pyscript/pyscript/issues/2246
|
||||||
|
const patchInput = [
|
||||||
|
"import builtins as _b",
|
||||||
|
"from pyscript import sync as _s",
|
||||||
|
"_b.input = _s.pyterminal_read",
|
||||||
|
"del _b",
|
||||||
|
"del _s",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
hooks.worker.codeBeforeRun.add(patchInput);
|
||||||
|
hooks.worker.codeBeforeRunAsync.add(patchInput);
|
||||||
} else {
|
} else {
|
||||||
// in the main case, just bootstrap XTerm without
|
// in the main case, just bootstrap XTerm without
|
||||||
// allowing any input as that's not possible / awkward
|
// allowing any input as that's not possible / awkward
|
||||||
19
core/src/stdlib/pyscript.js
Normal file
19
core/src/stdlib/pyscript.js
Normal file
File diff suppressed because one or more lines are too long
@@ -30,8 +30,6 @@
|
|||||||
# as it works transparently in both the main thread and worker cases.
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
from polyscript import lazy_py_modules as py_import
|
from polyscript import lazy_py_modules as py_import
|
||||||
from pyscript.display import HTML, display
|
|
||||||
from pyscript.fetch import fetch
|
|
||||||
from pyscript.magic_js import (
|
from pyscript.magic_js import (
|
||||||
RUNNING_IN_WORKER,
|
RUNNING_IN_WORKER,
|
||||||
PyWorker,
|
PyWorker,
|
||||||
@@ -43,19 +41,11 @@ from pyscript.magic_js import (
|
|||||||
sync,
|
sync,
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
from pyscript.storage import Storage, storage
|
from pyscript.storage import Storage, storage
|
||||||
from pyscript.websocket import WebSocket
|
from pyscript.websocket import WebSocket
|
||||||
|
from pyscript.events import when, Event
|
||||||
|
|
||||||
if not RUNNING_IN_WORKER:
|
if not RUNNING_IN_WORKER:
|
||||||
from pyscript.workers import create_named_worker, workers
|
from pyscript.workers import create_named_worker, workers
|
||||||
|
|
||||||
try:
|
|
||||||
from pyscript.event_handling import when
|
|
||||||
except:
|
|
||||||
# TODO: should we remove this? Or at the very least, we should capture
|
|
||||||
# the traceback otherwise it's very hard to debug
|
|
||||||
from pyscript.util import NotSupported
|
|
||||||
|
|
||||||
when = NotSupported(
|
|
||||||
"pyscript.when", "pyscript.when currently not available with this interpreter"
|
|
||||||
)
|
|
||||||
@@ -73,14 +73,14 @@ def _eval_formatter(obj, print_method):
|
|||||||
"""
|
"""
|
||||||
if print_method == "__repr__":
|
if print_method == "__repr__":
|
||||||
return repr(obj)
|
return repr(obj)
|
||||||
elif hasattr(obj, print_method):
|
if hasattr(obj, print_method):
|
||||||
if print_method == "savefig":
|
if print_method == "savefig":
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
obj.savefig(buf, format="png")
|
obj.savefig(buf, format="png")
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
return base64.b64encode(buf.read()).decode("utf-8")
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
return getattr(obj, print_method)()
|
return getattr(obj, print_method)()
|
||||||
elif print_method == "_repr_mimebundle_":
|
if print_method == "_repr_mimebundle_":
|
||||||
return {}, {}
|
return {}, {}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ def _format_mime(obj):
|
|||||||
|
|
||||||
if output is None:
|
if output is None:
|
||||||
continue
|
continue
|
||||||
elif mime_type not in _MIME_RENDERERS:
|
if mime_type not in _MIME_RENDERERS:
|
||||||
not_available.append(mime_type)
|
not_available.append(mime_type)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
@@ -149,9 +149,11 @@ def display(*values, target=None, append=True):
|
|||||||
if target is None:
|
if target is None:
|
||||||
target = current_target()
|
target = current_target()
|
||||||
elif not isinstance(target, str):
|
elif not isinstance(target, str):
|
||||||
raise TypeError(f"target must be str or None, not {target.__class__.__name__}")
|
msg = f"target must be str or None, not {target.__class__.__name__}"
|
||||||
|
raise TypeError(msg)
|
||||||
elif target == "":
|
elif target == "":
|
||||||
raise ValueError("Cannot have an empty target")
|
msg = "Cannot have an empty target"
|
||||||
|
raise ValueError(msg)
|
||||||
elif target.startswith("#"):
|
elif target.startswith("#"):
|
||||||
# note: here target is str and not None!
|
# note: here target is str and not None!
|
||||||
# align with @when behavior
|
# align with @when behavior
|
||||||
@@ -161,9 +163,8 @@ def display(*values, target=None, append=True):
|
|||||||
|
|
||||||
# If target cannot be found on the page, a ValueError is raised
|
# If target cannot be found on the page, a ValueError is raised
|
||||||
if element is None:
|
if element is None:
|
||||||
raise ValueError(
|
msg = f"Invalid selector with id={target}. Cannot be found in the page."
|
||||||
f"Invalid selector with id={target}. Cannot be found in the page."
|
raise ValueError(msg)
|
||||||
)
|
|
||||||
|
|
||||||
# if element is a <script type="py">, it has a 'target' attribute which
|
# if element is a <script type="py">, it has a 'target' attribute which
|
||||||
# points to the visual element holding the displayed values. In that case,
|
# points to the visual element holding the displayed values. In that case,
|
||||||
225
core/src/stdlib/pyscript/events.py
Normal file
225
core/src/stdlib/pyscript/events.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
from pyscript.magic_js import document
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
|
from pyscript.util import is_awaitable
|
||||||
|
from pyscript import config
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
"""
|
||||||
|
Events represent something that may happen at some point in time (usually
|
||||||
|
the future). They're used to coordinate code when the timing of an event is
|
||||||
|
not known in advance (e.g. a button click or a network response).
|
||||||
|
|
||||||
|
An event is triggered with an arbitrary result. If no result is given, then
|
||||||
|
None is assumed as the result.
|
||||||
|
|
||||||
|
Add listener functions to the event, to be called with the result when the
|
||||||
|
event is triggered. The listener functions can be callable or awaitable. If
|
||||||
|
the listener is added several times, it will be called only once.
|
||||||
|
|
||||||
|
If the event was triggered before a listener is added, the listener will be
|
||||||
|
called as soon as it is added, with the result of the event.
|
||||||
|
|
||||||
|
If the event is never triggered, then its listeners will never be called.
|
||||||
|
It's also possible to remove listeners from the event.
|
||||||
|
|
||||||
|
If the result of the event is not available, a ValueError will be raised
|
||||||
|
when trying to access the result property. A RuntimeError will be raised if
|
||||||
|
the event is triggered more than once.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# To contain the listeners to be called when the event is triggered.
|
||||||
|
self._listeners = []
|
||||||
|
# The result associated with the event.
|
||||||
|
self._result = None
|
||||||
|
# A flag to indicate if the event has been triggered.
|
||||||
|
self._triggered = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def triggered(self):
|
||||||
|
"""
|
||||||
|
A boolean flag to indicate if the event has been triggered.
|
||||||
|
"""
|
||||||
|
return self._triggered
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self):
|
||||||
|
"""
|
||||||
|
The result of the event.
|
||||||
|
"""
|
||||||
|
if self.triggered:
|
||||||
|
return self._result
|
||||||
|
msg = "Event has not been triggered yet. No result available."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
def trigger(self, result=None):
|
||||||
|
"""
|
||||||
|
Trigger the event with an arbitrary result to pass into the listeners.
|
||||||
|
An event may only be triggered once (otherwise a RuntimeError is
|
||||||
|
raised).
|
||||||
|
"""
|
||||||
|
if self.triggered:
|
||||||
|
msg = "Event has already been triggered."
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
self._triggered = True
|
||||||
|
self._result = result
|
||||||
|
for listener in self._listeners:
|
||||||
|
self._call_listener(listener)
|
||||||
|
|
||||||
|
def add_listener(self, listener):
|
||||||
|
"""
|
||||||
|
Add a callable/awaitable that listens for the result, when this event
|
||||||
|
is triggered.
|
||||||
|
"""
|
||||||
|
if is_awaitable(listener) or callable(listener):
|
||||||
|
if listener not in self._listeners:
|
||||||
|
self._listeners.append(listener)
|
||||||
|
if self.triggered:
|
||||||
|
# If the event was already triggered, call the listener
|
||||||
|
# immediately with the result.
|
||||||
|
self._call_listener(listener)
|
||||||
|
else:
|
||||||
|
msg = "Listener must be callable or awaitable."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
def remove_listener(self, *args):
|
||||||
|
"""
|
||||||
|
Clear the specified listener functions in *args. If no listeners are
|
||||||
|
provided, clear all the listeners.
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
for listener in args:
|
||||||
|
self._listeners.remove(listener)
|
||||||
|
else:
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
def _call_listener(self, listener):
|
||||||
|
"""
|
||||||
|
Call the referenced listener with the event's result.
|
||||||
|
"""
|
||||||
|
if is_awaitable(listener):
|
||||||
|
asyncio.create_task(listener(self._result))
|
||||||
|
else:
|
||||||
|
listener(self._result)
|
||||||
|
|
||||||
|
|
||||||
|
def when(target, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add an event listener to the target element(s) for the specified event type.
|
||||||
|
|
||||||
|
The target can be a string representing the event type, or an Event object.
|
||||||
|
If the target is an Event object, the event listener will be added to that
|
||||||
|
object. If the target is a string, the event listener will be added to the
|
||||||
|
element(s) that match the (second) selector argument.
|
||||||
|
|
||||||
|
If a (third) handler argument is provided, it will be called when the event
|
||||||
|
is triggered; thus allowing this to be used as both a function and a
|
||||||
|
decorator.
|
||||||
|
"""
|
||||||
|
# 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]) or is_awaitable(args[0])):
|
||||||
|
handler = args[0]
|
||||||
|
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
|
||||||
|
handler = kwargs.pop("handler")
|
||||||
|
# If the target is a string, it is the "older" use of `when` where it
|
||||||
|
# represents the name of a DOM event.
|
||||||
|
if isinstance(target, str):
|
||||||
|
# Extract the selector from the arguments or keyword arguments.
|
||||||
|
selector = args[0] if args else kwargs.pop("selector")
|
||||||
|
if not selector:
|
||||||
|
msg = "No selector provided."
|
||||||
|
raise ValueError(msg)
|
||||||
|
# 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)
|
||||||
|
elif isinstance(selector, Element):
|
||||||
|
elements = [selector._dom_element]
|
||||||
|
elif isinstance(selector, ElementCollection):
|
||||||
|
elements = [el._dom_element for el in selector]
|
||||||
|
else:
|
||||||
|
elements = selector if isinstance(selector, list) else [selector]
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
if config["type"] == "mpy": # Is MicroPython?
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a very ugly hack to get micropython working because
|
||||||
|
`inspect.signature` doesn't exist. It may be actually better
|
||||||
|
to not try any magic for now and raise the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return await func()
|
||||||
|
raise
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a very ugly hack to get micropython working because
|
||||||
|
`inspect.signature` doesn't exist. It may be actually better
|
||||||
|
to not try any magic for now and raise the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return func()
|
||||||
|
raise
|
||||||
|
|
||||||
|
else:
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
if sig.parameters:
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(event):
|
||||||
|
return await func(event)
|
||||||
|
|
||||||
|
else:
|
||||||
|
wrapper = func
|
||||||
|
else:
|
||||||
|
# Function doesn't receive events.
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func()
|
||||||
|
|
||||||
|
wrapper = wraps(func)(wrapper)
|
||||||
|
if isinstance(target, Event):
|
||||||
|
# The target is a single Event object.
|
||||||
|
target.add_listener(wrapper)
|
||||||
|
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
|
||||||
|
# The target is a list of Event objects.
|
||||||
|
for evt in target:
|
||||||
|
evt.add_listener(wrapper)
|
||||||
|
else:
|
||||||
|
# The target is a string representing an event type, and so a
|
||||||
|
# DOM element or collection of elements is found in "elements".
|
||||||
|
for el in elements:
|
||||||
|
el.addEventListener(target, create_proxy(wrapper))
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# If "when" was called as a decorator, return the decorator function,
|
||||||
|
# otherwise just call the internal decorator function with the supplied
|
||||||
|
# handler.
|
||||||
|
return decorator(handler) if handler else decorator
|
||||||
@@ -31,7 +31,7 @@ def _object_keys(value):
|
|||||||
|
|
||||||
|
|
||||||
def _is_array(value):
|
def _is_array(value):
|
||||||
return isinstance(value, list) or isinstance(value, tuple)
|
return isinstance(value, (list, tuple))
|
||||||
|
|
||||||
|
|
||||||
def _is_object(value):
|
def _is_object(value):
|
||||||
@@ -60,10 +60,10 @@ def _loop(keys, input, known, output):
|
|||||||
|
|
||||||
|
|
||||||
def _ref(key, value, input, known, output):
|
def _ref(key, value, input, known, output):
|
||||||
if _is_array(value) and not value in known:
|
if _is_array(value) and value not in known:
|
||||||
known.append(value)
|
known.append(value)
|
||||||
value = _loop(_array_keys(value), input, known, value)
|
value = _loop(_array_keys(value), input, known, value)
|
||||||
elif _is_object(value) and not value in known:
|
elif _is_object(value) and value not in known:
|
||||||
known.append(value)
|
known.append(value)
|
||||||
value = _loop(_object_keys(value), input, known, value)
|
value = _loop(_object_keys(value), input, known, value)
|
||||||
|
|
||||||
60
core/src/stdlib/pyscript/fs.py
Normal file
60
core/src/stdlib/pyscript/fs.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
mounted = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
||||||
|
import js
|
||||||
|
from _pyscript import fs, interpreter
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
from pyscript.magic_js import (
|
||||||
|
RUNNING_IN_WORKER,
|
||||||
|
sync,
|
||||||
|
)
|
||||||
|
|
||||||
|
js.console.warn("experimental pyscript.fs ⚠️")
|
||||||
|
|
||||||
|
handler = None
|
||||||
|
|
||||||
|
uid = f"{path}@{id}"
|
||||||
|
|
||||||
|
options = {"id": id, "mode": mode}
|
||||||
|
if root != "":
|
||||||
|
options["startIn"] = root
|
||||||
|
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
fsh = sync.storeFSHandler(uid, to_js(options))
|
||||||
|
|
||||||
|
# allow both async and/or SharedArrayBuffer use case
|
||||||
|
if isinstance(fsh, bool):
|
||||||
|
success = fsh
|
||||||
|
else:
|
||||||
|
success = await fsh
|
||||||
|
|
||||||
|
if success:
|
||||||
|
from polyscript import IDBMap
|
||||||
|
|
||||||
|
idb = IDBMap.new(fs.NAMESPACE)
|
||||||
|
handler = await idb.get(uid)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(fs.ERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
success = await fs.idb.has(uid)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
handler = await fs.idb.get(uid)
|
||||||
|
else:
|
||||||
|
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
|
||||||
|
await fs.idb.set(uid, handler)
|
||||||
|
|
||||||
|
mounted[path] = await interpreter.mountNativeFS(path, handler)
|
||||||
|
|
||||||
|
|
||||||
|
async def sync(path):
|
||||||
|
await mounted[path].syncfs()
|
||||||
|
|
||||||
|
|
||||||
|
async def unmount(path):
|
||||||
|
from _pyscript import interpreter
|
||||||
|
|
||||||
|
await sync(path)
|
||||||
|
interpreter._module.FS.unmount(path)
|
||||||
@@ -10,6 +10,11 @@ RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
|||||||
|
|
||||||
config = json.loads(globalThis.JSON.stringify(_config))
|
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`
|
# allow `from pyscript.js_modules.xxx import yyy`
|
||||||
class JSModule:
|
class JSModule:
|
||||||
@@ -20,6 +25,7 @@ class JSModule:
|
|||||||
# avoid pyodide looking for non existent fields
|
# avoid pyodide looking for non existent fields
|
||||||
if not field.startswith("_"):
|
if not field.startswith("_"):
|
||||||
return getattr(getattr(js_modules, self.name), field)
|
return getattr(getattr(js_modules, self.name), field)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# generate N modules in the system that will proxy the real value
|
# generate N modules in the system that will proxy the real value
|
||||||
87
core/src/stdlib/pyscript/media.py
Normal file
87
core/src/stdlib/pyscript/media.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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 = {}
|
||||||
|
options["audio"] = audio
|
||||||
|
if isinstance(video, bool):
|
||||||
|
options["video"] = video
|
||||||
|
else:
|
||||||
|
options["video"] = {}
|
||||||
|
for k in video:
|
||||||
|
options["video"][k] = video[k]
|
||||||
|
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
||||||
|
|
||||||
|
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()
|
||||||
|
]
|
||||||
@@ -10,10 +10,11 @@ def _to_idb(value):
|
|||||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||||
return _stringify(["generic", value])
|
return _stringify(["generic", value])
|
||||||
if isinstance(value, bytearray):
|
if isinstance(value, bytearray):
|
||||||
return _stringify(["bytearray", [v for v in value]])
|
return _stringify(["bytearray", list(value)])
|
||||||
if isinstance(value, memoryview):
|
if isinstance(value, memoryview):
|
||||||
return _stringify(["memoryview", [v for v in value]])
|
return _stringify(["memoryview", list(value)])
|
||||||
raise TypeError(f"Unexpected value: {value}")
|
msg = f"Unexpected value: {value}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
|
||||||
# convert an IndexedDB compatible entry into a Python value
|
# convert an IndexedDB compatible entry into a Python value
|
||||||
@@ -56,5 +57,6 @@ class Storage(dict):
|
|||||||
|
|
||||||
async def storage(name="", storage_class=Storage):
|
async def storage(name="", storage_class=Storage):
|
||||||
if not name:
|
if not name:
|
||||||
raise ValueError("The storage name must be defined")
|
msg = "The storage name must be defined"
|
||||||
|
raise ValueError(msg)
|
||||||
return storage_class(await _storage(f"@pyscript/{name}"))
|
return storage_class(await _storage(f"@pyscript/{name}"))
|
||||||
58
core/src/stdlib/pyscript/util.py
Normal file
58
core/src/stdlib/pyscript/util.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import js
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def as_bytearray(buffer):
|
||||||
|
"""
|
||||||
|
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
|
||||||
|
MicroPython friendly manner.
|
||||||
|
"""
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
size = ui8a.length
|
||||||
|
ba = bytearray(size)
|
||||||
|
for i in range(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)
|
||||||
|
|
||||||
|
|
||||||
|
def is_awaitable(obj):
|
||||||
|
"""
|
||||||
|
Returns a boolean indication if the passed in obj is an awaitable
|
||||||
|
function. (MicroPython treats awaitables as generator functions, and if
|
||||||
|
the object is a closure containing an async function we need to work
|
||||||
|
carefully.)
|
||||||
|
"""
|
||||||
|
from pyscript import config
|
||||||
|
|
||||||
|
if config["type"] == "mpy": # Is MicroPython?
|
||||||
|
# MicroPython doesn't appear to have a way to determine if a closure is
|
||||||
|
# an async function except via the repr. This is a bit hacky.
|
||||||
|
if "<closure <generator>" in repr(obj):
|
||||||
|
return True
|
||||||
|
return inspect.isgeneratorfunction(obj)
|
||||||
|
|
||||||
|
return inspect.iscoroutinefunction(obj)
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
# `when` is not used in this module. It is imported here save the user an additional
|
# `when` is not used in this module. It is imported here save the user an additional
|
||||||
# import (i.e. they can get what they need from `pyscript.web`).
|
# import (i.e. they can get what they need from `pyscript.web`).
|
||||||
from pyscript import document, when # NOQA
|
|
||||||
|
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
|
||||||
|
|
||||||
|
from pyscript import document, when, Event # noqa: F401
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
|
|
||||||
|
|
||||||
def wrap_dom_element(dom_element):
|
def wrap_dom_element(dom_element):
|
||||||
@@ -68,6 +72,18 @@ class Element:
|
|||||||
type(self).get_tag_name()
|
type(self).get_tag_name()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# HTML on_events attached to the element become pyscript.Event instances.
|
||||||
|
self._on_events = {}
|
||||||
|
|
||||||
|
# Handle kwargs for handling named events with a default handler function.
|
||||||
|
properties = {}
|
||||||
|
for name, handler in kwargs.items():
|
||||||
|
if name.startswith("on_"):
|
||||||
|
ev = self.get_event(name) # Create the default Event instance.
|
||||||
|
ev.add_listener(handler)
|
||||||
|
else:
|
||||||
|
properties[name] = handler
|
||||||
|
|
||||||
# A set-like interface to the element's `classList`.
|
# A set-like interface to the element's `classList`.
|
||||||
self._classes = Classes(self)
|
self._classes = Classes(self)
|
||||||
|
|
||||||
@@ -75,7 +91,7 @@ class Element:
|
|||||||
self._style = Style(self)
|
self._style = Style(self)
|
||||||
|
|
||||||
# Set any specified classes, styles, and DOM properties.
|
# Set any specified classes, styles, and DOM properties.
|
||||||
self.update(classes=classes, style=style, **kwargs)
|
self.update(classes=classes, style=style, **properties)
|
||||||
|
|
||||||
def __eq__(self, obj):
|
def __eq__(self, obj):
|
||||||
"""Check for equality by comparing the underlying DOM element."""
|
"""Check for equality by comparing the underlying DOM element."""
|
||||||
@@ -87,19 +103,27 @@ class Element:
|
|||||||
If `key` is an integer or a slice we use it to index/slice the element's
|
If `key` is an integer or a slice we use it to index/slice the element's
|
||||||
children. Otherwise, we use `key` as a query selector.
|
children. Otherwise, we use `key` as a query selector.
|
||||||
"""
|
"""
|
||||||
if isinstance(key, int) or isinstance(key, slice):
|
if isinstance(key, (int, slice)):
|
||||||
return self.children[key]
|
return self.children[key]
|
||||||
|
|
||||||
return self.find(key)
|
return self.find(key)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
"""
|
||||||
|
Get an attribute from the element.
|
||||||
|
|
||||||
|
If the attribute is an event (e.g. "on_click"), we wrap it in an `Event`
|
||||||
|
instance and return that. Otherwise, we return the attribute from the
|
||||||
|
underlying DOM element.
|
||||||
|
"""
|
||||||
|
if name.startswith("on_"):
|
||||||
|
return self.get_event(name)
|
||||||
# This allows us to get attributes on the underlying DOM element that clash
|
# This allows us to get attributes on the underlying DOM element that clash
|
||||||
# with Python keywords or built-ins (e.g. the output element has an
|
# with Python keywords or built-ins (e.g. the output element has an
|
||||||
# attribute `for` which is a Python keyword, so you can access it on the
|
# attribute `for` which is a Python keyword, so you can access it on the
|
||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1]
|
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
||||||
|
|
||||||
return getattr(self._dom_element, name)
|
return getattr(self._dom_element, name)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
@@ -117,10 +141,37 @@ class Element:
|
|||||||
# attribute `for` which is a Python keyword, so you can access it on the
|
# attribute `for` which is a Python keyword, so you can access it on the
|
||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1]
|
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
||||||
|
|
||||||
|
if name.startswith("on_"):
|
||||||
|
# Ensure on-events are cached in the _on_events dict if the
|
||||||
|
# user is setting them directly.
|
||||||
|
self._on_events[name] = value
|
||||||
|
|
||||||
setattr(self._dom_element, name, value)
|
setattr(self._dom_element, name, value)
|
||||||
|
|
||||||
|
def get_event(self, name):
|
||||||
|
"""
|
||||||
|
Get an `Event` instance for the specified event name.
|
||||||
|
"""
|
||||||
|
if not name.startswith("on_"):
|
||||||
|
msg = "Event names must start with 'on_'."
|
||||||
|
raise ValueError(msg)
|
||||||
|
event_name = name[3:] # Remove the "on_" prefix.
|
||||||
|
if not hasattr(self._dom_element, event_name):
|
||||||
|
msg = f"Element has no '{event_name}' event."
|
||||||
|
raise ValueError(msg)
|
||||||
|
if name in self._on_events:
|
||||||
|
return self._on_events[name]
|
||||||
|
# Such an on-event exists in the DOM element, but we haven't yet
|
||||||
|
# wrapped it in an Event instance. Let's do that now. When the
|
||||||
|
# underlying DOM element's event is triggered, the Event instance
|
||||||
|
# will be triggered too.
|
||||||
|
ev = Event()
|
||||||
|
self._on_events[name] = ev
|
||||||
|
self._dom_element.addEventListener(event_name, create_proxy(ev.trigger))
|
||||||
|
return ev
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self):
|
def children(self):
|
||||||
"""Return the element's children as an `ElementCollection`."""
|
"""Return the element's children as an `ElementCollection`."""
|
||||||
@@ -157,7 +208,7 @@ class Element:
|
|||||||
# We check for list/tuple here and NOT for any iterable as it will match
|
# We check for list/tuple here and NOT for any iterable as it will match
|
||||||
# a JS Nodelist which is handled explicitly below.
|
# a JS Nodelist which is handled explicitly below.
|
||||||
# NodeList.
|
# NodeList.
|
||||||
elif isinstance(item, list) or isinstance(item, tuple):
|
elif isinstance(item, (list, tuple)):
|
||||||
for child in item:
|
for child in item:
|
||||||
self.append(child)
|
self.append(child)
|
||||||
|
|
||||||
@@ -181,10 +232,11 @@ class Element:
|
|||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Nope! This is not an element or a NodeList.
|
# Nope! This is not an element or a NodeList.
|
||||||
raise TypeError(
|
msg = (
|
||||||
f'Element "{item}" is a proxy object, "'
|
f'Element "{item}" is a proxy object, "'
|
||||||
f"but not a valid element or a NodeList."
|
f"but not a valid element or a NodeList."
|
||||||
)
|
)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
def clone(self, clone_id=None):
|
def clone(self, clone_id=None):
|
||||||
"""Make a clone of the element (clones the underlying DOM object too)."""
|
"""Make a clone of the element (clones the underlying DOM object too)."""
|
||||||
@@ -355,8 +407,7 @@ class Options:
|
|||||||
|
|
||||||
new_option = option(**kwargs)
|
new_option = option(**kwargs)
|
||||||
|
|
||||||
if before:
|
if before and isinstance(before, Element):
|
||||||
if isinstance(before, Element):
|
|
||||||
before = before._dom_element
|
before = before._dom_element
|
||||||
|
|
||||||
self._element._dom_element.add(new_option._dom_element, before)
|
self._element._dom_element.add(new_option._dom_element, before)
|
||||||
@@ -417,11 +468,11 @@ class ContainerElement(Element):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for child in list(args) + (children or []):
|
for child in list(args) + (children or []):
|
||||||
if isinstance(child, Element) or isinstance(child, ElementCollection):
|
if isinstance(child, (Element, ElementCollection)):
|
||||||
self.append(child)
|
self.append(child)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.innerHTML += child
|
self._dom_element.insertAdjacentHTML("beforeend", child)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from self.children
|
yield from self.children
|
||||||
@@ -447,14 +498,13 @@ class ClassesCollection:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for class_name in self._all_class_names():
|
yield from self._all_class_names()
|
||||||
yield class_name
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._all_class_names())
|
return len(self._all_class_names())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"ClassesCollection({repr(self._collection)})"
|
return f"ClassesCollection({self._collection!r})"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return " ".join(self._all_class_names())
|
return " ".join(self._all_class_names())
|
||||||
@@ -507,7 +557,7 @@ class StyleCollection:
|
|||||||
element.style[key] = value
|
element.style[key] = value
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"StyleCollection({repr(self._collection)})"
|
return f"StyleCollection({self._collection!r})"
|
||||||
|
|
||||||
def remove(self, key):
|
def remove(self, key):
|
||||||
"""Remove a CSS property from the elements in the collection."""
|
"""Remove a CSS property from the elements in the collection."""
|
||||||
@@ -542,7 +592,7 @@ class ElementCollection:
|
|||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
return self._elements[key]
|
return self._elements[key]
|
||||||
|
|
||||||
elif isinstance(key, slice):
|
if isinstance(key, slice):
|
||||||
return ElementCollection(self._elements[key])
|
return ElementCollection(self._elements[key])
|
||||||
|
|
||||||
return self.find(key)
|
return self.find(key)
|
||||||
@@ -1079,7 +1129,8 @@ class video(ContainerElement):
|
|||||||
|
|
||||||
elif isinstance(to, Element):
|
elif isinstance(to, Element):
|
||||||
if to.tag != "canvas":
|
if to.tag != "canvas":
|
||||||
raise TypeError("Element to snap to must be a canvas.")
|
msg = "Element to snap to must be a canvas."
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
elif getattr(to, "tagName", "") == "CANVAS":
|
elif getattr(to, "tagName", "") == "CANVAS":
|
||||||
to = canvas(dom_element=to)
|
to = canvas(dom_element=to)
|
||||||
@@ -1088,10 +1139,12 @@ class video(ContainerElement):
|
|||||||
elif isinstance(to, str):
|
elif isinstance(to, str):
|
||||||
nodelist = document.querySelectorAll(to) # NOQA
|
nodelist = document.querySelectorAll(to) # NOQA
|
||||||
if nodelist.length == 0:
|
if nodelist.length == 0:
|
||||||
raise TypeError("No element with selector {to} to snap to.")
|
msg = "No element with selector {to} to snap to."
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
if nodelist[0].tagName != "CANVAS":
|
if nodelist[0].tagName != "CANVAS":
|
||||||
raise TypeError("Element to snap to must be a canvas.")
|
msg = "Element to snap to must be a canvas."
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
to = canvas(dom_element=nodelist[0])
|
to = canvas(dom_element=nodelist[0])
|
||||||
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import js
|
import js
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
from pyscript.util import as_bytearray
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
code = "code"
|
code = "code"
|
||||||
protocols = "protocols"
|
protocols = "protocols"
|
||||||
reason = "reason"
|
reason = "reason"
|
||||||
|
methods = ["onclose", "onerror", "onmessage", "onopen"]
|
||||||
|
|
||||||
|
|
||||||
class EventMessage:
|
class EventMessage:
|
||||||
@@ -22,7 +24,7 @@ class EventMessage:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(object):
|
class WebSocket:
|
||||||
CONNECTING = 0
|
CONNECTING = 0
|
||||||
OPEN = 1
|
OPEN = 1
|
||||||
CLOSING = 2
|
CLOSING = 2
|
||||||
@@ -36,18 +38,20 @@ class WebSocket(object):
|
|||||||
socket = js.WebSocket.new(url)
|
socket = js.WebSocket.new(url)
|
||||||
object.__setattr__(self, "_ws", socket)
|
object.__setattr__(self, "_ws", socket)
|
||||||
|
|
||||||
for t in ["onclose", "onerror", "onmessage", "onopen"]:
|
for t in methods:
|
||||||
if t in kw:
|
if t in kw:
|
||||||
socket[t] = kw[t]
|
# Pyodide fails at setting socket[t] directly
|
||||||
|
setattr(socket, t, create_proxy(kw[t]))
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self._ws, attr)
|
return getattr(self._ws, attr)
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
if attr == "onmessage":
|
if attr in methods:
|
||||||
self._ws[attr] = lambda e: value(EventMessage(e))
|
m = lambda e: value(EventMessage(e))
|
||||||
|
setattr(self._ws, attr, create_proxy(m))
|
||||||
else:
|
else:
|
||||||
self._ws[attr] = value
|
setattr(self._ws, attr, value)
|
||||||
|
|
||||||
def close(self, **kw):
|
def close(self, **kw):
|
||||||
if code in kw and reason in kw:
|
if code in kw and reason in kw:
|
||||||
@@ -25,10 +25,12 @@ async def create_named_worker(src="", name="", config=None, type="py"):
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
if not src:
|
if not src:
|
||||||
raise ValueError("Named workers require src")
|
msg = "Named workers require src"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
raise ValueError("Named workers require a name")
|
msg = "Named workers require a name"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
s = _js.document.createElement("script")
|
s = _js.document.createElement("script")
|
||||||
s.type = type
|
s.type = type
|
||||||
@@ -37,7 +39,7 @@ async def create_named_worker(src="", name="", config=None, type="py"):
|
|||||||
_set(s, "name", name)
|
_set(s, "name", name)
|
||||||
|
|
||||||
if config:
|
if config:
|
||||||
_set(s, "config", isinstance(config, str) and config or dumps(config))
|
_set(s, "config", (isinstance(config, str) and config) or dumps(config))
|
||||||
|
|
||||||
_js.document.body.append(s)
|
_js.document.body.append(s)
|
||||||
return await workers[name]
|
return await workers[name]
|
||||||
72
core/src/storage.js
Normal file
72
core/src/storage.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
31
core/src/sync.js
Normal file
31
core/src/sync.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { idb, getFileSystemDirectoryHandle } from "./fs.js";
|
||||||
|
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask a user action via dialog and returns the directory handler once granted.
|
||||||
|
* @param {string} uid
|
||||||
|
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
async storeFSHandler(uid, options = {}) {
|
||||||
|
if (await idb.has(uid)) return true;
|
||||||
|
return getFileSystemDirectoryHandle(options).then(
|
||||||
|
async (handler) => {
|
||||||
|
await idb.set(uid, handler);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
49
core/tests/README.md
Normal file
49
core/tests/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 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). ;-)
|
||||||
18
core/tests/index.html
Normal file
18
core/tests/index.html
Normal file
File diff suppressed because one or more lines are too long
24
core/tests/javascript/async-listener.html
Normal file
24
core/tests/javascript/async-listener.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!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>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PyScript Next Plugin</title>
|
<title>PyScript Next Plugin</title>
|
||||||
<link rel="stylesheet" href="../dist/core.css">
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
<script type="module" src="../dist/core.js"></script>
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
<mpy-config src="config-url/config.json"></mpy-config>
|
<mpy-config src="config-url/config.json"></mpy-config>
|
||||||
<script type="mpy">
|
<script type="mpy">
|
||||||
from pyscript import config
|
from pyscript import config
|
||||||
23
core/tests/javascript/config_type.html
Normal file
23
core/tests/javascript/config_type.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!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>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="../dist/core.css">
|
<link rel="stylesheet" href="../../../dist/core.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
document.createElement('script'),
|
document.createElement('script'),
|
||||||
{
|
{
|
||||||
type: 'module',
|
type: 'module',
|
||||||
src: '../dist/core.js'
|
src: '../../../dist/core.js'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PyScript FFI</title>
|
<title>PyScript FFI</title>
|
||||||
<link rel="stylesheet" href="../dist/core.css">
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
<script type="module" src="../dist/core.js"></script>
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="mpy">
|
<script type="mpy">
|
||||||
@@ -4,13 +4,13 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PyScript Next Plugin Bug?</title>
|
<title>PyScript Next Plugin Bug?</title>
|
||||||
<link rel="stylesheet" href="../dist/core.css">
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
<script type="module">
|
<script type="module">
|
||||||
addEventListener('mpy:done', () => {
|
addEventListener('mpy:done', () => {
|
||||||
document.documentElement.classList.add('done');
|
document.documentElement.classList.add('done');
|
||||||
});
|
});
|
||||||
|
|
||||||
import { hooks } from "../dist/core.js";
|
import { hooks } from "../../dist/core.js";
|
||||||
|
|
||||||
// Main
|
// Main
|
||||||
hooks.main.onReady.add((wrap, element) => {
|
hooks.main.onReady.add((wrap, element) => {
|
||||||
6
core/tests/javascript/issue-2093/error.js
Normal file
6
core/tests/javascript/issue-2093/error.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const { error } = console;
|
||||||
|
|
||||||
|
console.error = (...args) => {
|
||||||
|
error(...args);
|
||||||
|
document.documentElement.classList.add('errored');
|
||||||
|
};
|
||||||
16
core/tests/javascript/issue-2093/index.html
Normal file
16
core/tests/javascript/issue-2093/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!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>
|
||||||
28
core/tests/javascript/js-storage.html
Normal file
28
core/tests/javascript/js-storage.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!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>
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="../dist/core.css">
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
<script type="module" src="../dist/core.js"></script>
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<mpy-config>
|
<mpy-config>
|
||||||
29
core/tests/javascript/loader/index.html
Normal file
29
core/tests/javascript/loader/index.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!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>
|
||||||
39
core/tests/javascript/media.html
Normal file
39
core/tests/javascript/media.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Pyodide Media Module Test</title>
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Pyodide Media Module Test</h1>
|
||||||
|
<div id="test-results">Running tests...</div>
|
||||||
|
|
||||||
|
<script type="py" terminal>
|
||||||
|
from pyscript import window, document
|
||||||
|
from pyscript import media
|
||||||
|
|
||||||
|
async def run_tests():
|
||||||
|
# Test basic module structure
|
||||||
|
assert hasattr(media, "Device"), "media module should have Device class"
|
||||||
|
assert hasattr(media, "list_devices"), "media module should have list_devices function"
|
||||||
|
|
||||||
|
# Test device enumeration
|
||||||
|
devices = await media.list_devices()
|
||||||
|
assert isinstance(devices, list), "list_devices should return a list"
|
||||||
|
|
||||||
|
# If we have devices, test properties of one
|
||||||
|
if devices:
|
||||||
|
device = devices[0]
|
||||||
|
assert hasattr(device, "id"), "Device should have id property"
|
||||||
|
assert hasattr(device, "group"), "Device should have group property"
|
||||||
|
assert hasattr(device, "kind"), "Device should have kind property"
|
||||||
|
assert hasattr(device, "label"), "Device should have label property"
|
||||||
|
|
||||||
|
document.getElementById('test-results').innerText = "Success!"
|
||||||
|
document.documentElement.classList.add('media-ok')
|
||||||
|
|
||||||
|
await run_tests()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
core/tests/javascript/mpy-error.html
Normal file
14
core/tests/javascript/mpy-error.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user