mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Compare commits
167 Commits
fix-404-ge
...
when-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c91b941b | ||
|
|
b33661ff8e | ||
|
|
9db8b13d9c | ||
|
|
3003a9671d | ||
|
|
b87c86f266 | ||
|
|
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 | ||
|
|
0d0ea96435 | ||
|
|
fafdf74007 | ||
|
|
999897df12 | ||
|
|
d47fb58ede | ||
|
|
f316341e73 | ||
|
|
8c46fcabf7 | ||
|
|
e4ff4d8fab | ||
|
|
f20a0003ed | ||
|
|
6c938dfe3b | ||
|
|
d884586a82 | ||
|
|
f8f7ba89c1 | ||
|
|
67d47511d5 | ||
|
|
6f49f18937 | ||
|
|
7b8ef7ebe2 | ||
|
|
461ae38763 | ||
|
|
4b90ebdef5 | ||
|
|
15c19aa708 | ||
|
|
d0406be84c | ||
|
|
aab015b9b8 | ||
|
|
a1e5a05b49 | ||
|
|
f1a787e031 | ||
|
|
b41cfb7b60 | ||
|
|
1c675307e1 | ||
|
|
ac56f82c6d | ||
|
|
2ac5ca79d7 | ||
|
|
cb9ee6f7e2 | ||
|
|
9abaef33bd | ||
|
|
320a537db2 | ||
|
|
9b775ce015 | ||
|
|
66f72eda1e | ||
|
|
39ca29749c | ||
|
|
85da548447 | ||
|
|
9985787e4b | ||
|
|
18ec6ce775 | ||
|
|
ed6d0136b8 | ||
|
|
e7216d26e7 | ||
|
|
d1a0d8ea98 | ||
|
|
04222b0d03 | ||
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da | ||
|
|
83c2afeaf1 | ||
|
|
643b76479f | ||
|
|
cf92996071 | ||
|
|
c653296821 | ||
|
|
44cd6273ba | ||
|
|
d7d2dfb383 | ||
|
|
2d5cf096e0 | ||
|
|
6ee8217593 | ||
|
|
6d45728787 | ||
|
|
65954a627e | ||
|
|
2f1b764251 | ||
|
|
1fb6cddd70 | ||
|
|
239add4e20 | ||
|
|
4e4ac56729 | ||
|
|
1447cb3094 | ||
|
|
2f3659b676 | ||
|
|
910c666319 | ||
|
|
eee2f64c1d | ||
|
|
d080246a0f | ||
|
|
98c0f5e50d | ||
|
|
a1268f1aa2 | ||
|
|
69b8884045 | ||
|
|
df1d699fe6 | ||
|
|
84f197b657 | ||
|
|
5bed5ede52 | ||
|
|
f6d5cf06c8 | ||
|
|
30c6c830ae | ||
|
|
d7084f7f55 | ||
|
|
a87d2b3fea | ||
|
|
81a26363a3 | ||
|
|
53e945201d | ||
|
|
181d276c8b | ||
|
|
bcaab0eb93 | ||
|
|
3ff0f84391 | ||
|
|
2b411fc635 | ||
|
|
2128572ce5 | ||
|
|
63f2453091 | ||
|
|
f6470dcad5 | ||
|
|
a9717afeb7 | ||
|
|
cea52b4334 | ||
|
|
7ad7f0abfb | ||
|
|
1efd73af8f | ||
|
|
1e7fb9af44 | ||
|
|
154e00d320 | ||
|
|
0f788fa284 | ||
|
|
355866a1f1 | ||
|
|
6eca06ac0b | ||
|
|
a4aef0b530 | ||
|
|
136e95498f | ||
|
|
6a3e2834b6 | ||
|
|
c0d45d368b | ||
|
|
f18ec3d20a | ||
|
|
b0377cc7ab | ||
|
|
96e671b55f | ||
|
|
40e99abbdf | ||
|
|
8b6b055681 | ||
|
|
8e5605fa42 | ||
|
|
06e1fdecc2 | ||
|
|
a82e8334d6 | ||
|
|
539bc2ae0e | ||
|
|
0711acd30e | ||
|
|
1476131ab4 | ||
|
|
89902a440c | ||
|
|
156c23d550 | ||
|
|
30396ba79a | ||
|
|
a4343c62ca |
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -11,7 +11,9 @@ body:
|
||||
|
||||
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
||||
|
||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists
|
||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
|
||||
|
||||
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
|
||||
|
||||
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
||||
- type: checkboxes
|
||||
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,12 +4,9 @@
|
||||
|
||||
## 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
|
||||
|
||||
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
|
||||
|
||||
- [ ] All tests pass locally
|
||||
- [ ] I have updated `CHANGELOG.md`
|
||||
- [ ] I have created documentation for this(if applicable)
|
||||
- [ ] I have checked `make build` works locally.
|
||||
- [ ] I have created / updated documentation for this change (if applicable).
|
||||
|
||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
29
.github/workflows/prepare-release.yml
vendored
29
.github/workflows/prepare-release.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./pyscript.core
|
||||
working-directory: ./core
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
@@ -17,12 +17,27 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -35,20 +50,20 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: NPM Install
|
||||
run: npm install && npx playwright install
|
||||
run: npm install && npx playwright install chromium
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Generate index.html
|
||||
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
|
||||
run: zip -r -q ./build.zip ./dist
|
||||
|
||||
- name: Prepare Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
|
||||
31
.github/workflows/publish-release.yml
vendored
31
.github/workflows/publish-release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./pyscript.core
|
||||
working-directory: ./core
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
@@ -19,12 +19,27 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -37,14 +52,18 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: npm install
|
||||
run: npm install && npx playwright install
|
||||
run: npm install && npx playwright install chromium
|
||||
|
||||
- name: build
|
||||
run: npm run build
|
||||
|
||||
- name: Generate index.html in snapshot
|
||||
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/
|
||||
working-directory: .
|
||||
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
|
||||
27
.github/workflows/publish-snapshot.yml
vendored
27
.github/workflows/publish-snapshot.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./pyscript.core
|
||||
working-directory: ./core
|
||||
|
||||
jobs:
|
||||
publish-snapshot:
|
||||
@@ -23,12 +23,27 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -41,7 +56,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install && npx playwright install
|
||||
run: npm install && npx playwright install chromium
|
||||
|
||||
- name: Build Pyscript.core
|
||||
run: npm run build
|
||||
@@ -54,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Generate index.html in snapshot
|
||||
working-directory: .
|
||||
run: sed 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||
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
|
||||
run: >
|
||||
|
||||
31
.github/workflows/publish-unstable.yml
vendored
31
.github/workflows/publish-unstable.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: "Publish Unstable"
|
||||
|
||||
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:
|
||||
- main
|
||||
paths:
|
||||
- pyscript.core/**
|
||||
- core/**
|
||||
- examples/**
|
||||
|
||||
workflow_dispatch:
|
||||
@@ -18,18 +18,33 @@ jobs:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./pyscript.core
|
||||
working-directory: ./core
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -42,14 +57,14 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: NPM Install
|
||||
run: npm install && npx playwright install
|
||||
run: npm install && npx playwright install chromium
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Generate index.html in snapshot
|
||||
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
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
|
||||
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@@ -5,20 +5,20 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- pyscript.core/**
|
||||
- core/**
|
||||
- .github/workflows/test.yml
|
||||
|
||||
pull_request: # Only run on merges into main that modify certain files
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- pyscript.core/**
|
||||
- core/**
|
||||
- .github/workflows/test.yml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
BuildAndTest:
|
||||
runs-on: ubuntu-latest-8core
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MINICONDA_PYTHON_VERSION: py38
|
||||
MINICONDA_VERSION: 4.11.0
|
||||
@@ -37,12 +37,12 @@ jobs:
|
||||
run: git log --graph -3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
|
||||
- name: Create and activate virtual environment
|
||||
run: |
|
||||
@@ -69,24 +69,12 @@ jobs:
|
||||
make setup
|
||||
|
||||
- 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@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pyscript
|
||||
path: |
|
||||
pyscript.core/dist/
|
||||
core/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
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.6.0
|
||||
with:
|
||||
artifact: test_results
|
||||
name: Test reports
|
||||
path: "*.xml"
|
||||
reporter: java-junit
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -142,10 +142,11 @@ coverage/
|
||||
test_results
|
||||
|
||||
# @pyscript/core npm artifacts
|
||||
pyscript.core/core.*
|
||||
pyscript.core/dist
|
||||
pyscript.core/dist.zip
|
||||
pyscript.core/src/plugins.js
|
||||
pyscript.core/src/stdlib/pyscript.js
|
||||
pyscript.core/src/3rd-party/*
|
||||
!pyscript.core/src/3rd-party/READMEmd
|
||||
core/test-results/*
|
||||
core/core.*
|
||||
core/dist
|
||||
core/dist.zip
|
||||
core/src/plugins.js
|
||||
core/src/stdlib/pyscript.js
|
||||
core/src/3rd-party/*
|
||||
!core/src/3rd-party/READMEmd
|
||||
|
||||
@@ -4,10 +4,10 @@ ci:
|
||||
#skip: [eslint]
|
||||
autoupdate_schedule: monthly
|
||||
|
||||
default_stages: [commit]
|
||||
default_stages: [pre-commit]
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-builtin-literals
|
||||
- id: check-case-conflict
|
||||
@@ -21,17 +21,17 @@ repos:
|
||||
- id: check-yaml
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
exclude: pyscript\.core/dist|\.min\.js$
|
||||
exclude: core/dist|\.min\.js$
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 24.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
||||
args: ["-l", "88", "--skip-string-normalization"]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.4
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell # See 'pyproject.toml' for args
|
||||
exclude: \.js\.map$
|
||||
@@ -42,12 +42,5 @@ repos:
|
||||
rev: "v3.0.0-alpha.6"
|
||||
hooks:
|
||||
- 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/test|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
|
||||
args: [--tab-width, "4"]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: [--profile, black]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
ISSUE_TEMPLATE
|
||||
*.min.*
|
||||
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
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -1,87 +0,0 @@
|
||||
# Release Notes
|
||||
|
||||
## 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))
|
||||
@@ -1,81 +1,4 @@
|
||||
# Contributing to PyScript
|
||||
|
||||
Thank you for wanting to contribute to the PyScript project!
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Contributing to PyScript](#contributing-to-pyscript)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Contributing](#contributing)
|
||||
- [Reporting bugs](#reporting-bugs)
|
||||
- [Creating useful issues](#creating-useful-issues)
|
||||
- [Reporting security issues](#reporting-security-issues)
|
||||
- [Asking questions](#asking-questions)
|
||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||
- [Developing](#developing)
|
||||
- [Rebasing changes](#rebasing-changes)
|
||||
- [Building the docs](#building-the-docs)
|
||||
- [Places to start](#places-to-start)
|
||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||
- [Submitting a change](#submitting-a-change)
|
||||
- [License terms for contributions](#license-terms-for-contributions)
|
||||
- [Becoming a maintainer](#becoming-a-maintainer)
|
||||
- [Trademarks](#trademarks)
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
|
||||
|
||||
# Contributing
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
|
||||
|
||||
## Creating useful issues
|
||||
|
||||
- Use a clear and descriptive title.
|
||||
- Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
|
||||
- Describe the behavior you observed, and the behavior you had expected.
|
||||
- Include screenshots if they help make the issue clear.
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
|
||||
|
||||
## Asking questions
|
||||
|
||||
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
|
||||
|
||||
## Places to start
|
||||
|
||||
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions:
|
||||
|
||||
- **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
|
||||
- **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
|
||||
- **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
|
||||
- **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
|
||||
|
||||
## Setting up your local environment and developing
|
||||
|
||||
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/development/setting-up-environment.html) will help you get started.
|
||||
|
||||
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/development/developing.html) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
||||
|
||||
## License terms for contributions
|
||||
|
||||
This Project welcomes contributions, suggestions, and feedback. All contributions, suggestions, and feedback you submitted are accepted under the [Apache 2.0](./LICENSE) license. You represent that if you do not own copyright in the code that you have the authority to submit it under the [Apache 2.0](./LICENSE) license. All feedback, suggestions, or contributions are not confidential.
|
||||
|
||||
## Becoming a maintainer
|
||||
|
||||
Contributors are invited to be maintainers of the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md). New maintainers are invited by a 3/4 vote of the existing maintainers.
|
||||
|
||||
## Trademarks
|
||||
|
||||
The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md).
|
||||
|
||||
---
|
||||
|
||||
Part of MVG-0.1-beta.
|
||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
Please see our guide to contributing to PyScript
|
||||
[in our documentation](https://docs.pyscript.net/latest/contributing/).
|
||||
|
||||
6
LICENSE
6
LICENSE
@@ -186,7 +186,11 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Copyright (c) 2022-present, PyScript Development Team
|
||||
|
||||
Originated at Anaconda, Inc. in 2022
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
26
Makefile
26
Makefile
@@ -9,10 +9,11 @@ PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
|
||||
all:
|
||||
@echo "\nThere is no default Makefile target right now. Try:\n"
|
||||
@echo "make setup - check your environment and install the dependencies."
|
||||
@echo "make update - update dependencies."
|
||||
@echo "make clean - clean up auto-generated assets."
|
||||
@echo "make build - build PyScript."
|
||||
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||
@echo "make test-integration - run all integration tests sequentially."
|
||||
@echo "make test - run all automated tests in playwright."
|
||||
@echo "make fmt - format the code."
|
||||
@echo "make fmt-check - check the code formatting.\n"
|
||||
|
||||
@@ -39,13 +40,12 @@ check-python:
|
||||
|
||||
# Check the environment, install the dependencies.
|
||||
setup: check-node check-npm check-python
|
||||
cd pyscript.core && npm install && cd ..
|
||||
cd core && npm install && cd ..
|
||||
ifeq ($(VIRTUAL_ENV),)
|
||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
||||
false
|
||||
else
|
||||
python -m pip install -r requirements.txt
|
||||
playwright install
|
||||
endif
|
||||
|
||||
# Clean up generated assets.
|
||||
@@ -56,21 +56,20 @@ clean:
|
||||
|
||||
# Build PyScript.
|
||||
build:
|
||||
cd pyscript.core && npx playwright install && npm run build
|
||||
cd core && npx playwright install chromium && npm run build
|
||||
|
||||
# Update the dependencies.
|
||||
update:
|
||||
cd core && npm update && cd ..
|
||||
python -m pip install -r requirements.txt --upgrade
|
||||
|
||||
# Run the precommit checks (run eslint).
|
||||
precommit-check:
|
||||
pre-commit run --all-files
|
||||
|
||||
# Run all integration tests sequentially.
|
||||
test-integration:
|
||||
mkdir -p test_results
|
||||
pytest -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||
|
||||
# Run all integration tests in parallel.
|
||||
test-integration-parallel:
|
||||
mkdir -p test_results
|
||||
pytest --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||
# Run all automated tests in playwright.
|
||||
test:
|
||||
cd core && npm run test:integration
|
||||
|
||||
# Format the code.
|
||||
fmt: fmt-py
|
||||
@@ -83,7 +82,6 @@ fmt-check: fmt-py-check
|
||||
# Format Python code.
|
||||
fmt-py:
|
||||
black -l 88 --skip-string-normalization .
|
||||
isort --profile black .
|
||||
|
||||
# Check the format of Python code.
|
||||
fmt-py-check:
|
||||
|
||||
99
README.md
99
README.md
@@ -1,84 +1,85 @@
|
||||
# PyScript
|
||||
|
||||
## What is PyScript
|
||||
## PyScript is an open source platform for Python in the browser.
|
||||
|
||||
### Summary
|
||||
|
||||
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [MicroPython](https://micropython.org/) and [WASM](https://webassembly.org/), and modern web technologies.
|
||||
|
||||
To get started see the [getting started tutorial](docs/tutorials/getting-started.md).
|
||||
|
||||
For examples see [here](examples).
|
||||
|
||||
### Longer Version
|
||||
|
||||
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
|
||||
|
||||
## Try PyScript
|
||||
|
||||
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page:
|
||||
Using PyScript is as simple as:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>PyScript!</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/releases/2023.11.1/core.css"
|
||||
href="https://pyscript.net/snapshots/2024.9.2/core.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://pyscript.net/releases/2023.11.1/core.js"
|
||||
src="https://pyscript.net/snapshots/2024.9.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
|
||||
<!-- Use MicroPython to evaluate some Python -->
|
||||
<script type="mpy" terminal>
|
||||
print("Hello, world!")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You can then use PyScript components in your html page. PyScript currently offers various ways of running Python code:
|
||||
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.
|
||||
|
||||
- `<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 if 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.
|
||||
To start building, read the
|
||||
[Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||
|
||||
Check out the [official docs](https://docs.pyscript.net) for more detailed documentation.
|
||||
For example applications, see [here](https://pyscript.com/@examples).
|
||||
|
||||
## How to Contribute
|
||||
Other useful resources:
|
||||
|
||||
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
||||
- Our [Home Page](https://pyscript.net/) as an open source project.
|
||||
- The [official technical docs](https://docs.pyscript.net/).
|
||||
- A [YouTube channel](https://www.youtube.com/@PyScriptTV) with helpful videos
|
||||
and community content.
|
||||
- A free-to-use [online IDE](https://pyscript.com/) for trying PyScript.
|
||||
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in
|
||||
touch .
|
||||
|
||||
Check out the [developing process](https://docs.pyscript.net/latest/contributing) documentation for more information on how to setup your development environment.
|
||||
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.
|
||||
|
||||
## Community calls and events
|
||||
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.
|
||||
|
||||
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 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.
|
||||
|
||||
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;
|
||||
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
|
||||
|
||||
## Resources
|
||||
## Contribute
|
||||
|
||||
- [Official docs](https://docs.pyscript.net)
|
||||
- [Discussion board](https://community.anaconda.cloud/c/tech-topics/pyscript)
|
||||
- [Home Page](https://pyscript.net/)
|
||||
- [Blog Post](https://engineering.anaconda.com/2022/04/welcome-pyscript.html)
|
||||
- [Discord Channel](https://discord.gg/BYB2kvyFwm)
|
||||
For technical details of the code, please see the [README](core/README.md) in
|
||||
the `core` directory.
|
||||
|
||||
## Notes
|
||||
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.
|
||||
|
||||
- This is an extremely experimental project, so expect things to break!
|
||||
- PyScript has been only tested on Chrome at the moment.
|
||||
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||
documentation for more information on how to setup your development environment.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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.
|
||||
22
core/eslint.config.mjs
Normal file
22
core/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import globals from "globals";
|
||||
import js from "@eslint/js";
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
ignores: ["**/3rd-party/"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-implicit-globals": ["error"],
|
||||
},
|
||||
},
|
||||
];
|
||||
2222
pyscript.core/package-lock.json → core/package-lock.json
generated
2222
pyscript.core/package-lock.json → core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
107
core/package.json
Normal file
107
core/package.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"name": "@pyscript/core",
|
||||
"version": "0.6.7",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
"unpkg": "./index.js",
|
||||
"jsdelivr": "./jsdelivr.js",
|
||||
"browser": "./index.js",
|
||||
"main": "./index.js",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"files": [
|
||||
"./dist/",
|
||||
"./src/",
|
||||
"./types/",
|
||||
"./index.js",
|
||||
"./jsdelivr.js",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/core.d.ts",
|
||||
"import": "./src/core.js"
|
||||
},
|
||||
"./css": {
|
||||
"import": "./dist/core.css"
|
||||
},
|
||||
"./storage": {
|
||||
"import": "./dist/storage.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
|
||||
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:integration; fi",
|
||||
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||
"build:flatted": "node rollup/flatted.cjs",
|
||||
"build:plugins": "node rollup/plugins.cjs",
|
||||
"build:stdlib": "node rollup/stdlib.cjs",
|
||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||
"build:tests-index": "node rollup/build_test_index.cjs",
|
||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test tests/js_tests.spec.js tests/py_tests.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
|
||||
"dev": "node dev.cjs",
|
||||
"release": "npm run build && npm run zip",
|
||||
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||
"ts": "rm -rf types && tsc -p .",
|
||||
"zip": "zip -r dist.zip ./dist"
|
||||
},
|
||||
"keywords": [
|
||||
"pyscript",
|
||||
"core"
|
||||
],
|
||||
"author": "Anaconda Inc.",
|
||||
"license": "APACHE-2.0",
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"@webreflection/idb-map": "^0.3.2",
|
||||
"add-promise-listener": "^0.1.3",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.16.3",
|
||||
"sabayon": "^0.5.2",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
"type-checked-collections": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.7.0",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@webreflection/toml-j0.4": "^1.1.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"bun": "^1.1.30",
|
||||
"chokidar": "^4.0.1",
|
||||
"codedent": "^0.1.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^9.12.0",
|
||||
"flatted": "^3.3.1",
|
||||
"rollup": "^4.24.0",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.5.3",
|
||||
"string-width": "^7.2.0",
|
||||
"typescript": "^5.6.3",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pyscript/pyscript/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||
}
|
||||
@@ -10,6 +10,9 @@ const { devDependencies } = require(join(__dirname, "..", "package.json"));
|
||||
|
||||
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
|
||||
|
||||
const dropSourceMap = (str) =>
|
||||
str.replace(/^\/.+? sourceMappingURL=\/.+$/m, "");
|
||||
|
||||
// Fetch a module via jsdelivr CDN `/+esm` orchestration
|
||||
// then sanitize the resulting outcome to avoid importing
|
||||
// anything via `/npm/...` through Rollup
|
||||
@@ -31,22 +34,44 @@ const resolve = (name) => {
|
||||
);
|
||||
};
|
||||
|
||||
// create a file rollup can then process and understand
|
||||
const reBundle = (name) => Promise.resolve(`export * from "${name}";\n`);
|
||||
|
||||
// key/value pairs as:
|
||||
// "3rd-party/file-name.js"
|
||||
// string as content or
|
||||
// Promise<string> as resolved content
|
||||
const modules = {
|
||||
// toml
|
||||
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||
|
||||
// xterm
|
||||
"xterm.js": resolve("xterm"),
|
||||
"xterm-readline.js": resolve("xterm-readline"),
|
||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||
b.text(),
|
||||
),
|
||||
"xterm_addon-web-links.js": fetch(
|
||||
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||
).then((b) => b.text()),
|
||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||
(b) => b.text(),
|
||||
),
|
||||
"xterm-readline.js": resolve("xterm-readline"),
|
||||
|
||||
// codemirror
|
||||
"codemirror.js": reBundle("codemirror"),
|
||||
"codemirror_state.js": reBundle("@codemirror/state"),
|
||||
"codemirror_lang-python.js": reBundle("@codemirror/lang-python"),
|
||||
"codemirror_language.js": reBundle("@codemirror/language"),
|
||||
"codemirror_view.js": reBundle("@codemirror/view"),
|
||||
"codemirror_commands.js": reBundle("@codemirror/commands"),
|
||||
};
|
||||
|
||||
for (const [target, source] of Object.entries(modules)) {
|
||||
if (typeof source === "string") copyFileSync(source, join(targets, target));
|
||||
else {
|
||||
source.then((text) => writeFileSync(join(targets, target), text));
|
||||
source.then((text) =>
|
||||
writeFileSync(join(targets, target), dropSourceMap(text)),
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "./src/storage.js",
|
||||
plugins: plugins.concat(
|
||||
process.env.NO_MIN
|
||||
? [nodeResolve(), commonjs()]
|
||||
: [nodeResolve(), commonjs(), terser()],
|
||||
),
|
||||
output: {
|
||||
esModule: true,
|
||||
dir: "./dist",
|
||||
sourcemap: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
17
core/rollup/flatted.cjs
Normal file
17
core/rollup/flatted.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
const { writeFileSync, readFileSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
|
||||
const flatted = "# https://www.npmjs.com/package/flatted\n\n";
|
||||
const source = join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"flatted",
|
||||
"python",
|
||||
"flatted.py",
|
||||
);
|
||||
const dest = join(__dirname, "..", "src", "stdlib", "pyscript", "flatted.py");
|
||||
|
||||
const clear = (str) => String(str).replace(/^#.*/gm, "").trimStart();
|
||||
|
||||
writeFileSync(dest, flatted + clear(readFileSync(source)));
|
||||
@@ -13,7 +13,12 @@ for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
|
||||
plugins.push(
|
||||
// this comment is needed to avoid bundlers eagerly embedding lazy
|
||||
// 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`,
|
||||
);
|
||||
1
core/src/3rd-party/codemirror.js
vendored
Normal file
1
core/src/3rd-party/codemirror.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "codemirror";
|
||||
1
core/src/3rd-party/codemirror_commands.js
vendored
Normal file
1
core/src/3rd-party/codemirror_commands.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@codemirror/commands";
|
||||
1
core/src/3rd-party/codemirror_lang-python.js
vendored
Normal file
1
core/src/3rd-party/codemirror_lang-python.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@codemirror/lang-python";
|
||||
1
core/src/3rd-party/codemirror_language.js
vendored
Normal file
1
core/src/3rd-party/codemirror_language.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@codemirror/language";
|
||||
1
core/src/3rd-party/codemirror_state.js
vendored
Normal file
1
core/src/3rd-party/codemirror_state.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@codemirror/state";
|
||||
1
core/src/3rd-party/codemirror_view.js
vendored
Normal file
1
core/src/3rd-party/codemirror_view.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@codemirror/view";
|
||||
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@5.3.0/css/xterm.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||
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.1 and Terser v5.19.2.
|
||||
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
var e,t,r={exports:{}};self;var s=r.exports=(e=t={},Object.defineProperty(e,"__esModule",{value:!0}),e.FitAddon=void 0,e.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(s.getPropertyValue("height")),o=Math.max(0,parseInt(s.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=i-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=o-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}},t),i=r.exports.FitAddon,o=r.exports.__esModule;export{i as FitAddon,o as __esModule,s as default};
|
||||
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.1 and Terser v5.19.2.
|
||||
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
var e={exports:{}};self;var t=e.exports=(()=>{var e={6:(e,t)=>{function r(e){try{const t=new URL(e),r=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(r.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,r,n={}){this._terminal=e,this._regex=t,this._handler=r,this._options=n}provideLinks(e,t){const r=n.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(r))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,r)=>{if(this._options.hover){const{range:n}=e;this._options.hover(t,r,n)}},e)))}};class n{static computeLink(e,t,o,s){const i=new RegExp(t.source,(t.flags||"")+"g"),[a,l]=n._getWindowedLineStrings(e-1,o),c=a.join("");let d;const p=[];for(;d=i.exec(c);){const e=d[0];if(!r(e))continue;const[t,i]=n._mapStrIdx(o,l,0,d.index),[a,c]=n._mapStrIdx(o,t,i,e.length);if(-1===t||-1===i||-1===a||-1===c)continue;const h={start:{x:i+1,y:t+1},end:{x:c,y:a+1}};p.push({range:h,text:e,activate:s})}return p}static _getWindowedLineStrings(e,t){let r,n=e,o=e,s=0,i="";const a=[];if(r=t.buffer.active.getLine(e)){const e=r.translateToString(!0);if(r.isWrapped&&" "!==e[0]){for(s=0;(r=t.buffer.active.getLine(--n))&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),r.isWrapped&&-1===i.indexOf(" ")););a.reverse()}for(a.push(e),s=0;(r=t.buffer.active.getLine(++o))&&r.isWrapped&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),-1===i.indexOf(" ")););}return[a,n]}static _mapStrIdx(e,t,r,n){const o=e.buffer.active,s=o.getNullCell();let i=r;for(;n;){const e=o.getLine(t);if(!e)return[-1,-1];for(let r=i;r<e.length;++r){e.getCell(r,s);const i=s.getChars();if(s.getWidth()&&(n-=i.length||1,r===e.length-1&&""===i)){const e=o.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,s),2===s.getWidth()&&(n+=1))}if(n<0)return[t,r]}t++,i=0}return[t,i]}}t.LinkComputer=n}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=r(6),o=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function s(e,t){const r=window.open();if(r){try{r.opener=null}catch{}r.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=s,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const r=this._options,n=r.urlRegex||o;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,n,this._handler,r))}dispose(){this._linkProvider?.dispose()}}})(),n})(),r=e.exports.WebLinksAddon,n=e.exports.__esModule;export{r as WebLinksAddon,n as __esModule,t as default};
|
||||
@@ -3,15 +3,17 @@
|
||||
* to use as base config for all py-script elements, importing
|
||||
* also a queue of plugins *before* the interpreter (if any) resolves.
|
||||
*/
|
||||
import { $ } from "basic-devtools";
|
||||
import { $$ } from "basic-devtools";
|
||||
|
||||
import TYPES from "./types.js";
|
||||
import allPlugins from "./plugins.js";
|
||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||
import { ErrorCode } from "./exceptions.js";
|
||||
|
||||
const { BAD_CONFIG, CONFLICTING_CODE } = ErrorCode;
|
||||
|
||||
const badURL = (url, expected = "") => {
|
||||
let message = `(${ErrorCode.BAD_CONFIG}): Invalid URL: ${url}`;
|
||||
let message = `(${BAD_CONFIG}): Invalid URL: ${url}`;
|
||||
if (expected) message += `\nexpected ${expected} content`;
|
||||
throw new Error(message);
|
||||
};
|
||||
@@ -41,8 +43,12 @@ const configDetails = async (config, type) => {
|
||||
return { json, toml: toml || (!json && !!text), text, url };
|
||||
};
|
||||
|
||||
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
|
||||
|
||||
const relative_url = (url, base = location.href) => new URL(url, base).href;
|
||||
|
||||
const syntaxError = (type, url, { message }) => {
|
||||
let str = `(${ErrorCode.BAD_CONFIG}): Invalid ${type}`;
|
||||
let str = `(${BAD_CONFIG}): Invalid ${type}`;
|
||||
if (url) str += ` @ ${url}`;
|
||||
return new SyntaxError(`${str}\n${message}`);
|
||||
};
|
||||
@@ -56,29 +62,55 @@ for (const [TYPE] of TYPES) {
|
||||
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||
let parsed;
|
||||
|
||||
/** @type {SyntaxError | undefined} The error thrown when parsing the PyScript config, if any.*/
|
||||
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
|
||||
let error;
|
||||
|
||||
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
|
||||
let configURL;
|
||||
|
||||
let config,
|
||||
type,
|
||||
pyConfig = $(`${TYPE}-config`);
|
||||
if (pyConfig) {
|
||||
config = pyConfig.getAttribute("src") || pyConfig.textContent;
|
||||
type = pyConfig.getAttribute("type");
|
||||
} else {
|
||||
pyConfig = $(
|
||||
pyElement,
|
||||
pyConfigs = $$(`${TYPE}-config`),
|
||||
attrConfigs = $$(
|
||||
[
|
||||
`script[type="${TYPE}"][config]:not([worker])`,
|
||||
`${TYPE}-script[config]:not([worker])`,
|
||||
].join(","),
|
||||
);
|
||||
if (pyConfig) config = pyConfig.getAttribute("config");
|
||||
|
||||
// throw an error if there are multiple <py-config> or <mpy-config>
|
||||
if (pyConfigs.length > 1) {
|
||||
error = conflictError(`Too many ${TYPE}-config`);
|
||||
} else {
|
||||
// throw an error if there are <x-config> and config="x" attributes
|
||||
if (pyConfigs.length && attrConfigs.length) {
|
||||
error = conflictError(
|
||||
`Ambiguous ${TYPE}-config VS config attribute`,
|
||||
);
|
||||
} else if (pyConfigs.length) {
|
||||
[pyElement] = pyConfigs;
|
||||
config = pyElement.getAttribute("src") || pyElement.textContent;
|
||||
type = pyElement.getAttribute("type");
|
||||
} else if (attrConfigs.length) {
|
||||
[pyElement, ...attrConfigs] = attrConfigs;
|
||||
config = pyElement.getAttribute("config");
|
||||
// throw an error if dirrent scripts use different configs
|
||||
if (
|
||||
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
||||
) {
|
||||
error = conflictError(
|
||||
"Unable to use different configs on main",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// catch possible fetch errors
|
||||
if (config) {
|
||||
if (!error && config) {
|
||||
try {
|
||||
const { json, toml, text, url } = await configDetails(config, type);
|
||||
if (url) configURL = relative_url(url);
|
||||
config = text;
|
||||
if (json || type === "json") {
|
||||
try {
|
||||
@@ -114,13 +146,15 @@ for (const [TYPE] of TYPES) {
|
||||
}
|
||||
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||
} else if (key === "error") {
|
||||
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||
}
|
||||
}
|
||||
|
||||
// assign plugins as Promise.all only if needed
|
||||
plugins = Promise.all(toBeAwaited);
|
||||
|
||||
configs.set(TYPE, { config: parsed, plugins, error });
|
||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||
}
|
||||
|
||||
export default configs;
|
||||
export { configs, relative_url };
|
||||
75
core/src/core.css
Normal file
75
core/src/core.css
Normal file
@@ -0,0 +1,75 @@
|
||||
py-script,
|
||||
py-config,
|
||||
mpy-script,
|
||||
mpy-config {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* PyEditor */
|
||||
.py-editor-box,
|
||||
.mpy-editor-box {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.py-editor-input,
|
||||
.mpy-editor-input {
|
||||
position: relative;
|
||||
}
|
||||
.py-editor-box::before,
|
||||
.mpy-editor-box::before {
|
||||
content: attr(data-env);
|
||||
display: block;
|
||||
font-size: x-small;
|
||||
text-align: end;
|
||||
}
|
||||
.py-editor-output,
|
||||
.mpy-editor-output {
|
||||
white-space: pre;
|
||||
}
|
||||
.py-editor-run-button,
|
||||
.mpy-editor-run-button {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
z-index: 1;
|
||||
}
|
||||
.py-editor-box:hover .py-editor-run-button,
|
||||
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||
.py-editor-run-button:focus,
|
||||
.py-editor-run-button:disabled,
|
||||
.mpy-editor-run-button:focus,
|
||||
.mpy-editor-run-button:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.py-editor-run-button:disabled > *,
|
||||
.mpy-editor-run-button:disabled > * {
|
||||
display: none; /* hide all the child elements of the run button when it is disabled */
|
||||
}
|
||||
.py-editor-run-button:disabled,
|
||||
.mpy-editor-run-button:disabled {
|
||||
border-width: 0;
|
||||
}
|
||||
.py-editor-run-button:disabled::before,
|
||||
.mpy-editor-run-button:disabled::before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
|
||||
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
|
||||
border-radius: 50%;
|
||||
border: 2px solid #aaa;
|
||||
border-top-color: #000;
|
||||
background-color: #fff;
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
define,
|
||||
defineProperty,
|
||||
dispatch,
|
||||
isSync,
|
||||
queryTarget,
|
||||
unescape,
|
||||
whenDefined,
|
||||
@@ -19,44 +20,61 @@ import {
|
||||
|
||||
import "./all-done.js";
|
||||
import TYPES from "./types.js";
|
||||
import configs from "./config.js";
|
||||
import { configs, relative_url } from "./config.js";
|
||||
import sync from "./sync.js";
|
||||
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
||||
import { ErrorCode } from "./exceptions.js";
|
||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
||||
import {
|
||||
hooks,
|
||||
main,
|
||||
worker,
|
||||
codeFor,
|
||||
createFunction,
|
||||
inputFailure,
|
||||
} from "./hooks.js";
|
||||
|
||||
// allows lazy element features on code evaluation
|
||||
let currentElement;
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
export { stdlib, optional, inputFailure };
|
||||
|
||||
export const donkey = (options) =>
|
||||
import(/* webpackIgnore: true */ "./plugins/donkey.js").then((module) =>
|
||||
module.default(options),
|
||||
);
|
||||
|
||||
// generic helper to disambiguate between custom element and script
|
||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||
|
||||
let shouldRegister = true;
|
||||
const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
||||
// automatically use the pyscript stderr (when/if defined)
|
||||
// this defaults to console.error
|
||||
function PyWorker(...args) {
|
||||
const worker = $XWorker(...args);
|
||||
worker.onerror = ({ error }) => io.stderr(error);
|
||||
return worker;
|
||||
}
|
||||
|
||||
// enrich the Python env with some JS utility for main
|
||||
interpreter.registerJsModule("_pyscript", {
|
||||
PyWorker,
|
||||
get target() {
|
||||
return isScript(currentElement)
|
||||
? currentElement.target.id
|
||||
: currentElement.id;
|
||||
// Used to create either Pyodide or MicroPython workers
|
||||
// with the PyScript module available within the code
|
||||
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
|
||||
([TYPE, interpreter]) =>
|
||||
/**
|
||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||
* @param {string} file the python file to run ina worker.
|
||||
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||
* @returns {Promise<Worker & {sync: object}>}
|
||||
*/
|
||||
async function PyScriptWorker(file, options) {
|
||||
await configs.get(TYPE).plugins;
|
||||
const xworker = XWorker.call(
|
||||
new Hook(null, hooked.get(TYPE)),
|
||||
file,
|
||||
{
|
||||
...options,
|
||||
type: interpreter,
|
||||
},
|
||||
});
|
||||
};
|
||||
);
|
||||
assign(xworker.sync, sync);
|
||||
return xworker.ready;
|
||||
},
|
||||
);
|
||||
|
||||
// avoid multiple initialization of the same library
|
||||
const [
|
||||
{
|
||||
PyWorker: exportedPyWorker,
|
||||
MPWorker: exportedMPWorker,
|
||||
hooks: exportedHooks,
|
||||
config: exportedConfig,
|
||||
whenDefined: exportedWhenDefined,
|
||||
@@ -64,6 +82,7 @@ const [
|
||||
alreadyLive,
|
||||
] = stickyModule("@pyscript/core", {
|
||||
PyWorker,
|
||||
MPWorker,
|
||||
hooks,
|
||||
config: {},
|
||||
whenDefined,
|
||||
@@ -71,12 +90,17 @@ const [
|
||||
|
||||
export {
|
||||
TYPES,
|
||||
relative_url,
|
||||
exportedPyWorker as PyWorker,
|
||||
exportedMPWorker as MPWorker,
|
||||
exportedHooks as hooks,
|
||||
exportedConfig as config,
|
||||
exportedWhenDefined as whenDefined,
|
||||
};
|
||||
|
||||
export const offline_interpreter = (config) =>
|
||||
config?.interpreter && relative_url(config.interpreter);
|
||||
|
||||
const hooked = new Map();
|
||||
|
||||
for (const [TYPE, interpreter] of TYPES) {
|
||||
@@ -88,7 +112,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
else dispatch(element, TYPE, "done");
|
||||
};
|
||||
|
||||
const { config, plugins, error } = configs.get(TYPE);
|
||||
const { config, configURL, plugins, error } = configs.get(TYPE);
|
||||
|
||||
// create a unique identifier when/if needed
|
||||
let id = 0;
|
||||
@@ -118,6 +142,37 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
return code;
|
||||
};
|
||||
|
||||
// register once any interpreter
|
||||
let alreadyRegistered = false;
|
||||
|
||||
// allows lazy element features on code evaluation
|
||||
let currentElement;
|
||||
|
||||
const registerModule = ({ XWorker, interpreter, io }) => {
|
||||
// avoid multiple registration of the same interpreter
|
||||
if (alreadyRegistered) return;
|
||||
alreadyRegistered = true;
|
||||
|
||||
// automatically use the pyscript stderr (when/if defined)
|
||||
// this defaults to console.error
|
||||
function PyWorker(...args) {
|
||||
const worker = XWorker(...args);
|
||||
worker.onerror = ({ error }) => io.stderr(error);
|
||||
return worker;
|
||||
}
|
||||
|
||||
// enrich the Python env with some JS utility for main
|
||||
interpreter.registerJsModule("_pyscript", {
|
||||
PyWorker,
|
||||
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||
get target() {
|
||||
return isScript(currentElement)
|
||||
? currentElement.target.id
|
||||
: currentElement.id;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// define the module as both `<script type="py">` and `<py-script>`
|
||||
// but only if the config didn't throw an error
|
||||
if (!error) {
|
||||
@@ -131,12 +186,9 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// specific main and worker hooks
|
||||
const hooks = {
|
||||
main: {
|
||||
...codeFor(main),
|
||||
...codeFor(main, TYPE),
|
||||
async onReady(wrap, element) {
|
||||
if (shouldRegister) {
|
||||
shouldRegister = false;
|
||||
registerModule(wrap);
|
||||
}
|
||||
|
||||
// allows plugins to do whatever they want with the element
|
||||
// before regular stuff happens in here
|
||||
@@ -156,15 +208,13 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
}
|
||||
|
||||
if (isScript(element)) {
|
||||
const {
|
||||
attributes: { async: isAsync, target },
|
||||
} = element;
|
||||
const hasTarget = !!target?.value;
|
||||
const show = hasTarget
|
||||
? queryTarget(element, target.value)
|
||||
const isAsync = !isSync(element);
|
||||
const target = element.getAttribute("target");
|
||||
const show = target
|
||||
? queryTarget(element, target)
|
||||
: document.createElement("script-py");
|
||||
|
||||
if (!hasTarget) {
|
||||
if (!target) {
|
||||
const { head, body } = document;
|
||||
if (head.contains(element)) body.append(show);
|
||||
else element.after(show);
|
||||
@@ -231,7 +281,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
...codeFor(worker),
|
||||
...codeFor(worker, TYPE),
|
||||
// these are lazy getters that returns a composition
|
||||
// of the current hooks or undefined, if no hook is present
|
||||
get onReady() {
|
||||
@@ -256,10 +306,11 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
|
||||
define(TYPE, {
|
||||
config,
|
||||
configURL,
|
||||
interpreter,
|
||||
hooks,
|
||||
env: `${TYPE}-script`,
|
||||
version: config?.interpreter,
|
||||
version: offline_interpreter(config),
|
||||
onerror(error, element) {
|
||||
errors.set(element, error);
|
||||
},
|
||||
@@ -284,7 +335,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
async connectedCallback() {
|
||||
if (!this.executed) {
|
||||
this.executed = true;
|
||||
const isAsync = this.hasAttribute("async");
|
||||
const isAsync = !isSync(this);
|
||||
const { io, run, runAsync } = await this._wrap
|
||||
.promise;
|
||||
this.srcCode = await fetchSource(
|
||||
@@ -310,24 +361,3 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// export the used config without allowing leaks through it
|
||||
exportedConfig[TYPE] = structuredClone(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||
* @param {string} file the python file to run ina worker.
|
||||
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||
* @returns {Worker & {sync: ProxyHandler<object>}}
|
||||
*/
|
||||
function PyWorker(file, options) {
|
||||
const hooks = hooked.get("py");
|
||||
// this propagates pyscript worker hooks without needing a pyscript
|
||||
// bootstrap + it passes arguments and enforces `pyodide`
|
||||
// as the interpreter to use in the worker, as all hooks assume that
|
||||
// and as `pyodide` is the only default interpreter that can deal with
|
||||
// all the features we need to deliver pyscript out there.
|
||||
const xworker = XWorker.call(new Hook(null, hooks), file, {
|
||||
type: "pyodide",
|
||||
...options,
|
||||
});
|
||||
assign(xworker.sync, sync);
|
||||
return xworker;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const CLOSEBUTTON =
|
||||
|
||||
/**
|
||||
* These error codes are used to identify the type of error that occurred.
|
||||
* @see https://docs.pyscript.net/latest/reference/exceptions.html?highlight=errors
|
||||
* @see https://pyscript.github.io/docs/latest/reference/exceptions.html?highlight=errors
|
||||
*/
|
||||
export const ErrorCode = {
|
||||
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
|
||||
@@ -1,7 +1,10 @@
|
||||
import { FetchError, ErrorCode } from "./exceptions.js";
|
||||
import { getText } from "polyscript/exports";
|
||||
|
||||
export { getText };
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns
|
||||
*/
|
||||
export const getText = (response) => response.text();
|
||||
|
||||
/**
|
||||
* This is a fetch wrapper that handles any non 200 responses and throws a
|
||||
@@ -2,7 +2,7 @@ import { typedSet } from "type-checked-collections";
|
||||
import { dedent } from "polyscript/exports";
|
||||
import toJSONCallback from "to-json-callback";
|
||||
|
||||
import stdlib from "./stdlib.js";
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
|
||||
export const main = (name) => hooks.main[name];
|
||||
export const worker = (name) => hooks.worker[name];
|
||||
@@ -15,10 +15,11 @@ const code = (hooks, branch, key, lib) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const codeFor = (branch) => {
|
||||
export const codeFor = (branch, type) => {
|
||||
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
|
||||
const hooks = {};
|
||||
code(hooks, branch, `codeBeforeRun`, stdlib);
|
||||
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
|
||||
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||
code(hooks, branch, `codeAfterRun`);
|
||||
code(hooks, branch, `codeAfterRunAsync`);
|
||||
return hooks;
|
||||
@@ -45,12 +46,12 @@ export const createFunction = (self, name) => {
|
||||
const SetFunction = typedSet({ typeof: "function" });
|
||||
const SetString = typedSet({ typeof: "string" });
|
||||
|
||||
const inputFailure = `
|
||||
export const inputFailure = `
|
||||
import builtins
|
||||
def input(prompt=""):
|
||||
raise Exception("\\n ".join([
|
||||
"input() doesn't work when PyScript runs in the main thread.",
|
||||
"Consider using the worker attribute: https://docs.pyscript.net/2023.11.1/user-guide/workers/"
|
||||
"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/"
|
||||
]))
|
||||
|
||||
builtins.input = input
|
||||
28
core/src/plugins.js
Normal file
28
core/src/plugins.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||
export default {
|
||||
["deprecations-manager"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/deprecations-manager.js"
|
||||
),
|
||||
donkey: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/donkey.js"
|
||||
),
|
||||
error: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/error.js"
|
||||
),
|
||||
["py-editor"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/py-editor.js"
|
||||
),
|
||||
["py-terminal"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/py-terminal.js"
|
||||
),
|
||||
};
|
||||
113
core/src/plugins/donkey.js
Normal file
113
core/src/plugins/donkey.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import addPromiseListener from "add-promise-listener";
|
||||
import { assign, dedent } from "polyscript/exports";
|
||||
|
||||
const { stringify } = JSON;
|
||||
|
||||
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||
|
||||
const donkey = ({ type = "py", persistent, terminal, config }) => {
|
||||
const args = persistent ? ["globals()", "__locals__"] : ["{}", "{}"];
|
||||
|
||||
const src = URL.createObjectURL(
|
||||
new Blob([
|
||||
[
|
||||
// this array is to better minify this code once in production
|
||||
"from pyscript import sync, config",
|
||||
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
|
||||
"__locals__ = {}",
|
||||
'if config["type"] == "py":',
|
||||
" import sys",
|
||||
" def __error__(_):",
|
||||
" info = sys.exc_info()",
|
||||
" return __message__(info[0], info[1])",
|
||||
"else:",
|
||||
" __error__ = lambda e: __message__(e.__class__, e.value)",
|
||||
"def execute(code):",
|
||||
` try: return ${invoke("exec", args)};`,
|
||||
" except Exception as e: print(__error__(e));",
|
||||
"def evaluate(code):",
|
||||
` try: return ${invoke("eval", args)};`,
|
||||
" except Exception as e: print(__error__(e));",
|
||||
"sync.execute = execute",
|
||||
"sync.evaluate = evaluate",
|
||||
].join("\n"),
|
||||
]),
|
||||
);
|
||||
|
||||
// create the script that exposes the code to execute or evaluate
|
||||
const script = assign(document.createElement("script"), { type, src });
|
||||
script.toggleAttribute("worker", true);
|
||||
script.toggleAttribute("terminal", true);
|
||||
if (terminal) script.setAttribute("target", terminal);
|
||||
if (config) {
|
||||
script.setAttribute(
|
||||
"config",
|
||||
typeof config === "string" ? config : stringify(config),
|
||||
);
|
||||
}
|
||||
|
||||
return addPromiseListener(
|
||||
document.body.appendChild(script),
|
||||
`${type}:done`,
|
||||
{ stopPropagation: true },
|
||||
).then(() => {
|
||||
URL.revokeObjectURL(src);
|
||||
return script;
|
||||
});
|
||||
};
|
||||
|
||||
const utils = async (options) => {
|
||||
const script = await donkey(options);
|
||||
const { xworker, process, terminal } = script;
|
||||
const { execute, evaluate } = xworker.sync;
|
||||
script.remove();
|
||||
return {
|
||||
xworker,
|
||||
process,
|
||||
terminal,
|
||||
execute,
|
||||
evaluate,
|
||||
};
|
||||
};
|
||||
|
||||
export default async (options = {}) => {
|
||||
let farmer = await utils(options);
|
||||
let working = false;
|
||||
const kill = () => {
|
||||
if (farmer) {
|
||||
farmer.xworker.terminate();
|
||||
farmer.terminal.dispose();
|
||||
farmer = null;
|
||||
}
|
||||
working = false;
|
||||
};
|
||||
const reload = async () => {
|
||||
kill();
|
||||
farmer = await utils(options);
|
||||
};
|
||||
const asyncTask = (method) => async (code) => {
|
||||
// race condition ... a new task has been
|
||||
// assigned while the previous one didn't finish
|
||||
if (working) await reload();
|
||||
working = true;
|
||||
try {
|
||||
return await farmer[method](dedent(code));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
working = false;
|
||||
}
|
||||
};
|
||||
const asyncMethod = (method) => async () => {
|
||||
if (working) await reload();
|
||||
else farmer?.terminal[method]();
|
||||
};
|
||||
return {
|
||||
process: asyncTask("process"),
|
||||
execute: asyncTask("execute"),
|
||||
evaluate: asyncTask("evaluate"),
|
||||
clear: asyncMethod("clear"),
|
||||
reset: asyncMethod("reset"),
|
||||
kill,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
// PyScript Error Plugin
|
||||
import { buffered } from "polyscript/exports";
|
||||
import { hooks } from "../core.js";
|
||||
|
||||
let dontBotherDOM = false;
|
||||
export function notOnDOM() {
|
||||
dontBotherDOM = true;
|
||||
}
|
||||
|
||||
hooks.main.onReady.add(function override(pyScript) {
|
||||
// be sure this override happens only once
|
||||
hooks.main.onReady.delete(override);
|
||||
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
|
||||
// trap generic `stderr` to propagate to it regardless
|
||||
const { stderr } = pyScript.io;
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = (error, ...rest) => {
|
||||
const cb = (error, ...rest) => {
|
||||
notify(error.message || error);
|
||||
// let other plugins or stderr hook, if any, do the rest
|
||||
return stderr(error, ...rest);
|
||||
};
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
|
||||
|
||||
// be sure uncaught Python errors are also visible
|
||||
addEventListener("error", ({ message }) => {
|
||||
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
|
||||
* @param {string} message
|
||||
*/
|
||||
export function notify(message) {
|
||||
if (dontBotherDOM) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = "py-error";
|
||||
div.textContent = message;
|
||||
430
core/src/plugins/py-editor.js
Normal file
430
core/src/plugins/py-editor.js
Normal file
@@ -0,0 +1,430 @@
|
||||
// PyScript py-editor plugin
|
||||
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
|
||||
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||
|
||||
let id = 0;
|
||||
const getID = (type) => `${type}-editor-${id++}`;
|
||||
|
||||
const envs = new Map();
|
||||
const configs = new Map();
|
||||
|
||||
const hooks = {
|
||||
worker: {
|
||||
codeBeforeRun: () => stdlib,
|
||||
// works on both Pyodide and MicroPython
|
||||
onReady: ({ runAsync, io }, { sync }) => {
|
||||
io.stdout = io.buffered(sync.write);
|
||||
io.stderr = io.buffered(sync.writeErr);
|
||||
sync.revoke();
|
||||
sync.runAsync = runAsync;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validate = (config, result) => {
|
||||
if (typeof result === "boolean") throw `Invalid source: ${config}`;
|
||||
return result;
|
||||
};
|
||||
|
||||
async function execute({ currentTarget }) {
|
||||
const { env, pySrc, outDiv } = this;
|
||||
const hasRunButton = !!currentTarget;
|
||||
|
||||
if (hasRunButton) {
|
||||
currentTarget.disabled = true;
|
||||
outDiv.innerHTML = "";
|
||||
}
|
||||
|
||||
if (!envs.has(env)) {
|
||||
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||
const details = {
|
||||
type: this.interpreter,
|
||||
serviceWorker: this.serviceWorker,
|
||||
};
|
||||
const { config } = this;
|
||||
if (config) {
|
||||
// verify that config can be parsed and used
|
||||
try {
|
||||
details.configURL = relative_url(config);
|
||||
if (config.endsWith(".toml")) {
|
||||
const [{ parse }, toml] = await Promise.all([
|
||||
import(
|
||||
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||
),
|
||||
fetch(config).then((r) => r.ok && r.text()),
|
||||
]);
|
||||
details.config = parse(validate(config, toml));
|
||||
} else if (config.endsWith(".json")) {
|
||||
const json = await fetch(config).then(
|
||||
(r) => r.ok && r.json(),
|
||||
);
|
||||
details.config = validate(config, json);
|
||||
} else {
|
||||
details.configURL = relative_url("./config.txt");
|
||||
details.config = JSON.parse(config);
|
||||
}
|
||||
details.version = offline_interpreter(details.config);
|
||||
} catch (error) {
|
||||
notify(error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
details.config = {};
|
||||
}
|
||||
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||
|
||||
// expose xworker like in terminal or other workers to allow
|
||||
// creation and destruction of editors on the fly
|
||||
if (hasRunButton) {
|
||||
for (const type of TYPES.keys()) {
|
||||
const editor = currentTarget.closest(`.${type}-editor-box`);
|
||||
const script = editor?.parentNode?.previousElementSibling;
|
||||
if (script) {
|
||||
defineProperties(script, { xworker: { value: xworker } });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { sync } = xworker;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
envs.set(env, promise);
|
||||
sync.revoke = () => {
|
||||
URL.revokeObjectURL(srcLink);
|
||||
resolve(xworker);
|
||||
};
|
||||
}
|
||||
|
||||
// wait for the env then set the target div
|
||||
// before executing the current code
|
||||
return envs.get(env).then((xworker) => {
|
||||
xworker.onerror = ({ error }) => {
|
||||
if (hasRunButton) {
|
||||
outDiv.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<span style='color:red'>${
|
||||
error.message || error
|
||||
}</span>\n`,
|
||||
);
|
||||
}
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
const enable = () => {
|
||||
if (hasRunButton) currentTarget.disabled = false;
|
||||
};
|
||||
const { sync } = xworker;
|
||||
sync.write = (str) => {
|
||||
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||
else console.log(str);
|
||||
};
|
||||
sync.writeErr = (str) => {
|
||||
if (hasRunButton) {
|
||||
outDiv.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<span style='color:red'>${str}</span>\n`,
|
||||
);
|
||||
} else {
|
||||
notify(str);
|
||||
console.error(str);
|
||||
}
|
||||
};
|
||||
sync.runAsync(pySrc).then(enable, enable);
|
||||
});
|
||||
}
|
||||
|
||||
const makeRunButton = (handler, type) => {
|
||||
const runButton = document.createElement("button");
|
||||
runButton.className = `absolute ${type}-editor-run-button`;
|
||||
runButton.innerHTML = RUN_BUTTON;
|
||||
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||
runButton.addEventListener("click", async (event) => {
|
||||
runButton.blur();
|
||||
await handler.handleEvent(event);
|
||||
});
|
||||
return runButton;
|
||||
};
|
||||
|
||||
const makeEditorDiv = (handler, type) => {
|
||||
const editorDiv = document.createElement("div");
|
||||
editorDiv.className = `${type}-editor-input`;
|
||||
editorDiv.setAttribute("aria-label", "Python Script Area");
|
||||
|
||||
const runButton = makeRunButton(handler, type);
|
||||
const editorShadowContainer = document.createElement("div");
|
||||
|
||||
// avoid outer elements intercepting key events (reveal as example)
|
||||
editorShadowContainer.addEventListener("keydown", (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
editorDiv.append(runButton, editorShadowContainer);
|
||||
|
||||
return editorDiv;
|
||||
};
|
||||
|
||||
const makeOutDiv = (type) => {
|
||||
const outDiv = document.createElement("div");
|
||||
outDiv.className = `${type}-editor-output`;
|
||||
outDiv.id = `${getID(type)}-output`;
|
||||
return outDiv;
|
||||
};
|
||||
|
||||
const makeBoxDiv = (handler, type) => {
|
||||
const boxDiv = document.createElement("div");
|
||||
boxDiv.className = `${type}-editor-box`;
|
||||
|
||||
const editorDiv = makeEditorDiv(handler, type);
|
||||
const outDiv = makeOutDiv(type);
|
||||
boxDiv.append(editorDiv, outDiv);
|
||||
|
||||
return [boxDiv, outDiv, editorDiv.querySelector("button")];
|
||||
};
|
||||
|
||||
const init = async (script, type, interpreter) => {
|
||||
const [
|
||||
{ basicSetup, EditorView },
|
||||
{ Compartment },
|
||||
{ python },
|
||||
{ indentUnit },
|
||||
{ keymap },
|
||||
{ defaultKeymap, indentWithTab },
|
||||
] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||
),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||
]);
|
||||
|
||||
let isSetup = script.hasAttribute("setup");
|
||||
const hasConfig = script.hasAttribute("config");
|
||||
const serviceWorker = script.getAttribute("service-worker");
|
||||
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||
|
||||
// helps preventing too lazy ServiceWorker initialization on button run
|
||||
if (serviceWorker) {
|
||||
new XWorker("data:application/javascript,postMessage(0)", {
|
||||
type: "dummy",
|
||||
serviceWorker,
|
||||
}).onmessage = ({ target }) => target.terminate();
|
||||
}
|
||||
|
||||
if (hasConfig && configs.has(env)) {
|
||||
throw new SyntaxError(
|
||||
configs.get(env)
|
||||
? `duplicated config for env: ${env}`
|
||||
: `unable to add a config to the env: ${env}`,
|
||||
);
|
||||
}
|
||||
|
||||
configs.set(env, hasConfig);
|
||||
|
||||
let source = script.textContent;
|
||||
|
||||
// verify the src points to a valid file that can be parsed
|
||||
const { src } = script;
|
||||
if (src) {
|
||||
try {
|
||||
source = validate(
|
||||
src,
|
||||
await fetch(src).then((b) => b.ok && b.text()),
|
||||
);
|
||||
} catch (error) {
|
||||
notify(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
// allow the listener to be overridden at distance
|
||||
handleEvent: execute,
|
||||
serviceWorker,
|
||||
interpreter,
|
||||
env,
|
||||
config: hasConfig && script.getAttribute("config"),
|
||||
get pySrc() {
|
||||
return isSetup ? source : editor.state.doc.toString();
|
||||
},
|
||||
get outDiv() {
|
||||
return isSetup ? null : outDiv;
|
||||
},
|
||||
};
|
||||
|
||||
let target;
|
||||
defineProperties(script, {
|
||||
target: { get: () => target },
|
||||
handleEvent: {
|
||||
get: () => context.handleEvent,
|
||||
set: (callback) => {
|
||||
// do not bother with logic if it was set back as its original handler
|
||||
if (callback === execute) context.handleEvent = execute;
|
||||
// in every other case be sure that if the listener override returned
|
||||
// `false` nothing happens, otherwise keep doing what it always did
|
||||
else {
|
||||
context.handleEvent = async (event) => {
|
||||
// trap the currentTarget ASAP (if any)
|
||||
// otherwise it gets lost asynchronously
|
||||
const { currentTarget } = event;
|
||||
// augment a code snapshot before invoking the override
|
||||
defineProperties(event, {
|
||||
code: { value: context.pySrc },
|
||||
});
|
||||
// avoid executing the default handler if the override returned `false`
|
||||
if ((await callback(event)) !== false)
|
||||
await execute.call(context, { currentTarget });
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
code: {
|
||||
get: () => context.pySrc,
|
||||
set: (insert) => {
|
||||
if (isSetup) return;
|
||||
editor.update([
|
||||
editor.state.update({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
},
|
||||
process: {
|
||||
/**
|
||||
* Simulate a setup node overriding the source to evaluate.
|
||||
* @param {string} code the Python code to evaluate.
|
||||
* @param {boolean} asRunButtonAction invoke the `Run` button handler.
|
||||
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||
*/
|
||||
value(code, asRunButtonAction = false) {
|
||||
if (asRunButtonAction) return listener();
|
||||
const wasSetup = isSetup;
|
||||
const wasSource = source;
|
||||
isSetup = true;
|
||||
source = code;
|
||||
const restore = () => {
|
||||
isSetup = wasSetup;
|
||||
source = wasSource;
|
||||
};
|
||||
return context
|
||||
.handleEvent({ currentTarget: null })
|
||||
.then(restore, restore);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const notifyEditor = () => {
|
||||
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||
script.dispatchEvent(event);
|
||||
};
|
||||
|
||||
if (isSetup) {
|
||||
await context.handleEvent({ currentTarget: null });
|
||||
notifyEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = script.getAttribute("target");
|
||||
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement(`${type}-editor`);
|
||||
target.style.display = "block";
|
||||
script.after(target);
|
||||
}
|
||||
|
||||
if (!target.id) target.id = getID(type);
|
||||
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||
|
||||
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
||||
const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
|
||||
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
|
||||
|
||||
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
|
||||
const parent = inputChild.attachShadow({ mode: "open" });
|
||||
// avoid inheriting styles from the outer component
|
||||
parent.innerHTML = `<style> :host { all: initial; }</style>`;
|
||||
|
||||
target.appendChild(boxDiv);
|
||||
|
||||
const doc = dedent(script.textContent).trim();
|
||||
|
||||
// preserve user indentation, if any
|
||||
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
||||
|
||||
const listener = () => runButton.click();
|
||||
const editor = new EditorView({
|
||||
extensions: [
|
||||
indentUnit.of(indentation),
|
||||
new Compartment().of(python()),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||
{ key: "Shift-Enter", run: listener, preventDefault: true },
|
||||
// @see https://codemirror.net/examples/tab/
|
||||
indentWithTab,
|
||||
]),
|
||||
basicSetup,
|
||||
],
|
||||
foldGutter: true,
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
parent,
|
||||
doc,
|
||||
});
|
||||
|
||||
editor.focus();
|
||||
notifyEditor();
|
||||
};
|
||||
|
||||
// avoid too greedy MutationObserver operations at distance
|
||||
let timeout = 0;
|
||||
|
||||
// avoid delayed initialization
|
||||
let queue = Promise.resolve();
|
||||
|
||||
// reset interval value then check for new scripts
|
||||
const resetTimeout = () => {
|
||||
timeout = 0;
|
||||
pyEditor();
|
||||
};
|
||||
|
||||
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||
const pyEditor = () => {
|
||||
if (timeout) return;
|
||||
timeout = setTimeout(resetTimeout, 250);
|
||||
for (const [type, interpreter] of TYPES) {
|
||||
const selector = `script[type="${type}-editor"]`;
|
||||
for (const script of document.querySelectorAll(selector)) {
|
||||
// avoid any further bootstrap by changing the type as active
|
||||
script.type += "-active";
|
||||
// don't await in here or multiple calls might happen
|
||||
// while the first script is being initialized
|
||||
queue = queue.then(() => init(script, type, interpreter));
|
||||
}
|
||||
}
|
||||
return queue;
|
||||
};
|
||||
|
||||
new MutationObserver(pyEditor).observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// try to check the current document ASAP
|
||||
export default pyEditor();
|
||||
60
core/src/plugins/py-terminal.js
Normal file
60
core/src/plugins/py-terminal.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { TYPES, relative_url } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
import { customObserver } from "polyscript/exports";
|
||||
|
||||
// will contain all valid selectors
|
||||
const SELECTORS = [];
|
||||
|
||||
// avoid processing same elements twice
|
||||
const processed = new WeakSet();
|
||||
|
||||
// show the error on main and
|
||||
// stops the module from keep executing
|
||||
const notifyAndThrow = (message) => {
|
||||
notify(message);
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||
|
||||
let addStyle = true;
|
||||
|
||||
for (const type of TYPES.keys()) {
|
||||
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
|
||||
SELECTORS.push(selector);
|
||||
customObserver.set(selector, async (element) => {
|
||||
// we currently support only one terminal on main as in "classic"
|
||||
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||
notifyAndThrow("You can use at most 1 main terminal");
|
||||
|
||||
// import styles lazily
|
||||
if (addStyle) {
|
||||
addStyle = false;
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("link"), {
|
||||
rel: "stylesheet",
|
||||
href: relative_url("./xterm.css", import.meta.url),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (processed.has(element)) return;
|
||||
processed.add(element);
|
||||
|
||||
const bootstrap = (module) => module.default(element);
|
||||
|
||||
// we can't be smart with template literals for the dynamic import
|
||||
// or bundlers are incapable of producing multiple files around
|
||||
if (type === "mpy") {
|
||||
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
|
||||
bootstrap,
|
||||
);
|
||||
} else {
|
||||
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
|
||||
bootstrap,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
252
core/src/plugins/py-terminal/mpy.js
Normal file
252
core/src/plugins/py-terminal/mpy.js
Normal file
@@ -0,0 +1,252 @@
|
||||
// PyScript pyodide terminal plugin
|
||||
import { hooks, inputFailure } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
// this callback will be serialized as string and it never needs
|
||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||
// only once thanks to the `sync.is_pyterminal()` check.
|
||||
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
if (type !== "mpy" || !sync.is_pyterminal()) return;
|
||||
|
||||
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
|
||||
|
||||
interpreter.registerJsModule("_pyscript_input", {
|
||||
input: pyterminal_read,
|
||||
});
|
||||
|
||||
run(
|
||||
[
|
||||
"from _pyscript_input import input",
|
||||
"from polyscript import currentScript as _",
|
||||
"__terminal__ = _.terminal",
|
||||
"del _",
|
||||
].join(";"),
|
||||
);
|
||||
|
||||
const missingReturn = new Uint8Array([13]);
|
||||
io.stdout = (buffer) => {
|
||||
if (buffer[0] === 10) pyterminal_write(missingReturn);
|
||||
pyterminal_write(buffer);
|
||||
};
|
||||
io.stderr = (error) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
// tiny shim of the code module with only interact
|
||||
// to bootstrap a REPL like environment
|
||||
interpreter.registerJsModule("code", {
|
||||
interact() {
|
||||
const encoder = new TextEncoderStream();
|
||||
encoder.readable.pipeTo(
|
||||
new WritableStream({
|
||||
write(buffer) {
|
||||
for (const c of buffer) interpreter.replProcessChar(c);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const writer = encoder.writable.getWriter();
|
||||
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
|
||||
|
||||
interpreter.replInit();
|
||||
},
|
||||
});
|
||||
|
||||
pyterminal_ready();
|
||||
};
|
||||
|
||||
export default async (element) => {
|
||||
// lazy load these only when a valid terminal is found
|
||||
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||
),
|
||||
]);
|
||||
|
||||
const terminalOptions = {
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
};
|
||||
|
||||
let stream;
|
||||
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = () => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...terminalOptions,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperties(element, {
|
||||
terminal: { value: terminal },
|
||||
process: {
|
||||
value: async (code) => {
|
||||
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||
await stream.write(`${line}\r`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
return terminal;
|
||||
};
|
||||
|
||||
// branch logic for the worker
|
||||
if (element.hasAttribute("worker")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
const terminal = init();
|
||||
|
||||
const { sync } = xworker;
|
||||
|
||||
// handle the read mode on input
|
||||
let promisedChunks = null;
|
||||
let readChunks = "";
|
||||
|
||||
sync.is_pyterminal = () => true;
|
||||
|
||||
// put the terminal in a read-only state
|
||||
// frees the worker on \r
|
||||
sync.pyterminal_read = (buffer) => {
|
||||
terminal.write(buffer);
|
||||
promisedChunks = Promise.withResolvers();
|
||||
return promisedChunks.promise;
|
||||
};
|
||||
|
||||
// write if not reading input
|
||||
sync.pyterminal_write = (buffer) => {
|
||||
if (!promisedChunks) terminal.write(buffer);
|
||||
};
|
||||
|
||||
// add the onData terminal listener which forwards to the worker
|
||||
// everything typed in a queued char-by-char way
|
||||
sync.pyterminal_ready = () => {
|
||||
let queue = Promise.resolve();
|
||||
stream = {
|
||||
write: (buffer) =>
|
||||
(queue = queue.then(() =>
|
||||
sync.pyterminal_stream_write(buffer),
|
||||
)),
|
||||
};
|
||||
terminal.onData((buffer) => {
|
||||
if (promisedChunks) {
|
||||
// handle backspace on input
|
||||
if (buffer === "\x7f") {
|
||||
// avoid over-greedy backspace
|
||||
if (readChunks.length) {
|
||||
readChunks = readChunks.slice(0, -1);
|
||||
// override previous char position
|
||||
// put an empty space to clear the char
|
||||
// move back position again
|
||||
buffer = "\b \b";
|
||||
} else buffer = "";
|
||||
} else readChunks += buffer;
|
||||
if (buffer) {
|
||||
terminal.write(buffer);
|
||||
if (readChunks.endsWith("\r")) {
|
||||
terminal.write("\n");
|
||||
promisedChunks.resolve(readChunks.slice(0, -1));
|
||||
promisedChunks = null;
|
||||
readChunks = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stream.write(buffer);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
} else {
|
||||
// ⚠️ In an ideal world the inputFailure should never be used on main.
|
||||
// However, Pyodide still can't compete with MicroPython REPL mode
|
||||
// so while it's OK to keep that entry on main as default, we need
|
||||
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
|
||||
// also throw whenever an `input(...)` is required / digited.
|
||||
hooks.main.codeBeforeRun.delete(inputFailure);
|
||||
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
if (type !== "mpy") return;
|
||||
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
const terminal = init();
|
||||
|
||||
const missingReturn = new Uint8Array([13]);
|
||||
io.stdout = (buffer) => {
|
||||
if (buffer[0] === 10) terminal.write(missingReturn);
|
||||
terminal.write(buffer);
|
||||
};
|
||||
|
||||
// expose the __terminal__ one-off reference
|
||||
globalThis.__py_terminal__ = terminal;
|
||||
run(
|
||||
[
|
||||
"from js import prompt as input",
|
||||
"from js import __py_terminal__ as __terminal__",
|
||||
].join(";"),
|
||||
);
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
// NOTE: this is NOT the same as the one within
|
||||
// the onWorkerReady callback!
|
||||
interpreter.registerJsModule("code", {
|
||||
interact() {
|
||||
const encoder = new TextEncoderStream();
|
||||
encoder.readable.pipeTo(
|
||||
new WritableStream({
|
||||
write(buffer) {
|
||||
for (const c of buffer)
|
||||
interpreter.replProcessChar(c);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
stream = encoder.writable.getWriter();
|
||||
terminal.onData((buffer) => stream.write(buffer));
|
||||
|
||||
interpreter.replInit();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
179
core/src/plugins/py-terminal/py.js
Normal file
179
core/src/plugins/py-terminal/py.js
Normal file
@@ -0,0 +1,179 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { hooks } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
// this callback will be serialized as string and it never needs
|
||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||
// only once thanks to the `sync.is_pyterminal()` check.
|
||||
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
if (type !== "py" || !sync.is_pyterminal()) return;
|
||||
|
||||
run(
|
||||
[
|
||||
"from polyscript import currentScript as _",
|
||||
"__terminal__ = _.terminal",
|
||||
"del _",
|
||||
].join(";"),
|
||||
);
|
||||
|
||||
let data = "";
|
||||
const { pyterminal_read, pyterminal_write } = sync;
|
||||
const decoder = new TextDecoder();
|
||||
const generic = {
|
||||
isatty: false,
|
||||
write(buffer) {
|
||||
data = decoder.decode(buffer);
|
||||
pyterminal_write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
|
||||
io.stderr = (error) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: false,
|
||||
stdin: () => pyterminal_read(data),
|
||||
});
|
||||
};
|
||||
|
||||
export default async (element) => {
|
||||
// lazy load these only when a valid terminal is found
|
||||
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
|
||||
await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
|
||||
),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
|
||||
),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||
),
|
||||
]);
|
||||
|
||||
const readline = new Readline();
|
||||
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = (options) => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...options,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(readline);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperties(element, {
|
||||
terminal: { value: terminal },
|
||||
process: {
|
||||
value: async (code) => {
|
||||
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||
terminal.paste(`${line}`);
|
||||
terminal.write("\r\n");
|
||||
do {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 0),
|
||||
);
|
||||
} while (!readline.activeRead?.resolve);
|
||||
readline.activeRead.resolve(line);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
return terminal;
|
||||
};
|
||||
|
||||
// branch logic for the worker
|
||||
if (element.hasAttribute("worker")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
init({
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
} else {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
if (type !== "py") return;
|
||||
|
||||
console.warn("py-terminal is read only on main thread");
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
// on main, it's easy to trash and clean the current terminal
|
||||
globalThis.__py_terminal__ = init({
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "underline",
|
||||
});
|
||||
run("from js import __py_terminal__ as __terminal__");
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
io.stderr = (error) => {
|
||||
readline.write(String(error.message || error));
|
||||
};
|
||||
|
||||
let data = "";
|
||||
const decoder = new TextDecoder();
|
||||
const generic = {
|
||||
isatty: false,
|
||||
write(buffer) {
|
||||
data = decoder.decode(buffer);
|
||||
readline.write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: false,
|
||||
stdin: () => readline.read(data),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
70
core/src/stdlib.js
Normal file
70
core/src/stdlib.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Create through Python the pyscript module through
|
||||
* the artifact generated at build time.
|
||||
* This the returned value is a string that must be used
|
||||
* either before a worker execute code or when the module
|
||||
* is registered on the main thread.
|
||||
*/
|
||||
|
||||
import pyscript from "./stdlib/pyscript.js";
|
||||
|
||||
class Ignore extends Array {
|
||||
#add = false;
|
||||
#paths;
|
||||
#array;
|
||||
constructor(array, ...paths) {
|
||||
super();
|
||||
this.#array = array;
|
||||
this.#paths = paths;
|
||||
}
|
||||
push(...values) {
|
||||
if (this.#add) super.push(...values);
|
||||
return this.#array.push(...values);
|
||||
}
|
||||
path(path) {
|
||||
for (const _path of this.#paths) {
|
||||
// bails out at the first `true` value
|
||||
if ((this.#add = path.startsWith(_path))) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { entries } = Object;
|
||||
|
||||
const python = [
|
||||
"import os as _os",
|
||||
"from pathlib import Path as _Path",
|
||||
"_path = None",
|
||||
];
|
||||
|
||||
const ignore = new Ignore(python, "-");
|
||||
|
||||
const write = (base, literal) => {
|
||||
for (const [key, value] of entries(literal)) {
|
||||
ignore.path(`${base}/${key}`);
|
||||
ignore.push(`_path = _Path("${base}/${key}")`);
|
||||
if (typeof value === "string") {
|
||||
const code = JSON.stringify(value);
|
||||
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
|
||||
} else {
|
||||
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
||||
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||
write(`${base}/${key}`, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
write(".", pyscript);
|
||||
|
||||
// in order to fix js.document in the Worker case
|
||||
// we need to bootstrap pyscript module ASAP
|
||||
python.push("import pyscript as _pyscript");
|
||||
|
||||
python.push(
|
||||
...["_Path", "_path", "_os", "_pyscript"].map((ref) => `del ${ref}`),
|
||||
);
|
||||
python.push("\n");
|
||||
|
||||
export const stdlib = python.join("\n");
|
||||
export const optional = ignore.join("\n");
|
||||
18
core/src/stdlib/pyscript.js
Normal file
18
core/src/stdlib/pyscript.js
Normal file
File diff suppressed because one or more lines are too long
@@ -29,21 +29,23 @@
|
||||
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
||||
# as it works transparently in both the main thread and worker cases.
|
||||
|
||||
from polyscript import lazy_py_modules as py_import
|
||||
from pyscript.event_handling import when
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
PyWorker,
|
||||
config,
|
||||
current_target,
|
||||
document,
|
||||
js_import,
|
||||
js_modules,
|
||||
sync,
|
||||
window,
|
||||
)
|
||||
from pyscript.storage import Storage, storage
|
||||
from pyscript.websocket import WebSocket
|
||||
|
||||
try:
|
||||
from pyscript.event_handling import when
|
||||
except:
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
when = NotSupported(
|
||||
"pyscript.when", "pyscript.when currently not available with this interpreter"
|
||||
)
|
||||
if not RUNNING_IN_WORKER:
|
||||
from pyscript.workers import create_named_worker, workers
|
||||
@@ -6,17 +6,17 @@ import re
|
||||
from pyscript.magic_js import current_target, document, window
|
||||
|
||||
_MIME_METHODS = {
|
||||
"__repr__": "text/plain",
|
||||
"_repr_html_": "text/html",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"savefig": "image/png",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_html_": "text/html",
|
||||
"__repr__": "text/plain",
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ def _format_mime(obj):
|
||||
format_dict = mimebundle
|
||||
|
||||
output, not_available = None, []
|
||||
for method, mime_type in reversed(_MIME_METHODS.items()):
|
||||
for method, mime_type in _MIME_METHODS.items():
|
||||
if mime_type in format_dict:
|
||||
output = format_dict[mime_type]
|
||||
else:
|
||||
109
core/src/stdlib/pyscript/event_handling.py
Normal file
109
core/src/stdlib/pyscript/event_handling.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import inspect
|
||||
|
||||
try:
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
|
||||
except ImportError:
|
||||
|
||||
def add_event_listener(el, event_type, func):
|
||||
el.addEventListener(event_type, func)
|
||||
|
||||
|
||||
from pyscript.magic_js import document
|
||||
|
||||
|
||||
def when(target, *args, **kwargs):
|
||||
"""
|
||||
A decorator and function for attaching event handlers to DOM elements or
|
||||
whenable objects.
|
||||
|
||||
When used as a decorator, the target is the object that will trigger the
|
||||
event. The handler function is the decorated function. The handler function
|
||||
will be called when the target is triggered.
|
||||
|
||||
When used as a function, the target is the object that will trigger the
|
||||
event. The handler function is the next argument. The remaining arguments
|
||||
and keyword arguments are passed to the target when it is triggered.
|
||||
"""
|
||||
# If "when" is called as a function, try to grab the handler from the
|
||||
# arguments. If there's no handler, this must be a decorator based call.
|
||||
handler = None
|
||||
if args and callable(args[0]):
|
||||
handler = args[0]
|
||||
args = args[1:]
|
||||
elif callable(kwargs.get("handler")):
|
||||
handler = kwargs.pop("handler")
|
||||
|
||||
# Does the target implement the when protocol?
|
||||
whenable = hasattr(target, "__when__")
|
||||
# If not when-able, the DOM selector for the target event.
|
||||
if not whenable:
|
||||
# The target is an event linked to a DOM selector. Extract the
|
||||
# selector from the arguments or keyword arguments.
|
||||
if args:
|
||||
selector = args[0]
|
||||
elif kwargs:
|
||||
selector = kwargs.get("selector")
|
||||
if not selector:
|
||||
# There must be a selector if the target is not when-able.
|
||||
raise ValueError("No selector provided.")
|
||||
# Grab the DOM elements to which the target event will be attached.
|
||||
from pyscript.web import Element, ElementCollection
|
||||
|
||||
if isinstance(selector, str):
|
||||
elements = document.querySelectorAll(selector)
|
||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
||||
# and we can better manage the imports without circular dependencies
|
||||
elif isinstance(selector, Element):
|
||||
elements = [selector._dom_element]
|
||||
elif isinstance(selector, ElementCollection):
|
||||
elements = [el._dom_element for el in selector]
|
||||
else:
|
||||
if isinstance(selector, list):
|
||||
elements = selector
|
||||
else:
|
||||
elements = [selector]
|
||||
|
||||
def decorator(func):
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
# Function is async: must be awaited
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
await func()
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
func()
|
||||
|
||||
else:
|
||||
wrapper = func
|
||||
except AttributeError:
|
||||
# TODO: this is very ugly hack to get micropython working because inspect.signature
|
||||
# doesn't exist, but we need to actually properly replace inspect.signature.
|
||||
# It may be actually better to not try any magic for now and raise the error
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if "takes" in str(e) and "positional arguments" in str(e):
|
||||
return func()
|
||||
|
||||
raise
|
||||
|
||||
if whenable:
|
||||
target.__when__(wrapper, *args, **kwargs)
|
||||
else:
|
||||
for el in elements:
|
||||
add_event_listener(el, target, wrapper)
|
||||
|
||||
return func
|
||||
|
||||
if handler:
|
||||
decorator(handler)
|
||||
else:
|
||||
return decorator
|
||||
87
core/src/stdlib/pyscript/fetch.py
Normal file
87
core/src/stdlib/pyscript/fetch.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import json
|
||||
|
||||
import js
|
||||
from pyscript.util import as_bytearray
|
||||
|
||||
|
||||
### wrap the response to grant Pythonic results
|
||||
class _Response:
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
# grant access to response.ok and other fields
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._response, attr)
|
||||
|
||||
# exposed methods with Pythonic results
|
||||
async def arrayBuffer(self):
|
||||
buffer = await self._response.arrayBuffer()
|
||||
# works in Pyodide
|
||||
if hasattr(buffer, "to_py"):
|
||||
return buffer.to_py()
|
||||
# shims in MicroPython
|
||||
return memoryview(as_bytearray(buffer))
|
||||
|
||||
async def blob(self):
|
||||
return await self._response.blob()
|
||||
|
||||
async def bytearray(self):
|
||||
buffer = await self._response.arrayBuffer()
|
||||
return as_bytearray(buffer)
|
||||
|
||||
async def json(self):
|
||||
return json.loads(await self.text())
|
||||
|
||||
async def text(self):
|
||||
return await self._response.text()
|
||||
|
||||
|
||||
### allow direct await to _Response methods
|
||||
class _DirectResponse:
|
||||
@staticmethod
|
||||
def setup(promise, response):
|
||||
promise._response = _Response(response)
|
||||
return promise._response
|
||||
|
||||
def __init__(self, promise):
|
||||
self._promise = promise
|
||||
promise._response = None
|
||||
promise.arrayBuffer = self.arrayBuffer
|
||||
promise.blob = self.blob
|
||||
promise.bytearray = self.bytearray
|
||||
promise.json = self.json
|
||||
promise.text = self.text
|
||||
|
||||
async def _response(self):
|
||||
if not self._promise._response:
|
||||
await self._promise
|
||||
return self._promise._response
|
||||
|
||||
async def arrayBuffer(self):
|
||||
response = await self._response()
|
||||
return await response.arrayBuffer()
|
||||
|
||||
async def blob(self):
|
||||
response = await self._response()
|
||||
return await response.blob()
|
||||
|
||||
async def bytearray(self):
|
||||
response = await self._response()
|
||||
return await response.bytearray()
|
||||
|
||||
async def json(self):
|
||||
response = await self._response()
|
||||
return await response.json()
|
||||
|
||||
async def text(self):
|
||||
response = await self._response()
|
||||
return await response.text()
|
||||
|
||||
|
||||
def fetch(url, **kw):
|
||||
# workaround Pyodide / MicroPython dict <-> js conversion
|
||||
options = js.JSON.parse(json.dumps(kw))
|
||||
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
|
||||
promise = js.fetch(url, options).then(awaited)
|
||||
_DirectResponse(promise)
|
||||
return promise
|
||||
18
core/src/stdlib/pyscript/ffi.py
Normal file
18
core/src/stdlib/pyscript/ffi.py
Normal file
@@ -0,0 +1,18 @@
|
||||
try:
|
||||
import js
|
||||
from pyodide.ffi import create_proxy as _cp
|
||||
from pyodide.ffi import to_js as _py_tjs
|
||||
|
||||
from_entries = js.Object.fromEntries
|
||||
|
||||
def _tjs(value, **kw):
|
||||
if not hasattr(kw, "dict_converter"):
|
||||
kw["dict_converter"] = from_entries
|
||||
return _py_tjs(value, **kw)
|
||||
|
||||
except:
|
||||
from jsffi import create_proxy as _cp
|
||||
from jsffi import to_js as _tjs
|
||||
|
||||
create_proxy = _cp
|
||||
to_js = _tjs
|
||||
148
core/src/stdlib/pyscript/flatted.py
Normal file
148
core/src/stdlib/pyscript/flatted.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# https://www.npmjs.com/package/flatted
|
||||
|
||||
import json as _json
|
||||
|
||||
|
||||
class _Known:
|
||||
def __init__(self):
|
||||
self.key = []
|
||||
self.value = []
|
||||
|
||||
|
||||
class _String:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
def _array_keys(value):
|
||||
keys = []
|
||||
i = 0
|
||||
for _ in value:
|
||||
keys.append(i)
|
||||
i += 1
|
||||
return keys
|
||||
|
||||
|
||||
def _object_keys(value):
|
||||
keys = []
|
||||
for key in value:
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
|
||||
def _is_array(value):
|
||||
return isinstance(value, list) or isinstance(value, tuple)
|
||||
|
||||
|
||||
def _is_object(value):
|
||||
return isinstance(value, dict)
|
||||
|
||||
|
||||
def _is_string(value):
|
||||
return isinstance(value, str)
|
||||
|
||||
|
||||
def _index(known, input, value):
|
||||
input.append(value)
|
||||
index = str(len(input) - 1)
|
||||
known.key.append(value)
|
||||
known.value.append(index)
|
||||
return index
|
||||
|
||||
|
||||
def _loop(keys, input, known, output):
|
||||
for key in keys:
|
||||
value = output[key]
|
||||
if isinstance(value, _String):
|
||||
_ref(key, input[int(value.value)], input, known, output)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _ref(key, value, input, known, output):
|
||||
if _is_array(value) and not value in known:
|
||||
known.append(value)
|
||||
value = _loop(_array_keys(value), input, known, value)
|
||||
elif _is_object(value) and not value in known:
|
||||
known.append(value)
|
||||
value = _loop(_object_keys(value), input, known, value)
|
||||
|
||||
output[key] = value
|
||||
|
||||
|
||||
def _relate(known, input, value):
|
||||
if _is_string(value) or _is_array(value) or _is_object(value):
|
||||
try:
|
||||
return known.value[known.key.index(value)]
|
||||
except:
|
||||
return _index(known, input, value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _transform(known, input, value):
|
||||
if _is_array(value):
|
||||
output = []
|
||||
for val in value:
|
||||
output.append(_relate(known, input, val))
|
||||
return output
|
||||
|
||||
if _is_object(value):
|
||||
obj = {}
|
||||
for key in value:
|
||||
obj[key] = _relate(known, input, value[key])
|
||||
return obj
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _wrap(value):
|
||||
if _is_string(value):
|
||||
return _String(value)
|
||||
|
||||
if _is_array(value):
|
||||
i = 0
|
||||
for val in value:
|
||||
value[i] = _wrap(val)
|
||||
i += 1
|
||||
|
||||
elif _is_object(value):
|
||||
for key in value:
|
||||
value[key] = _wrap(value[key])
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def parse(value, *args, **kwargs):
|
||||
json = _json.loads(value, *args, **kwargs)
|
||||
wrapped = []
|
||||
for value in json:
|
||||
wrapped.append(_wrap(value))
|
||||
|
||||
input = []
|
||||
for value in wrapped:
|
||||
if isinstance(value, _String):
|
||||
input.append(value.value)
|
||||
else:
|
||||
input.append(value)
|
||||
|
||||
value = input[0]
|
||||
|
||||
if _is_array(value):
|
||||
return _loop(_array_keys(value), input, [value], value)
|
||||
|
||||
if _is_object(value):
|
||||
return _loop(_object_keys(value), input, [value], value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def stringify(value, *args, **kwargs):
|
||||
known = _Known()
|
||||
input = []
|
||||
output = []
|
||||
i = int(_index(known, input, value))
|
||||
while i < len(input):
|
||||
output.append(_transform(known, input, input[i]))
|
||||
i += 1
|
||||
return _json.dumps(output, *args, **kwargs)
|
||||
79
core/src/stdlib/pyscript/magic_js.py
Normal file
79
core/src/stdlib/pyscript/magic_js.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
import js as globalThis
|
||||
from polyscript import config as _config
|
||||
from polyscript import js_modules
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
||||
|
||||
config = json.loads(globalThis.JSON.stringify(_config))
|
||||
|
||||
if "MicroPython" in sys.version:
|
||||
config["type"] = "mpy"
|
||||
else:
|
||||
config["type"] = "py"
|
||||
|
||||
|
||||
# allow `from pyscript.js_modules.xxx import yyy`
|
||||
class JSModule:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __getattr__(self, field):
|
||||
# avoid pyodide looking for non existent fields
|
||||
if not field.startswith("_"):
|
||||
return getattr(getattr(js_modules, self.name), field)
|
||||
|
||||
|
||||
# generate N modules in the system that will proxy the real value
|
||||
for name in globalThis.Reflect.ownKeys(js_modules):
|
||||
sys.modules[f"pyscript.js_modules.{name}"] = JSModule(name)
|
||||
sys.modules["pyscript.js_modules"] = js_modules
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
import polyscript
|
||||
|
||||
PyWorker = NotSupported(
|
||||
"pyscript.PyWorker",
|
||||
"pyscript.PyWorker works only when running in the main thread",
|
||||
)
|
||||
|
||||
try:
|
||||
import js
|
||||
|
||||
window = polyscript.xworker.window
|
||||
document = window.document
|
||||
js.document = document
|
||||
# this is the same as js_import on main and it lands modules on main
|
||||
js_import = window.Function(
|
||||
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||
)()
|
||||
except:
|
||||
message = "Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
|
||||
globalThis.console.warn(message)
|
||||
window = NotSupported("pyscript.window", message)
|
||||
document = NotSupported("pyscript.document", message)
|
||||
js_import = None
|
||||
|
||||
sync = polyscript.xworker.sync
|
||||
|
||||
# in workers the display does not have a default ID
|
||||
# but there is a sync utility from xworker
|
||||
def current_target():
|
||||
return polyscript.target
|
||||
|
||||
else:
|
||||
import _pyscript
|
||||
from _pyscript import PyWorker, js_import
|
||||
|
||||
window = globalThis
|
||||
document = globalThis.document
|
||||
sync = NotSupported(
|
||||
"pyscript.sync", "pyscript.sync works only when running in a worker"
|
||||
)
|
||||
|
||||
# in MAIN the current element target exist, just use it
|
||||
def current_target():
|
||||
return _pyscript.target
|
||||
91
core/src/stdlib/pyscript/media.py
Normal file
91
core/src/stdlib/pyscript/media.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from pyscript import window
|
||||
from pyscript.ffi import to_js
|
||||
|
||||
|
||||
class Device:
|
||||
"""Device represents a media input or output device, such as a microphone,
|
||||
camera, or headset.
|
||||
"""
|
||||
|
||||
def __init__(self, device):
|
||||
self._dom_element = device
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._dom_element.deviceId
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self._dom_element.groupId
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
return self._dom_element.kind
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._dom_element.label
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
@classmethod
|
||||
async def load(cls, audio=False, video=True):
|
||||
"""Load the device stream."""
|
||||
options = window.Object.new()
|
||||
options.audio = audio
|
||||
if isinstance(video, bool):
|
||||
options.video = video
|
||||
else:
|
||||
# TODO: Think this can be simplified but need to check it on the pyodide side
|
||||
|
||||
# TODO: this is pyodide specific. shouldn't be!
|
||||
options.video = window.Object.new()
|
||||
for k in video:
|
||||
setattr(options.video, k, to_js(video[k]))
|
||||
|
||||
stream = await window.navigator.mediaDevices.getUserMedia(options)
|
||||
return stream
|
||||
|
||||
async def get_stream(self):
|
||||
key = self.kind.replace("input", "").replace("output", "")
|
||||
options = {key: {"deviceId": {"exact": self.id}}}
|
||||
|
||||
return await self.load(**options)
|
||||
|
||||
|
||||
async def list_devices() -> list[dict]:
|
||||
"""
|
||||
Return the list of the currently available media input and output devices,
|
||||
such as microphones, cameras, headsets, and so forth.
|
||||
|
||||
Output:
|
||||
|
||||
list(dict) - list of dictionaries representing the available media devices.
|
||||
Each dictionary has the following keys:
|
||||
* deviceId: a string that is an identifier for the represented device
|
||||
that is persisted across sessions. It is un-guessable by other
|
||||
applications and unique to the origin of the calling application.
|
||||
It is reset when the user clears cookies (for Private Browsing, a
|
||||
different identifier is used that is not persisted across sessions).
|
||||
|
||||
* groupId: a string that is a group identifier. Two devices have the same
|
||||
group identifier if they belong to the same physical device — for
|
||||
example a monitor with both a built-in camera and a microphone.
|
||||
|
||||
* kind: an enumerated value that is either "videoinput", "audioinput"
|
||||
or "audiooutput".
|
||||
|
||||
* label: a string describing this device (for example "External USB
|
||||
Webcam").
|
||||
|
||||
Note: the returned list will omit any devices that are blocked by the document
|
||||
Permission Policy: microphone, camera, speaker-selection (for output devices),
|
||||
and so on. Access to particular non-default devices is also gated by the
|
||||
Permissions API, and the list will omit devices for which the user has not
|
||||
granted explicit permission.
|
||||
"""
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
|
||||
return [
|
||||
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
|
||||
]
|
||||
60
core/src/stdlib/pyscript/storage.py
Normal file
60
core/src/stdlib/pyscript/storage.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from polyscript import storage as _storage
|
||||
from pyscript.flatted import parse as _parse
|
||||
from pyscript.flatted import stringify as _stringify
|
||||
|
||||
|
||||
# convert a Python value into an IndexedDB compatible entry
|
||||
def _to_idb(value):
|
||||
if value is None:
|
||||
return _stringify(["null", 0])
|
||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||
return _stringify(["generic", value])
|
||||
if isinstance(value, bytearray):
|
||||
return _stringify(["bytearray", [v for v in value]])
|
||||
if isinstance(value, memoryview):
|
||||
return _stringify(["memoryview", [v for v in value]])
|
||||
raise TypeError(f"Unexpected value: {value}")
|
||||
|
||||
|
||||
# convert an IndexedDB compatible entry into a Python value
|
||||
def _from_idb(value):
|
||||
(
|
||||
kind,
|
||||
result,
|
||||
) = _parse(value)
|
||||
if kind == "null":
|
||||
return None
|
||||
if kind == "generic":
|
||||
return result
|
||||
if kind == "bytearray":
|
||||
return bytearray(result)
|
||||
if kind == "memoryview":
|
||||
return memoryview(bytearray(result))
|
||||
return value
|
||||
|
||||
|
||||
class Storage(dict):
|
||||
def __init__(self, store):
|
||||
super().__init__({k: _from_idb(v) for k, v in store.entries()})
|
||||
self.__store__ = store
|
||||
|
||||
def __delitem__(self, attr):
|
||||
self.__store__.delete(attr)
|
||||
super().__delitem__(attr)
|
||||
|
||||
def __setitem__(self, attr, value):
|
||||
self.__store__.set(attr, _to_idb(value))
|
||||
super().__setitem__(attr, value)
|
||||
|
||||
def clear(self):
|
||||
self.__store__.clear()
|
||||
super().clear()
|
||||
|
||||
async def sync(self):
|
||||
await self.__store__.sync()
|
||||
|
||||
|
||||
async def storage(name="", storage_class=Storage):
|
||||
if not name:
|
||||
raise ValueError("The storage name must be defined")
|
||||
return storage_class(await _storage(f"@pyscript/{name}"))
|
||||
@@ -1,3 +1,15 @@
|
||||
import js
|
||||
|
||||
|
||||
def as_bytearray(buffer):
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
size = ui8a.length
|
||||
ba = bytearray(size)
|
||||
for i in range(0, size):
|
||||
ba[i] = ui8a[i]
|
||||
return ba
|
||||
|
||||
|
||||
class NotSupported:
|
||||
"""
|
||||
Small helper that raises exceptions if you try to get/set any attribute on
|
||||
1176
core/src/stdlib/pyscript/web.py
Normal file
1176
core/src/stdlib/pyscript/web.py
Normal file
File diff suppressed because it is too large
Load Diff
71
core/src/stdlib/pyscript/websocket.py
Normal file
71
core/src/stdlib/pyscript/websocket.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import js
|
||||
from pyscript.ffi import create_proxy
|
||||
from pyscript.util import as_bytearray
|
||||
|
||||
code = "code"
|
||||
protocols = "protocols"
|
||||
reason = "reason"
|
||||
methods = ["onclose", "onerror", "onmessage", "onopen"]
|
||||
|
||||
|
||||
class EventMessage:
|
||||
def __init__(self, event):
|
||||
self._event = event
|
||||
|
||||
def __getattr__(self, attr):
|
||||
value = getattr(self._event, attr)
|
||||
|
||||
if attr == "data" and not isinstance(value, str):
|
||||
if hasattr(value, "to_py"):
|
||||
return value.to_py()
|
||||
# shims in MicroPython
|
||||
return memoryview(as_bytearray(value))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class WebSocket(object):
|
||||
CONNECTING = 0
|
||||
OPEN = 1
|
||||
CLOSING = 2
|
||||
CLOSED = 3
|
||||
|
||||
def __init__(self, **kw):
|
||||
url = kw["url"]
|
||||
if protocols in kw:
|
||||
socket = js.WebSocket.new(url, kw[protocols])
|
||||
else:
|
||||
socket = js.WebSocket.new(url)
|
||||
object.__setattr__(self, "_ws", socket)
|
||||
|
||||
for t in methods:
|
||||
if t in kw:
|
||||
# Pyodide fails at setting socket[t] directly
|
||||
setattr(socket, t, create_proxy(kw[t]))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._ws, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr in methods:
|
||||
m = lambda e: value(EventMessage(e))
|
||||
setattr(self._ws, attr, create_proxy(m))
|
||||
else:
|
||||
setattr(self._ws, attr, value)
|
||||
|
||||
def close(self, **kw):
|
||||
if code in kw and reason in kw:
|
||||
self._ws.close(kw[code], kw[reason])
|
||||
elif code in kw:
|
||||
self._ws.close(kw[code])
|
||||
else:
|
||||
self._ws.close()
|
||||
|
||||
def send(self, data):
|
||||
if isinstance(data, str):
|
||||
self._ws.send(data)
|
||||
else:
|
||||
buffer = js.Uint8Array.new(len(data))
|
||||
for pos, b in enumerate(data):
|
||||
buffer[pos] = b
|
||||
self._ws.send(buffer)
|
||||
43
core/src/stdlib/pyscript/workers.py
Normal file
43
core/src/stdlib/pyscript/workers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import js as _js
|
||||
from polyscript import workers as _workers
|
||||
|
||||
_get = _js.Reflect.get
|
||||
|
||||
|
||||
def _set(script, name, value=""):
|
||||
script.setAttribute(name, value)
|
||||
|
||||
|
||||
# this solves an inconsistency between Pyodide and MicroPython
|
||||
# @see https://github.com/pyscript/pyscript/issues/2106
|
||||
class _ReadOnlyProxy:
|
||||
def __getitem__(self, name):
|
||||
return _get(_workers, name)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return _get(_workers, name)
|
||||
|
||||
|
||||
workers = _ReadOnlyProxy()
|
||||
|
||||
|
||||
async def create_named_worker(src="", name="", config=None, type="py"):
|
||||
from json import dumps
|
||||
|
||||
if not src:
|
||||
raise ValueError("Named workers require src")
|
||||
|
||||
if not name:
|
||||
raise ValueError("Named workers require a name")
|
||||
|
||||
s = _js.document.createElement("script")
|
||||
s.type = type
|
||||
s.src = src
|
||||
_set(s, "worker")
|
||||
_set(s, "name", name)
|
||||
|
||||
if config:
|
||||
_set(s, "config", isinstance(config, str) and config or dumps(config))
|
||||
|
||||
_js.document.body.append(s)
|
||||
return await workers[name]
|
||||
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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,7 @@
|
||||
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.
|
||||
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
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript tests</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; }
|
||||
a {
|
||||
display: block;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
a, span { opacity: .7; }
|
||||
a:hover { opacity: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body><ul><li><strong><span>javascript</span></strong><ul><li><a href="./javascript/async-listener.html">async-listener<small>.html</small></a></li><li><a href="./javascript/config-url.html">config-url<small>.html</small></a></li><li><a href="./javascript/config_type.html">config_type<small>.html</small></a></li><li><strong><a href="./javascript/fetch/index.html">fetch</a></strong></li><li><a href="./javascript/ffi.html">ffi<small>.html</small></a></li><li><a href="./javascript/hooks.html">hooks<small>.html</small></a></li><li><strong><a href="./javascript/issue-2093/index.html">issue-2093</a></strong></li><li><a href="./javascript/js-storage.html">js-storage<small>.html</small></a></li><li><a href="./javascript/js_modules.html">js_modules<small>.html</small></a></li><li><strong><a href="./javascript/loader/index.html">loader</a></strong></li><li><a href="./javascript/mpy-error.html">mpy-error<small>.html</small></a></li><li><a href="./javascript/mpy-no-error.html">mpy-no-error<small>.html</small></a></li><li><a href="./javascript/mpy.html">mpy<small>.html</small></a></li><li><a href="./javascript/py-terminal-main.html">py-terminal-main<small>.html</small></a></li><li><a href="./javascript/py-terminal-worker.html">py-terminal-worker<small>.html</small></a></li><li><a href="./javascript/py-terminal.html">py-terminal<small>.html</small></a></li><li><a href="./javascript/py-terminals.html">py-terminals<small>.html</small></a></li><li><strong><a href="./javascript/pyodide-cache/index.html">pyodide-cache</a></strong></li><li><a href="./javascript/storage.html">storage<small>.html</small></a></li><li><strong><span>workers</span></strong><ul><li><strong><a href="./javascript/workers/create_named/index.html">create_named</a></strong></li><li><strong><a href="./javascript/workers/mpy/index.html">mpy</a></strong></li><li><strong><a href="./javascript/workers/py/index.html">py</a></strong></li></ul></li></ul></li><li><strong><a href="./manual/index.html">manual</a></strong><ul><li><a href="./manual/all-done.html">all-done<small>.html</small></a></li><li><a href="./manual/async.html">async<small>.html</small></a></li><li><a href="./manual/camera.html">camera<small>.html</small></a></li><li><a href="./manual/click.html">click<small>.html</small></a></li><li><a href="./manual/code-a-part.html">code-a-part<small>.html</small></a></li><li><a href="./manual/combo.html">combo<small>.html</small></a></li><li><a href="./manual/config.html">config<small>.html</small></a></li><li><a href="./manual/create-element.html">create-element<small>.html</small></a></li><li><a href="./manual/dialog.html">dialog<small>.html</small></a></li><li><a href="./manual/display.html">display<small>.html</small></a></li><li><strong><a href="./manual/donkey/index.html">donkey</a></strong></li><li><a href="./manual/error.html">error<small>.html</small></a></li><li><a href="./manual/html-decode.html">html-decode<small>.html</small></a></li><li><a href="./manual/input.html">input<small>.html</small></a></li><li><a href="./manual/interpreter.html">interpreter<small>.html</small></a></li><li><strong><a href="./manual/issue-7015/index.html">issue-7015</a></strong></li><li><a href="./manual/multi.html">multi<small>.html</small></a></li><li><a href="./manual/multiple-editors.html">multiple-editors<small>.html</small></a></li><li><a href="./manual/no-error.html">no-error<small>.html</small></a></li><li><strong><a href="./manual/no_sab/index.html">no_sab</a></strong></li><li><strong><a href="./manual/piratical/index.html">piratical</a></strong></li><li><a href="./manual/py-editor.html">py-editor<small>.html</small></a></li><li><a href="./manual/py-editor-failure.html">py-editor-failure<small>.html</small></a></li><li><strong><a href="./manual/py-terminals/index.html">py-terminals</a></strong><ul><li><a href="./manual/py-terminals/no-repl.html">no-repl<small>.html</small></a></li><li><a href="./manual/py-terminals/repl.html">repl<small>.html</small></a></li></ul></li><li><a href="./manual/py_modules.html">py_modules<small>.html</small></a></li><li><strong><a href="./manual/service-worker/index.html">service-worker</a></strong></li><li><a href="./manual/split-config.html">split-config<small>.html</small></a></li><li><a href="./manual/submit.html">submit<small>.html</small></a></li><li><a href="./manual/target.html">target<small>.html</small></a></li><li><a href="./manual/test_display_HTML.html">test_display_HTML<small>.html</small></a></li><li><a href="./manual/test_when.html">test_when<small>.html</small></a></li><li><a href="./manual/worker.html">worker<small>.html</small></a></li></ul></li><li><strong><a href="./python/index.html">python</a></strong></li></ul></body>
|
||||
</html>
|
||||
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>
|
||||
25
core/tests/javascript/config-url.html
Normal file
25
core/tests/javascript/config-url.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin</title>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<mpy-config src="config-url/config.json"></mpy-config>
|
||||
<script type="mpy">
|
||||
from pyscript import config
|
||||
if config["files"]["{TO}"] != "./runtime":
|
||||
raise Exception("wrong config tree")
|
||||
|
||||
from runtime import test
|
||||
</script>
|
||||
<script type="mpy" worker>
|
||||
from pyscript import config
|
||||
if config["files"]["{TO}"] != "./runtime":
|
||||
raise Exception("wrong config tree")
|
||||
|
||||
from runtime import test
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
7
core/tests/javascript/config-url/config.json
Normal file
7
core/tests/javascript/config-url/config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files":{
|
||||
"{FROM}": "./src",
|
||||
"{TO}": "./runtime",
|
||||
"{FROM}/test.py": "{TO}/test.py"
|
||||
}
|
||||
}
|
||||
8
core/tests/javascript/config-url/src/test.py
Normal file
8
core/tests/javascript/config-url/src/test.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from pyscript import RUNNING_IN_WORKER, document
|
||||
|
||||
classList = document.documentElement.classList
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
classList.add("worker")
|
||||
else:
|
||||
classList.add("main")
|
||||
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>
|
||||
95
core/tests/javascript/fetch/index.html
Normal file
95
core/tests/javascript/fetch/index.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import fetch from 'https://esm.run/@webreflection/fetch';
|
||||
|
||||
globalThis.fetch_text = await fetch("config.json").text();
|
||||
globalThis.fetch_json = JSON.stringify(await fetch("config.json").json());
|
||||
globalThis.fetch_buffer = new Uint8Array((await fetch("config.json").arrayBuffer())).length;
|
||||
|
||||
document.head.appendChild(
|
||||
Object.assign(
|
||||
document.createElement('script'),
|
||||
{
|
||||
type: 'module',
|
||||
src: '../../../dist/core.js'
|
||||
}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
<script type="mpy" async>
|
||||
import js, json
|
||||
from pyscript import document, fetch
|
||||
|
||||
fetch_text = await (await fetch("config.json")).text()
|
||||
if (fetch_text != js.fetch_text):
|
||||
raise Exception("fetch_text")
|
||||
|
||||
fetch_text = await fetch("config.json").text()
|
||||
if (fetch_text != js.fetch_text):
|
||||
raise Exception("fetch_text")
|
||||
|
||||
fetch_json = await (await fetch("config.json")).json()
|
||||
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||
raise Exception("fetch_json")
|
||||
|
||||
fetch_json = await fetch("config.json").json()
|
||||
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||
raise Exception("fetch_json")
|
||||
|
||||
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
|
||||
if (len(fetch_buffer) != js.fetch_buffer):
|
||||
raise Exception("fetch_buffer")
|
||||
|
||||
fetch_buffer = await fetch("config.json").arrayBuffer()
|
||||
if (len(fetch_buffer) != js.fetch_buffer):
|
||||
raise Exception("fetch_buffer")
|
||||
|
||||
print(await (await fetch("config.json")).bytearray())
|
||||
print(await (await fetch("config.json")).blob())
|
||||
|
||||
if (await fetch("shenanigans.nope")).ok == False:
|
||||
document.documentElement.classList.add('mpy')
|
||||
</script>
|
||||
<script type="py" async>
|
||||
import js, json
|
||||
from pyscript import document, fetch
|
||||
|
||||
fetch_text = await (await fetch("config.json")).text()
|
||||
if (fetch_text != js.fetch_text):
|
||||
raise Exception("fetch_text")
|
||||
|
||||
fetch_text = await fetch("config.json").text()
|
||||
if (fetch_text != js.fetch_text):
|
||||
raise Exception("fetch_text")
|
||||
|
||||
fetch_json = await (await fetch("config.json")).json()
|
||||
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||
raise Exception("fetch_json")
|
||||
|
||||
fetch_json = await fetch("config.json").json()
|
||||
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||
raise Exception("fetch_json")
|
||||
|
||||
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
|
||||
if (len(fetch_buffer) != js.fetch_buffer):
|
||||
raise Exception("fetch_buffer")
|
||||
|
||||
fetch_buffer = await fetch("config.json").arrayBuffer()
|
||||
if (len(fetch_buffer) != js.fetch_buffer):
|
||||
raise Exception("fetch_buffer")
|
||||
|
||||
print(await (await fetch("config.json")).bytearray())
|
||||
print(await (await fetch("config.json")).blob())
|
||||
|
||||
if (await fetch("shenanigans.nope")).ok == False:
|
||||
document.documentElement.classList.add('py')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
26
core/tests/javascript/ffi.html
Normal file
26
core/tests/javascript/ffi.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PyScript FFI</title>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy">
|
||||
from pyscript import document
|
||||
from pyscript.ffi import to_js
|
||||
document.documentElement.classList.add(
|
||||
to_js({"ok": "mpy"}).ok
|
||||
)
|
||||
</script>
|
||||
<script type="py">
|
||||
from pyscript import document
|
||||
from pyscript.ffi import to_js
|
||||
document.documentElement.classList.add(
|
||||
to_js({"ok": "py"}).ok
|
||||
)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,13 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin Bug?</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module">
|
||||
addEventListener('mpy:done', () => {
|
||||
document.documentElement.classList.add('done');
|
||||
});
|
||||
|
||||
import { hooks } from "../dist/core.js";
|
||||
import { hooks } from "../../dist/core.js";
|
||||
|
||||
// Main
|
||||
hooks.main.onReady.add((wrap, element) => {
|
||||
@@ -48,12 +48,12 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" worker>
|
||||
<script type="mpy" async="false" worker>
|
||||
from pyscript import document
|
||||
print("actual code in worker")
|
||||
document.documentElement.classList.add('worker')
|
||||
</script>
|
||||
<script type="mpy">
|
||||
<script type="mpy" async="false">
|
||||
print("actual code in main")
|
||||
</script>
|
||||
</body>
|
||||
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>
|
||||
39
core/tests/javascript/js_modules.html
Normal file
39
core/tests/javascript/js_modules.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<mpy-config>
|
||||
[js_modules.main]
|
||||
"./js_modules.js" = "random_js"
|
||||
</mpy-config>
|
||||
<mpy-script>
|
||||
from pyscript.js_modules.random_js import default as value
|
||||
from pyscript.js_modules import random_js
|
||||
from pyscript import js_modules
|
||||
|
||||
print("mpy", value)
|
||||
print("mpy", random_js.default)
|
||||
print("mpy", js_modules.random_js.default)
|
||||
</mpy-script>
|
||||
<py-config>
|
||||
[js_modules.main]
|
||||
"./js_modules.js" = "random_js"
|
||||
</py-config>
|
||||
<py-script>
|
||||
from pyscript.js_modules.random_js import default as value
|
||||
from pyscript.js_modules import random_js
|
||||
from pyscript import js_modules, document
|
||||
|
||||
print("py", value)
|
||||
print("py", random_js.default)
|
||||
print("py", js_modules.random_js.default)
|
||||
|
||||
document.documentElement.classList.add('done')
|
||||
</py-script>
|
||||
</body>
|
||||
</html>
|
||||
1
core/tests/javascript/js_modules.js
Normal file
1
core/tests/javascript/js_modules.js
Normal file
@@ -0,0 +1 @@
|
||||
export default Math.random();
|
||||
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>
|
||||
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>
|
||||
14
core/tests/javascript/mpy-no-error.html
Normal file
14
core/tests/javascript/mpy-no-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" config="mpy-no-error.toml">
|
||||
from pyscript import document
|
||||
import sys
|
||||
print("This is an error", file=sys.stderr)
|
||||
document.documentElement.classList.add("ok");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
1
core/tests/javascript/mpy-no-error.toml
Normal file
1
core/tests/javascript/mpy-no-error.toml
Normal file
@@ -0,0 +1 @@
|
||||
plugins = ["!error"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user