mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Compare commits
254 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02ff691d2 | ||
|
|
90ae3cea95 | ||
|
|
97f5922973 | ||
|
|
4a801fa3a2 | ||
|
|
7afe5c55e1 | ||
|
|
7785e17bb5 | ||
|
|
c37c3a1f9a | ||
|
|
d8250f2c8c | ||
|
|
83b41f9928 | ||
|
|
a8684a2168 | ||
|
|
f8cf58d6c4 | ||
|
|
8cd9c4c382 | ||
|
|
1f609233e7 | ||
|
|
66966a732e | ||
|
|
ec090922cb | ||
|
|
f769f215b2 | ||
|
|
ffc78ab6a2 | ||
|
|
b609b605f5 | ||
|
|
100a1e4bc1 | ||
|
|
c848061a44 | ||
|
|
2647e78480 | ||
|
|
482d57c27c | ||
|
|
4ce989acf3 | ||
|
|
1e62d0b1fe | ||
|
|
2d3ad0ab2d | ||
|
|
3657492c52 | ||
|
|
a8b8e1de36 | ||
|
|
726009029a | ||
|
|
8b35304ab4 | ||
|
|
9e4cb44d73 | ||
|
|
4bf3651c9a | ||
|
|
67fa31e4ea | ||
|
|
4937a46731 | ||
|
|
b4e9a3093c | ||
|
|
a129be8136 | ||
|
|
eaa6711756 | ||
|
|
b528ba67a9 | ||
|
|
71ad1a40cb | ||
|
|
e433275938 | ||
|
|
87256a662b | ||
|
|
7336ae545e | ||
|
|
d68260c0c7 | ||
|
|
14cc05fb80 | ||
|
|
42c6cb775e | ||
|
|
b11fb2e893 | ||
|
|
3223a9c7e9 | ||
|
|
139ce9b5fb | ||
|
|
3b1af0688c | ||
|
|
7284f7f15f | ||
|
|
16ebc50481 | ||
|
|
b911ea99fb | ||
|
|
46ca9154c4 | ||
|
|
afd7a8eb00 | ||
|
|
b22f384d73 | ||
|
|
caeab77a8e | ||
|
|
f2bbc6ed5f | ||
|
|
1d666b92a2 | ||
|
|
290eb03388 | ||
|
|
55031f2347 | ||
|
|
8168383653 | ||
|
|
3ff2c171bc | ||
|
|
edbac13713 | ||
|
|
46239caa19 | ||
|
|
0366e48fad | ||
|
|
b13317d32f | ||
|
|
57b1440a10 | ||
|
|
fc53356a1d | ||
|
|
5be99456f0 | ||
|
|
7adedcc704 | ||
|
|
d143b229ed | ||
|
|
0d74a60227 | ||
|
|
ce923a354f | ||
|
|
7e65836423 | ||
|
|
796373cfa6 | ||
|
|
0a1d3bb678 | ||
|
|
4e43d3e92d | ||
|
|
5acc2afaf3 | ||
|
|
56c64cbee7 | ||
|
|
4ff02a24d1 | ||
|
|
a5dc94792b | ||
|
|
0db79e0f02 | ||
|
|
283eabdb30 | ||
|
|
3f19e00410 | ||
|
|
9233d5e45a | ||
|
|
fe580cd90b | ||
|
|
00e6cfed29 | ||
|
|
6b1330d28a | ||
|
|
5d751493f6 | ||
|
|
c3517f7973 | ||
|
|
b1c33b7f79 | ||
|
|
722abda895 | ||
|
|
8061bc0143 | ||
|
|
993e812e7b | ||
|
|
a6b6dd8479 | ||
|
|
febbb031ac | ||
|
|
f827efe2fc | ||
|
|
8c6bfecbff | ||
|
|
f562860e49 | ||
|
|
62c78b0f42 | ||
|
|
4fe6ad80af | ||
|
|
89a0a556f3 | ||
|
|
af06bc4826 | ||
|
|
6fab9a1c26 | ||
|
|
386966ebd2 | ||
|
|
c257b704e6 | ||
|
|
fc599d96a2 | ||
|
|
8de97a7e7b | ||
|
|
2642be863b | ||
|
|
9dad29ec17 | ||
|
|
abb1eb28fe | ||
|
|
532ac0c80f | ||
|
|
c1849d28e0 | ||
|
|
ad2af2392b | ||
|
|
957ab69c21 | ||
|
|
f5d49ee52c | ||
|
|
06138bbb48 | ||
|
|
ae66d13d57 | ||
|
|
5aaeebf32c | ||
|
|
a6b0964185 | ||
|
|
dd86169f2a | ||
|
|
84c7d69db9 | ||
|
|
ca9b565adc | ||
|
|
b2d1018db1 | ||
|
|
c4e25d879e | ||
|
|
c82dbb755e | ||
|
|
1ed77321a5 | ||
|
|
e36a57eb06 | ||
|
|
ee3cd76022 | ||
|
|
eb31e51a45 | ||
|
|
c8c2dd0806 | ||
|
|
e525d54be0 | ||
|
|
7b9f7c13f5 | ||
|
|
7582cbef9c | ||
|
|
b395cde49c | ||
|
|
9f46234f71 | ||
|
|
f4c4edeb29 | ||
|
|
7166c32384 | ||
|
|
ed126889ae | ||
|
|
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 | ||
|
|
4b89c84692 | ||
|
|
df68449b82 | ||
|
|
48e3383f66 | ||
|
|
e750fa7393 | ||
|
|
5a15199a3a | ||
|
|
1801472fc4 |
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 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 💐!
|
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
|||||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
<!-- List the changes done to fix a bug or introduce a new feature.Please note both user-facing changes and changes to internal API's here -->
|
<!-- List the technical changes done to fix a bug or introduce a new feature. -->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
|
- [ ] I have checked `make build` works locally.
|
||||||
|
- [ ] I have created / updated documentation for this change (if applicable).
|
||||||
- [ ] All tests pass locally
|
|
||||||
- [ ] I have updated `CHANGELOG.md`
|
|
||||||
- [ ] I have created documentation for this(if applicable)
|
|
||||||
|
|||||||
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
|
||||||
31
.github/workflows/prepare-release.yml
vendored
31
.github/workflows/prepare-release.yml
vendored
@@ -7,22 +7,37 @@ on:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-release:
|
prepare-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -35,20 +50,20 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: NPM Install
|
- name: NPM Install
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Generate index.html
|
- name: Generate index.html
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#./#' ./public/index.html > ./pyscript.core/dist/index.html
|
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
- name: Zip dist folder
|
- name: Zip dist folder
|
||||||
run: zip -r -q ./build.zip ./dist
|
run: zip -r -q ./build.zip ./dist
|
||||||
|
|
||||||
- name: Prepare Release
|
- name: Prepare Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
51
.github/workflows/publish-release.yml
vendored
51
.github/workflows/publish-release.yml
vendored
@@ -6,25 +6,40 @@ on:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-release:
|
publish-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: read
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -37,23 +52,37 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: build offline
|
||||||
|
run: npm run build:offline
|
||||||
|
|
||||||
|
- name: Rename offline.zip with version metadata
|
||||||
|
run: mv ./dist/offline.zip ./dist/offline_${{ github.ref_name }}.zip
|
||||||
|
|
||||||
- name: Generate index.html in snapshot
|
- name: Generate index.html in snapshot
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
run: sed -e 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#g' -e 's#_DOC_VERSION_#${{ github.ref_name }}#g' -e 's#_TAG_VERSION_#/tag/${{ github.ref_name }}#g' -e 's#_VERSION_#${{ github.ref_name }}#g' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
|
- name: Generate release.tar from snapshot and put it in dist/
|
||||||
|
working-directory: .
|
||||||
|
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||||
|
|
||||||
|
- name: Upload offline.zip to release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: gh release upload ${{ github.ref_name }} ./dist/offline_${{ github.ref_name }}.zip
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
- name: Sync to S3
|
- name: Sync to S3
|
||||||
run:
|
run:
|
||||||
| # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
|
| # Create an explicitly versioned directory under releases/YYYY.MM.MICRO/
|
||||||
aws s3 sync --quiet ./dist/ s3://pyscript.net/latest/
|
|
||||||
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/
|
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/
|
||||||
|
|||||||
31
.github/workflows/publish-snapshot.yml
vendored
31
.github/workflows/publish-snapshot.yml
vendored
@@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-snapshot:
|
publish-snapshot:
|
||||||
@@ -20,15 +20,30 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -41,20 +56,20 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Build Pyscript.core
|
- name: Build Pyscript.core
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
- name: Generate index.html in snapshot
|
- name: Generate index.html in snapshot
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
run: sed -e 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' -e 's#_DOC_VERSION_#${{ inputs.snapshot_version }}#' -e 's#_TAG_VERSION_#/tag/${{ inputs.snapshot_version }}#' -e 's#_VERSION_#${{ inputs.snapshot_version }}#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
- name: Copy to Snapshot
|
- name: Copy to Snapshot
|
||||||
run: >
|
run: >
|
||||||
|
|||||||
35
.github/workflows/publish-unstable.yml
vendored
35
.github/workflows/publish-unstable.yml
vendored
@@ -1,11 +1,11 @@
|
|||||||
name: "Publish Unstable"
|
name: "Publish Unstable"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push: # Only run on merges into main that modify files under pyscript.core/ and examples/
|
push: # Only run on merges into main that modify files under core/ and examples/
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- pyscript.core/**
|
- core/**
|
||||||
- examples/**
|
- examples/**
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -18,18 +18,33 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./pyscript.core
|
working-directory: ./core
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -42,17 +57,17 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: NPM Install
|
- name: NPM Install
|
||||||
run: npm install && npx playwright install
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Generate index.html in snapshot
|
- name: Generate index.html in snapshot
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/unstable/#' ./public/index.html > ./pyscript.core/dist/index.html
|
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|||||||
32
.github/workflows/test.yml
vendored
32
.github/workflows/test.yml
vendored
@@ -5,26 +5,26 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- pyscript.core/**
|
- core/**
|
||||||
- .github/workflows/test.yml
|
- .github/workflows/test.yml
|
||||||
|
|
||||||
pull_request: # Only run on merges into main that modify certain files
|
pull_request: # Only run on merges into main that modify certain files
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- pyscript.core/**
|
- core/**
|
||||||
- .github/workflows/test.yml
|
- .github/workflows/test.yml
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
BuildAndTest:
|
BuildAndTest:
|
||||||
runs-on: ubuntu-latest-8core
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
MINICONDA_PYTHON_VERSION: py38
|
MINICONDA_PYTHON_VERSION: py38
|
||||||
MINICONDA_VERSION: 4.11.0
|
MINICONDA_VERSION: 4.11.0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 3
|
fetch-depth: 3
|
||||||
|
|
||||||
@@ -37,12 +37,12 @@ jobs:
|
|||||||
run: git log --graph -3
|
run: git log --graph -3
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: setup Miniconda
|
- name: setup Miniconda
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
uses: conda-incubator/setup-miniconda@v3
|
||||||
|
|
||||||
- name: Create and activate virtual environment
|
- name: Create and activate virtual environment
|
||||||
run: |
|
run: |
|
||||||
@@ -69,24 +69,12 @@ jobs:
|
|||||||
make setup
|
make setup
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build
|
run: make build # Integration tests run in the build step.
|
||||||
|
|
||||||
- name: Integration Tests
|
- uses: actions/upload-artifact@v4
|
||||||
#run: make test-integration-parallel
|
|
||||||
run: |
|
|
||||||
make test-integration
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
name: pyscript
|
name: pyscript
|
||||||
path: |
|
path: |
|
||||||
pyscript.core/dist/
|
core/dist/
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- uses: actions/upload-artifact@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
|
test_results
|
||||||
|
|
||||||
# @pyscript/core npm artifacts
|
# @pyscript/core npm artifacts
|
||||||
pyscript.core/core.*
|
core/test-results/*
|
||||||
pyscript.core/dist
|
core/core.*
|
||||||
pyscript.core/dist.zip
|
core/dist
|
||||||
pyscript.core/src/plugins.js
|
core/dist.zip
|
||||||
pyscript.core/src/stdlib/pyscript.js
|
core/src/plugins.js
|
||||||
pyscript.core/src/3rd-party/*
|
core/src/stdlib/pyscript.js
|
||||||
!pyscript.core/src/3rd-party/READMEmd
|
core/src/3rd-party/*
|
||||||
|
!core/src/3rd-party/READMEmd
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ ci:
|
|||||||
#skip: [eslint]
|
#skip: [eslint]
|
||||||
autoupdate_schedule: monthly
|
autoupdate_schedule: monthly
|
||||||
|
|
||||||
default_stages: [commit]
|
default_stages: [pre-commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-docstring-first
|
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
- id: check-json
|
- id: check-json
|
||||||
exclude: tsconfig\.json
|
exclude: tsconfig\.json
|
||||||
@@ -21,33 +20,33 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: pyscript\.core/dist|\.min\.js$
|
exclude: core/dist|\.min\.js$
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.1.0
|
rev: 25.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
exclude: core/tests
|
||||||
|
args: ["-l", "88", "--skip-string-normalization"]
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.4
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'pyproject.toml' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
exclude: \.js\.map$
|
exclude: fs\.py|\.js\.map$
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- tomli
|
- tomli
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.13.3
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
exclude: core/tests
|
||||||
|
|
||||||
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
||||||
rev: "v3.0.0-alpha.6"
|
rev: "v3.0.0-alpha.6"
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
exclude: pyscript\.core/test|pyscript\.core/dist|pyscript\.core/types|pyscript.core/src/stdlib/pyscript.js|pyscript\.sw/|pyscript.core/src/3rd-party
|
exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
|
||||||
args: [--tab-width, "4"]
|
args: [--tab-width, "4"]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: 5.12.0
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
name: isort (python)
|
|
||||||
args: [--profile, black]
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
ISSUE_TEMPLATE
|
ISSUE_TEMPLATE
|
||||||
*.min.*
|
*.min.*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
docs
|
bridge/
|
||||||
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
|
# Contributing to PyScript
|
||||||
|
|
||||||
Thank you for wanting to contribute to the PyScript project!
|
Please see our guide to contributing to PyScript
|
||||||
|
[in our documentation](https://docs.pyscript.net/latest/contributing/).
|
||||||
## Table of contents
|
|
||||||
|
|
||||||
- [Contributing to PyScript](#contributing-to-pyscript)
|
|
||||||
- [Table of contents](#table-of-contents)
|
|
||||||
- [Code of Conduct](#code-of-conduct)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [Reporting bugs](#reporting-bugs)
|
|
||||||
- [Creating useful issues](#creating-useful-issues)
|
|
||||||
- [Reporting security issues](#reporting-security-issues)
|
|
||||||
- [Asking questions](#asking-questions)
|
|
||||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
|
||||||
- [Developing](#developing)
|
|
||||||
- [Rebasing changes](#rebasing-changes)
|
|
||||||
- [Building the docs](#building-the-docs)
|
|
||||||
- [Places to start](#places-to-start)
|
|
||||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
|
||||||
- [Submitting a change](#submitting-a-change)
|
|
||||||
- [License terms for contributions](#license-terms-for-contributions)
|
|
||||||
- [Becoming a maintainer](#becoming-a-maintainer)
|
|
||||||
- [Trademarks](#trademarks)
|
|
||||||
|
|
||||||
# Code of Conduct
|
|
||||||
|
|
||||||
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
## Reporting bugs
|
|
||||||
|
|
||||||
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
|
|
||||||
|
|
||||||
## Creating useful issues
|
|
||||||
|
|
||||||
- Use a clear and descriptive title.
|
|
||||||
- Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
|
|
||||||
- Describe the behavior you observed, and the behavior you had expected.
|
|
||||||
- Include screenshots if they help make the issue clear.
|
|
||||||
|
|
||||||
## Reporting security issues
|
|
||||||
|
|
||||||
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
|
|
||||||
|
|
||||||
## Asking questions
|
|
||||||
|
|
||||||
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
|
|
||||||
|
|
||||||
## Places to start
|
|
||||||
|
|
||||||
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions:
|
|
||||||
|
|
||||||
- **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
|
|
||||||
- **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
|
|
||||||
- **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
|
|
||||||
- **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
|
|
||||||
|
|
||||||
## Setting up your local environment and developing
|
|
||||||
|
|
||||||
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/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/).
|
|
||||||
|
|||||||
6
LICENSE
6
LICENSE
@@ -186,7 +186,11 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
33
Makefile
33
Makefile
@@ -9,10 +9,11 @@ PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
|
|||||||
all:
|
all:
|
||||||
@echo "\nThere is no default Makefile target right now. Try:\n"
|
@echo "\nThere is no default Makefile target right now. Try:\n"
|
||||||
@echo "make setup - check your environment and install the dependencies."
|
@echo "make setup - check your environment and install the dependencies."
|
||||||
|
@echo "make update - update dependencies."
|
||||||
@echo "make clean - clean up auto-generated assets."
|
@echo "make clean - clean up auto-generated assets."
|
||||||
@echo "make build - build PyScript."
|
@echo "make build - build PyScript."
|
||||||
@echo "make precommit-check - run the precommit checks (run eslint)."
|
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||||
@echo "make test-integration - run all integration tests sequentially."
|
@echo "make test - run all automated tests in playwright."
|
||||||
@echo "make fmt - format the code."
|
@echo "make fmt - format the code."
|
||||||
@echo "make fmt-check - check the code formatting.\n"
|
@echo "make fmt-check - check the code formatting.\n"
|
||||||
|
|
||||||
@@ -39,13 +40,12 @@ check-python:
|
|||||||
|
|
||||||
# Check the environment, install the dependencies.
|
# Check the environment, install the dependencies.
|
||||||
setup: check-node check-npm check-python
|
setup: check-node check-npm check-python
|
||||||
cd pyscript.core && npm install && cd ..
|
cd core && npm ci && cd ..
|
||||||
ifeq ($(VIRTUAL_ENV),)
|
ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
|
||||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
python -m pip install -r requirements.txt
|
python -m pip install -r requirements.txt
|
||||||
playwright install
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Clean up generated assets.
|
# Clean up generated assets.
|
||||||
@@ -55,22 +55,24 @@ clean:
|
|||||||
rm -rf .pytest_cache .coverage coverage.xml
|
rm -rf .pytest_cache .coverage coverage.xml
|
||||||
|
|
||||||
# Build PyScript.
|
# Build PyScript.
|
||||||
build:
|
build: precommit-check
|
||||||
cd pyscript.core && npx playwright install && npm run build
|
cd core && npx playwright install chromium && npm run build
|
||||||
|
|
||||||
|
# Update the dependencies.
|
||||||
|
update:
|
||||||
|
python -m pip install -r requirements.txt --upgrade
|
||||||
|
|
||||||
# Run the precommit checks (run eslint).
|
# Run the precommit checks (run eslint).
|
||||||
precommit-check:
|
precommit-check:
|
||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
|
|
||||||
# Run all integration tests sequentially.
|
# Run all automated tests in playwright.
|
||||||
test-integration:
|
test:
|
||||||
mkdir -p test_results
|
cd core && npm run test:integration
|
||||||
pytest -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
|
||||||
|
|
||||||
# Run all integration tests in parallel.
|
# Serve the repository with the correct headers.
|
||||||
test-integration-parallel:
|
serve:
|
||||||
mkdir -p test_results
|
npx mini-coi .
|
||||||
pytest --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
|
||||||
|
|
||||||
# Format the code.
|
# Format the code.
|
||||||
fmt: fmt-py
|
fmt: fmt-py
|
||||||
@@ -83,7 +85,6 @@ fmt-check: fmt-py-check
|
|||||||
# Format Python code.
|
# Format Python code.
|
||||||
fmt-py:
|
fmt-py:
|
||||||
black -l 88 --skip-string-normalization .
|
black -l 88 --skip-string-normalization .
|
||||||
isort --profile black .
|
|
||||||
|
|
||||||
# Check the format of Python code.
|
# Check the format of Python code.
|
||||||
fmt-py-check:
|
fmt-py-check:
|
||||||
|
|||||||
106
README.md
106
README.md
@@ -1,62 +1,94 @@
|
|||||||
# PyScript
|
# PyScript
|
||||||
|
|
||||||
## What is PyScript
|
## PyScript is an open source platform for Python in the browser.
|
||||||
|
|
||||||
### Summary
|
Using PyScript is as simple as:
|
||||||
|
|
||||||
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [WASM](https://webassembly.org/), and modern web technologies.
|
|
||||||
|
|
||||||
To get started see the [getting started tutorial](docs/tutorials/getting-started.md).
|
|
||||||
|
|
||||||
For examples see [here](examples).
|
|
||||||
|
|
||||||
### Longer Version
|
|
||||||
|
|
||||||
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
|
|
||||||
|
|
||||||
## Try PyScript
|
|
||||||
|
|
||||||
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page with:
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<head>
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>PyScript!</title>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://pyscript.net/releases/2023.11.1/core.css"
|
href="https://pyscript.net/releases/2025.11.2/core.css"
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="module"
|
type="module"
|
||||||
src="https://pyscript.net/releases/2023.11.1/core.js"
|
src="https://pyscript.net/releases/2025.11.2/core.js"
|
||||||
></script>
|
></script>
|
||||||
</head>
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- type mpy (MicroPython) or py (Pyodide) to run some Python -->
|
||||||
|
<script type="mpy" terminal>
|
||||||
|
print("Hello, world!")
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then use PyScript components in your html page. PyScript currently implements the following elements:
|
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.
|
||||||
|
|
||||||
- `<py-script>`: can be used to define python code that is executable within the web page. The element itself is not rendered to the page and is only used to add logic
|
To start building, read the
|
||||||
- `<py-repl>`: creates a REPL component that is rendered to the page as a code editor and allows users to write executable code
|
[Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||||
|
|
||||||
Check out the [the examples directory](examples) folder for more examples on how to use it, all you need to do is open them in Chrome.
|
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.
|
||||||
|
|
||||||
## Resources
|
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.
|
||||||
|
|
||||||
- [Official docs](https://docs.pyscript.net)
|
For more details on how to join the calls and up to date schedule, consult the
|
||||||
- [Discussion board](https://community.anaconda.cloud/c/tech-topics/pyscript)
|
official calendar:
|
||||||
- [Home Page](https://pyscript.net/)
|
|
||||||
- [Blog Post](https://engineering.anaconda.com/2022/04/welcome-pyscript.html)
|
|
||||||
- [Discord Channel](https://discord.gg/BYB2kvyFwm)
|
|
||||||
|
|
||||||
## Notes
|
- [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).
|
||||||
|
|
||||||
- This is an extremely experimental project, so expect things to break!
|
## Contribute
|
||||||
- PyScript has been only tested on Chrome at the moment.
|
|
||||||
|
For technical details of the code, please see the [README](core/README.md) in
|
||||||
|
the `core` directory.
|
||||||
|
|
||||||
|
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
||||||
|
to learn about our development process, reporting bugs and improvements,
|
||||||
|
creating issues and asking questions.
|
||||||
|
|
||||||
|
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||||
|
documentation for more information on how to setup your development environment.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|
||||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
The [PyScript organization governance](https://github.com/pyscript/governance)
|
||||||
|
is documented in a separate repository.
|
||||||
|
|
||||||
|
## Supporters
|
||||||
|
|
||||||
|
PyScript is an independent open source project.
|
||||||
|
|
||||||
|
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
|
||||||
|
core contributors are currently employed by Anaconda to work on PyScript. We
|
||||||
|
would like to acknowledge and celebrate Anaconda's continued support of this
|
||||||
|
project. Thank you [Anaconda Inc](https://anaconda.com/)!
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
|
||||||
59
bridge/README.md
Normal file
59
bridge/README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# @pyscript/bridge
|
||||||
|
|
||||||
|
Import Python utilities directly in JS
|
||||||
|
|
||||||
|
```js
|
||||||
|
// main thread
|
||||||
|
const { ffi: { func_a, func_b } } = await import('./test.js');
|
||||||
|
|
||||||
|
// test.js
|
||||||
|
import bridge from 'https://esm.run/@pyscript/bridge';
|
||||||
|
export const ffi = bridge(import.meta.url, { type: 'mpy', worker: false });
|
||||||
|
|
||||||
|
// test.py
|
||||||
|
def func_a(value):
|
||||||
|
print(f"hello {value}")
|
||||||
|
|
||||||
|
def func_b():
|
||||||
|
import sys
|
||||||
|
return sys.version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
* **pyscript**: the release version to automatically import if not already available on the page. If no version is provided the *developers' channel* version will be used instead (for developers' purposes only).
|
||||||
|
* **type**: `py` by default to bootstrap *Pyodide*.
|
||||||
|
* **worker**: `true` by default to bootstrap in a *Web Worker*.
|
||||||
|
* **config**: either a *string* or a PyScript compatible config *JS literal* to make it possible to bootstrap files and whatnot. If specified, the `worker` becomes implicitly `true` to avoid multiple configs conflicting on the main thread.
|
||||||
|
* **env**: to share the same environment across multiple modules loaded at different times.
|
||||||
|
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Run `npx mini-coi .` within this folder to then reach out `http://localhost:8080/test/` that will show:
|
||||||
|
|
||||||
|
```
|
||||||
|
PyScript Bridge
|
||||||
|
------------------
|
||||||
|
no config
|
||||||
|
```
|
||||||
|
|
||||||
|
The [test.js](./test/test.js) files uses the following defaults:
|
||||||
|
|
||||||
|
* `pyscript` as `"2025.8.1"`
|
||||||
|
* `type` as `"mpy"`
|
||||||
|
* `worker` as `false`
|
||||||
|
* `config` as `undefined`
|
||||||
|
* `env` as `undefined`
|
||||||
|
|
||||||
|
To test any variant use query string parameters so that `?type=py` will use `py` instead, `worker` will use a worker and `config` will use a basic *config* that brings in another file from the same folder which exposes the version.
|
||||||
|
|
||||||
|
To recap: `http://localhost:8080/test/?type=py&worker&config` will show this instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
PyScript Bridge
|
||||||
|
------------------
|
||||||
|
3.12.7 (main, May 15 2025, 18:47:24) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note when a *config* is used, the `worker` attribute is always `true`.
|
||||||
163
bridge/index.js
Normal file
163
bridge/index.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/*! (c) PyScript Development Team */
|
||||||
|
|
||||||
|
const { stringify } = JSON;
|
||||||
|
const { assign, create, entries } = Object;
|
||||||
|
|
||||||
|
const el = (name, props) => assign(document.createElement(name), props);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a list of keys into a Python dictionary.
|
||||||
|
* `['a', 'b']` => `{ "a": a, "b": b }`
|
||||||
|
* @param {Iterable<string>} keys
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const dictionary = keys => {
|
||||||
|
const fields = [];
|
||||||
|
for (const key of keys)
|
||||||
|
fields.push(`${stringify(key)}: ${key}`);
|
||||||
|
return `{ ${fields.join(',')} }`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve properly config files relative URLs.
|
||||||
|
* @param {string|Object} config - The configuration to normalize.
|
||||||
|
* @param {string} base - The base URL to resolve relative URLs against.
|
||||||
|
* @returns {string} - The JSON serialized config.
|
||||||
|
*/
|
||||||
|
const normalize = async (config, base) => {
|
||||||
|
if (typeof config === 'string') {
|
||||||
|
base = config;
|
||||||
|
config = await fetch(config).then(res => res.json());
|
||||||
|
}
|
||||||
|
if (typeof config.files === 'object') {
|
||||||
|
const files = {};
|
||||||
|
for (const [key, value] of entries(config.files)) {
|
||||||
|
files[key.startsWith('{') ? key : new URL(key, base)] = value;
|
||||||
|
}
|
||||||
|
config.files = files;
|
||||||
|
}
|
||||||
|
return stringify(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
// this logic is based on a 3 levels cache ...
|
||||||
|
const cache = new Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a bridge to a Python module via a `.js` file that has a `.py` alter ego.
|
||||||
|
* @param {string} url - The URL of the JS module that has a Python counterpart.
|
||||||
|
* @param {Object} options - The options for the bridge.
|
||||||
|
* @param {string} [options.type='py'] - The `py` or `mpy` interpreter type, `py` by default.
|
||||||
|
* @param {boolean} [options.worker=true] - Whether to use a worker, `true` by default.
|
||||||
|
* @param {string|Object} [options.config=null] - The configuration for the bridge, `null` by default.
|
||||||
|
* @param {string} [options.env=null] - The optional shared environment to use.
|
||||||
|
* @param {string} [options.serviceWorker=null] - The optional service worker to use as fallback.
|
||||||
|
* @returns {Object} - The bridge to the Python module.
|
||||||
|
*/
|
||||||
|
export default (url, {
|
||||||
|
type = 'py',
|
||||||
|
worker = true,
|
||||||
|
config = null,
|
||||||
|
env = null,
|
||||||
|
serviceWorker = null,
|
||||||
|
pyscript = null,
|
||||||
|
} = {}) => {
|
||||||
|
const { protocol, host, pathname } = new URL(url);
|
||||||
|
const py = pathname.replace(/\.m?js(?:\/\+\w+)?$/, '.py');
|
||||||
|
const file = `${protocol}//${host}${py}`;
|
||||||
|
|
||||||
|
// the first cache is about the desired file in the wild ...
|
||||||
|
if (!cache.has(file)) {
|
||||||
|
// the second cache is about all fields one needs to access out there
|
||||||
|
const exports = new Map;
|
||||||
|
let python;
|
||||||
|
|
||||||
|
cache.set(file, new Proxy(create(null), {
|
||||||
|
get(_, field) {
|
||||||
|
if (!exports.has(field)) {
|
||||||
|
// create an async callback once and always return the same later on
|
||||||
|
exports.set(field, async (...args) => {
|
||||||
|
// the third cache is about reaching lazily the code only once
|
||||||
|
// augmenting its content with exports once and drop it on done
|
||||||
|
if (!python) {
|
||||||
|
// do not await or multiple calls will fetch multiple times
|
||||||
|
// just assign the fetch `Promise` once and return it
|
||||||
|
python = fetch(file).then(async response => {
|
||||||
|
const code = await response.text();
|
||||||
|
// create a unique identifier for the Python context
|
||||||
|
const identifier = pathname.replace(/[^a-zA-Z0-9_]/g, '');
|
||||||
|
const name = `__pyscript_${identifier}${Date.now()}`;
|
||||||
|
// create a Python dictionary with all accessed fields
|
||||||
|
const detail = `{"detail":${dictionary(exports.keys())}}`;
|
||||||
|
// create the arguments for the `dispatchEvent` call
|
||||||
|
const eventArgs = `${stringify(name)},${name}to_ts(${detail})`;
|
||||||
|
// bootstrap the script element type and its attributes
|
||||||
|
const script = el('script', { type, textContent: [
|
||||||
|
'\n', code, '\n',
|
||||||
|
// this is to avoid local scope name clashing
|
||||||
|
`from pyscript import window as ${name}`,
|
||||||
|
`from pyscript.ffi import to_js as ${name}to_ts`,
|
||||||
|
`${name}.dispatchEvent(${name}.CustomEvent.new(${eventArgs}))`,
|
||||||
|
// remove these references even if non-clashing to keep
|
||||||
|
// the local scope clean from undesired entries
|
||||||
|
`del ${name}`,
|
||||||
|
`del ${name}to_ts`,
|
||||||
|
].join('\n') });
|
||||||
|
|
||||||
|
// if config is provided it needs to be a worker to avoid
|
||||||
|
// conflicting with main config on the main thread (just like always)
|
||||||
|
script.toggleAttribute('worker', !!config || !!worker);
|
||||||
|
if (config) {
|
||||||
|
const attribute = await normalize(config, file);
|
||||||
|
script.setAttribute('config', attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env) script.setAttribute('env', env);
|
||||||
|
if (serviceWorker) script.setAttribute('service-worker', serviceWorker);
|
||||||
|
|
||||||
|
// let PyScript resolve and execute this script
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
// intercept once the unique event identifier with all exports
|
||||||
|
globalThis.addEventListener(
|
||||||
|
name,
|
||||||
|
event => {
|
||||||
|
resolve(event.detail);
|
||||||
|
script.remove();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// return a promise that will resolve only once the event
|
||||||
|
// has been emitted and the interpreter evaluated the code
|
||||||
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
|
|
||||||
|
if (!(Symbol.for('@pyscript/core') in globalThis)) {
|
||||||
|
// bring in PyScript if not available already
|
||||||
|
const cdn = pyscript ?
|
||||||
|
`https://pyscript.net/releases/${pyscript}` :
|
||||||
|
// ⚠️ fallback to developers' channel !!!
|
||||||
|
'https://cdn.jsdelivr.net/npm/@pyscript/core/dist'
|
||||||
|
;
|
||||||
|
document.head.appendChild(
|
||||||
|
el('link', { rel: 'stylesheet', href: `${cdn}/core.css` }),
|
||||||
|
);
|
||||||
|
try { await import(`${cdn}/core.js`) }
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the `Promise` that will after invoke the exported field
|
||||||
|
return python.then(foreign => foreign[field](...args));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the lazily to be resolved once callback to invoke
|
||||||
|
return exports.get(field);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.get(file);
|
||||||
|
};
|
||||||
31
bridge/package.json
Normal file
31
bridge/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@pyscript/bridge",
|
||||||
|
"version": "0.2.2",
|
||||||
|
"description": "A JS based way to use PyScript modules",
|
||||||
|
"type": "module",
|
||||||
|
"module": "./index.js",
|
||||||
|
"unpkg": "./index.js",
|
||||||
|
"jsdelivr": "./jsdelivr.js",
|
||||||
|
"browser": "./index.js",
|
||||||
|
"main": "./index.js",
|
||||||
|
"keywords": [
|
||||||
|
"PyScript",
|
||||||
|
"JS",
|
||||||
|
"Python",
|
||||||
|
"bridge"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"author": "Anaconda Inc.",
|
||||||
|
"license": "APACHE-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/pyscript/pyscript/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||||
|
}
|
||||||
31
bridge/test/index.html
Normal file
31
bridge/test/index.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>PyScript Bridge</title>
|
||||||
|
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
|
||||||
|
<!-- for local testing purpose only-->
|
||||||
|
<script type="importmap">{"imports":{"https://esm.run/@pyscript/bridge":"../index.js"}}</script>
|
||||||
|
<script type="module">
|
||||||
|
const { ffi: { test_func, test_other, version } } = await import('./test.js');
|
||||||
|
|
||||||
|
console.time("⏱️ first invoke");
|
||||||
|
const result = await test_func("PyScript Bridge");
|
||||||
|
console.timeEnd("⏱️ first invoke");
|
||||||
|
|
||||||
|
document.body.append(
|
||||||
|
Object.assign(
|
||||||
|
document.createElement("h3"),
|
||||||
|
{ textContent: result },
|
||||||
|
),
|
||||||
|
document.createElement("hr"),
|
||||||
|
await version(),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.time("⏱️ other invokes");
|
||||||
|
await test_other("🐍");
|
||||||
|
console.timeEnd("⏱️ other invokes");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
40
bridge/test/remote/index.html
Normal file
40
bridge/test/remote/index.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>PyScript Bridge</title>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"https://esm.run/@pyscript/bridge": "https://esm.run/@pyscript/bridge@latest",
|
||||||
|
"https://esm.run/@pyscript/bridge/test/test.js": "https://esm.run/@pyscript/bridge@latest/test/test.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
|
||||||
|
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
|
||||||
|
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
const cdn_test = 'https://esm.run/@pyscript/bridge/test/test.js';
|
||||||
|
const { ffi: { test_func, test_other, version } } = await import(cdn_test);
|
||||||
|
|
||||||
|
console.time("⏱️ first invoke");
|
||||||
|
const result = await test_func("PyScript Bridge");
|
||||||
|
console.timeEnd("⏱️ first invoke");
|
||||||
|
|
||||||
|
document.body.append(
|
||||||
|
Object.assign(
|
||||||
|
document.createElement("h3"),
|
||||||
|
{ textContent: result },
|
||||||
|
),
|
||||||
|
document.createElement("hr"),
|
||||||
|
await version(),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.time("⏱️ other invokes");
|
||||||
|
await test_other("🐍");
|
||||||
|
console.timeEnd("⏱️ other invokes");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
5
bridge/test/sys_version.py
Normal file
5
bridge/test/sys_version.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
return sys.version
|
||||||
18
bridge/test/test.js
Normal file
18
bridge/test/test.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import bridge from "https://esm.run/@pyscript/bridge";
|
||||||
|
|
||||||
|
// for local testing purpose only
|
||||||
|
const { searchParams } = new URL(location.href);
|
||||||
|
|
||||||
|
// the named (or default) export for test.py
|
||||||
|
export const ffi = bridge(import.meta.url, {
|
||||||
|
pyscript: "2025.8.1",
|
||||||
|
env: searchParams.get("env"),
|
||||||
|
type: searchParams.get("type") || "mpy",
|
||||||
|
worker: searchParams.has("worker"),
|
||||||
|
config: searchParams.has("config") ?
|
||||||
|
({
|
||||||
|
files: {
|
||||||
|
"./sys_version.py": "./sys_version.py",
|
||||||
|
},
|
||||||
|
}) : undefined,
|
||||||
|
});
|
||||||
22
bridge/test/test.py
Normal file
22
bridge/test/test.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from pyscript import config, RUNNING_IN_WORKER
|
||||||
|
|
||||||
|
type = config["type"]
|
||||||
|
print(f"{type}-script", RUNNING_IN_WORKER and "worker" or "main")
|
||||||
|
|
||||||
|
|
||||||
|
def test_func(message):
|
||||||
|
print("Python", message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def test_other(message):
|
||||||
|
print("Python", message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
try:
|
||||||
|
from sys_version import version
|
||||||
|
except ImportError:
|
||||||
|
version = lambda: "no config"
|
||||||
|
return version()
|
||||||
@@ -186,10 +186,12 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
|
|
||||||
Copyright (c) 2022-present, PyScript Development Team
|
Copyright (c) 2022-present, PyScript Development Team
|
||||||
|
|
||||||
Originated at Anaconda, Inc. in 2022
|
Originated at Anaconda, Inc. in 2022
|
||||||
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
2500
pyscript.core/package-lock.json → core/package-lock.json
generated
2500
pyscript.core/package-lock.json → core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
115
core/package.json
Normal file
115
core/package.json
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"name": "@pyscript/core",
|
||||||
|
"version": "0.7.11",
|
||||||
|
"type": "module",
|
||||||
|
"description": "PyScript",
|
||||||
|
"module": "./index.js",
|
||||||
|
"unpkg": "./index.js",
|
||||||
|
"jsdelivr": "./jsdelivr.js",
|
||||||
|
"browser": "./index.js",
|
||||||
|
"main": "./index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./dist/",
|
||||||
|
"./src/",
|
||||||
|
"./types/",
|
||||||
|
"./index.js",
|
||||||
|
"./jsdelivr.js",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./types/core.d.ts",
|
||||||
|
"import": "./src/core.js"
|
||||||
|
},
|
||||||
|
"./js": {
|
||||||
|
"types": "./types/core.d.ts",
|
||||||
|
"import": "./dist/core.js"
|
||||||
|
},
|
||||||
|
"./css": {
|
||||||
|
"import": "./dist/core.css"
|
||||||
|
},
|
||||||
|
"./storage": {
|
||||||
|
"import": "./dist/storage.js"
|
||||||
|
},
|
||||||
|
"./service-worker": {
|
||||||
|
"import": "./dist/service-worker.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
|
||||||
|
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run test:integration; fi",
|
||||||
|
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
|
"build:flatted": "node rollup/flatted.cjs",
|
||||||
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
|
"build:stdlib": "node rollup/stdlib.cjs",
|
||||||
|
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||||
|
"build:offline": "node rollup/offline.cjs | bash",
|
||||||
|
"build:tests-index": "node rollup/build_test_index.cjs",
|
||||||
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
|
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
|
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
|
||||||
|
"dev": "node dev.cjs",
|
||||||
|
"release": "npm run build && npm run zip",
|
||||||
|
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||||
|
"ts": "rm -rf types && tsc -p .",
|
||||||
|
"zip": "zip -r dist.zip ./dist"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"pyscript",
|
||||||
|
"core"
|
||||||
|
],
|
||||||
|
"author": "Anaconda Inc.",
|
||||||
|
"license": "APACHE-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
|
"@webreflection/idb-map": "^0.3.2",
|
||||||
|
"@webreflection/utils": "^0.1.1",
|
||||||
|
"add-promise-listener": "^0.1.3",
|
||||||
|
"basic-devtools": "^0.1.6",
|
||||||
|
"polyscript": "^0.20.0",
|
||||||
|
"sticky-module": "^0.1.1",
|
||||||
|
"to-json-callback": "^0.1.1",
|
||||||
|
"type-checked-collections": "^0.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@codemirror/commands": "^6.10.0",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/language": "^6.11.3",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/view": "^6.38.8",
|
||||||
|
"@playwright/test": "^1.56.1",
|
||||||
|
"@rollup/plugin-commonjs": "^29.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@webreflection/toml-j0.4": "^1.1.4",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"bun": "^1.3.3",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"codedent": "^0.1.2",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"flatted": "^3.3.3",
|
||||||
|
"rollup": "^4.53.3",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
|
"static-handler": "^0.5.3",
|
||||||
|
"string-width": "^8.1.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"xterm-readline": "^1.1.2"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/pyscript/pyscript/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||||
|
}
|
||||||
48
core/pyodide.sh
Executable file
48
core/pyodide.sh
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script assumes the following folder structure:
|
||||||
|
# ./pyscript - it must be a GitHub clone/fork
|
||||||
|
# ./polyscript - it must be a GitHub clone/fork
|
||||||
|
#
|
||||||
|
# Running from ./pyscript/core via:
|
||||||
|
#
|
||||||
|
# cd ./pyscript/core
|
||||||
|
# bash ./pyodide.sh
|
||||||
|
#
|
||||||
|
# will print a JSON compatible string like:
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# "2024.10.1": "0.26.2",
|
||||||
|
# ...
|
||||||
|
# "2025.11.1": "0.29.0",
|
||||||
|
# "": null
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Each key represents the PyScript release and each
|
||||||
|
# value represents the Pyodide version used by that PyScript release.
|
||||||
|
#
|
||||||
|
# The last empty key with `null` value is used just to close the JSON object.
|
||||||
|
# One could remove manually that entry as long as there are no dangling commas.
|
||||||
|
#
|
||||||
|
|
||||||
|
current_pyscript=$(git branch | grep \\* | cut -d ' ' -f2)
|
||||||
|
|
||||||
|
echo "{"
|
||||||
|
for release in $(git tag --list --sort=version:refname); do
|
||||||
|
git checkout ${release} > /dev/null 2>&1
|
||||||
|
if test -e "package.json"; then
|
||||||
|
polyscript=$(cat package.json | jq -r '.dependencies.polyscript')
|
||||||
|
tag="v${polyscript:1:${#polyscript}-1}"
|
||||||
|
cd ../../polyscript > /dev/null 2>&1
|
||||||
|
current_polyscript=$(git branch | grep \\* | cut -d ' ' -f2)
|
||||||
|
git checkout ${tag} > /dev/null 2>&1
|
||||||
|
if test -e "versions/pyodide"; then
|
||||||
|
echo " \"${release}\": \"$(cat versions/pyodide)\","
|
||||||
|
fi
|
||||||
|
git checkout ${current_polyscript} > /dev/null 2>&1
|
||||||
|
cd - > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
git checkout ${current_pyscript} > /dev/null 2>&1
|
||||||
|
done
|
||||||
|
echo " \"\": null"
|
||||||
|
echo "}"
|
||||||
@@ -10,6 +10,9 @@ const { devDependencies } = require(join(__dirname, "..", "package.json"));
|
|||||||
|
|
||||||
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
|
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
|
||||||
|
|
||||||
|
const dropSourceMap = (str) =>
|
||||||
|
str.replace(/^\/.+? sourceMappingURL=\/.+$/m, "");
|
||||||
|
|
||||||
// Fetch a module via jsdelivr CDN `/+esm` orchestration
|
// Fetch a module via jsdelivr CDN `/+esm` orchestration
|
||||||
// then sanitize the resulting outcome to avoid importing
|
// then sanitize the resulting outcome to avoid importing
|
||||||
// anything via `/npm/...` through Rollup
|
// 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:
|
// key/value pairs as:
|
||||||
// "3rd-party/file-name.js"
|
// "3rd-party/file-name.js"
|
||||||
// string as content or
|
// string as content or
|
||||||
// Promise<string> as resolved content
|
// Promise<string> as resolved content
|
||||||
const modules = {
|
const modules = {
|
||||||
|
// toml
|
||||||
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||||
"xterm.js": resolve("xterm"),
|
|
||||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
// xterm
|
||||||
(b) => b.text(),
|
"xterm.js": resolve("@xterm/xterm"),
|
||||||
),
|
|
||||||
"xterm-readline.js": resolve("xterm-readline"),
|
"xterm-readline.js": resolve("xterm-readline"),
|
||||||
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
|
b.text(),
|
||||||
|
),
|
||||||
|
"xterm_addon-web-links.js": fetch(
|
||||||
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
|
).then((b) => b.text()),
|
||||||
|
"xterm.css": fetch(
|
||||||
|
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
||||||
|
).then((b) => b.text()),
|
||||||
|
|
||||||
|
// codemirror
|
||||||
|
"codemirror.js": reBundle("codemirror"),
|
||||||
|
"codemirror_state.js": reBundle("@codemirror/state"),
|
||||||
|
"codemirror_lang-python.js": reBundle("@codemirror/lang-python"),
|
||||||
|
"codemirror_language.js": reBundle("@codemirror/language"),
|
||||||
|
"codemirror_view.js": reBundle("@codemirror/view"),
|
||||||
|
"codemirror_commands.js": reBundle("@codemirror/commands"),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [target, source] of Object.entries(modules)) {
|
for (const [target, source] of Object.entries(modules)) {
|
||||||
if (typeof source === "string") copyFileSync(source, join(targets, target));
|
if (typeof source === "string") copyFileSync(source, join(targets, target));
|
||||||
else {
|
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,28 @@ export default [
|
|||||||
warn(warning);
|
warn(warning);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: "./src/storage.js",
|
||||||
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN
|
||||||
|
? [nodeResolve(), commonjs()]
|
||||||
|
: [nodeResolve(), commonjs(), terser()],
|
||||||
|
),
|
||||||
|
output: {
|
||||||
|
esModule: true,
|
||||||
|
dir: "./dist",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "./src/service-worker.js",
|
||||||
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN
|
||||||
|
? [nodeResolve(), commonjs()]
|
||||||
|
: [nodeResolve(), commonjs(), terser()],
|
||||||
|
),
|
||||||
|
output: {
|
||||||
|
file: "./dist/service-worker.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
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)));
|
||||||
79
core/rollup/offline.cjs
Normal file
79
core/rollup/offline.cjs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const { readFileSync, writeFileSync } = require("node:fs");
|
||||||
|
const { join, resolve } = require("node:path");
|
||||||
|
|
||||||
|
const versions = resolve(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"node_modules",
|
||||||
|
"polyscript",
|
||||||
|
"versions",
|
||||||
|
);
|
||||||
|
let pyodide = String(readFileSync(join(versions, "pyodide"), "utf8")).trim();
|
||||||
|
let micropython = String(
|
||||||
|
readFileSync(join(versions, "micropython"), "utf8"),
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(process.cwd(), "offline.html"),
|
||||||
|
`<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript Offline</title>
|
||||||
|
<script src="./mini-coi-fd.js"></script>
|
||||||
|
<script type="module" src="./pyscript/core.js" offline></script>
|
||||||
|
<link rel="stylesheet" href="./pyscript/core.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="mpy">
|
||||||
|
from pyscript import document
|
||||||
|
|
||||||
|
document.body.append("MicroPython Offline", document.createElement("hr"))
|
||||||
|
</script>
|
||||||
|
<script type="py" worker>
|
||||||
|
from pyscript import document
|
||||||
|
|
||||||
|
document.body.append("Pyodide Offline")
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
let bash = `#!/usr/bin/env bash
|
||||||
|
rm -rf dist/offline
|
||||||
|
|
||||||
|
mkdir -p dist/offline/node_modules
|
||||||
|
echo '{"dependencies":{"pyodide":"${pyodide}","@micropython/micropython-webassembly-pyscript":"${micropython}"}}' > dist/offline/package.json
|
||||||
|
cd dist/offline
|
||||||
|
curl -sLO https://raw.githubusercontent.com/WebReflection/mini-coi/refs/heads/main/mini-coi-fd.js
|
||||||
|
npm i
|
||||||
|
cd -
|
||||||
|
|
||||||
|
mkdir -p dist/offline/pyscript/pyodide
|
||||||
|
cd dist/offline/pyscript/pyodide
|
||||||
|
cp ../../node_modules/pyodide/pyodide* ./
|
||||||
|
cp ../../node_modules/pyodide/python_stdlib.zip ./
|
||||||
|
cd -
|
||||||
|
|
||||||
|
mkdir -p dist/offline/pyscript/micropython
|
||||||
|
cd dist/offline/pyscript/micropython
|
||||||
|
cp ../../node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./
|
||||||
|
cd -
|
||||||
|
|
||||||
|
rm -rf dist/offline/node_modules
|
||||||
|
rm -rf dist/offline/*.json
|
||||||
|
|
||||||
|
mv offline.html dist/offline/index.html
|
||||||
|
cp dist/*.* dist/offline/pyscript/
|
||||||
|
rm -f dist/offline/pyscript/offline.zip
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
zip -r offline.zip offline
|
||||||
|
rm -rf offline
|
||||||
|
cd -
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log(bash);
|
||||||
@@ -13,7 +13,12 @@ for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
|
|||||||
plugins.push(
|
plugins.push(
|
||||||
// this comment is needed to avoid bundlers eagerly embedding lazy
|
// this comment is needed to avoid bundlers eagerly embedding lazy
|
||||||
// dependencies, causing all sort of issues once in production
|
// dependencies, causing all sort of issues once in production
|
||||||
` ${key}: () => import(/* webpackIgnore: true */ ${value}),`,
|
// ⚠️ THIS HAS TO BE LIKE THIS or prettier changes it every single time
|
||||||
|
` ${key}: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
${value}
|
||||||
|
),`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
66
core/rollup/stdlib.cjs
Normal file
66
core/rollup/stdlib.cjs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const {
|
||||||
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} = require("node:fs");
|
||||||
|
|
||||||
|
const { spawnSync } = require("node:child_process");
|
||||||
|
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const dedent = require("codedent");
|
||||||
|
|
||||||
|
const crawl = (path, json) => {
|
||||||
|
for (const file of readdirSync(path)) {
|
||||||
|
const full = join(path, file);
|
||||||
|
if (/\.py$/.test(file)) {
|
||||||
|
if (process.env.NO_MIN) json[file] = readFileSync(full).toString();
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
output: [error, result],
|
||||||
|
} = spawnSync("pyminify", [
|
||||||
|
"--remove-literal-statements",
|
||||||
|
full,
|
||||||
|
]);
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
json[file] = result.toString();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.log(
|
||||||
|
dedent(`
|
||||||
|
\x1b[1m⚠️ is your env activated?\x1b[0m
|
||||||
|
\x1b[2mYou need a Python env to run \x1b[0mpyminify\x1b[2m.\x1b[0m
|
||||||
|
\x1b[2mTo do so, you can try the following:\x1b[0m
|
||||||
|
python -m venv env
|
||||||
|
source env/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install --ignore-requires-python python-minifier
|
||||||
|
pip install setuptools
|
||||||
|
\x1b[2mand you can then try \x1b[0mnpm run build\x1b[2m again.\x1b[0m
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (statSync(full).isDirectory() && !file.endsWith("_"))
|
||||||
|
crawl(full, (json[file] = {}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const json = {};
|
||||||
|
|
||||||
|
crawl(join(__dirname, "..", "src", "stdlib"), json);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(__dirname, "..", "src", "stdlib", "pyscript.js"),
|
||||||
|
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default ${JSON.stringify(
|
||||||
|
json,
|
||||||
|
null,
|
||||||
|
" ",
|
||||||
|
)};\n`,
|
||||||
|
);
|
||||||
21
core/src/3rd-party-licenses/codemirror.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_commands.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_commands.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_language.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_language.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_state.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_state.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_view.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_view.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/toml.license.txt
Normal file
21
core/src/3rd-party-licenses/toml.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Jak Wings
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
25
core/src/3rd-party-licenses/xterm-readline.license.txt
Normal file
25
core/src/3rd-party-licenses/xterm-readline.license.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Copyright 2021 Erik Bremen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/xterm.license.txt
Normal file
21
core/src/3rd-party-licenses/xterm.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||||
|
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
19
core/src/3rd-party-licenses/xterm_addon-fit.license.txt
Normal file
19
core/src/3rd-party-licenses/xterm_addon-fit.license.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -5,3 +5,7 @@ This folder contains artifacts created via [3rd-party.cjs](../../rollup/3rd-part
|
|||||||
As we would like to offer a way to run PyScript offline, and we already offer a `dist` folder with all the necessary scripts, we have created a foreign dependencies resolver that allow to lazy-load CDN dependencies out of the box.
|
As we would like to offer a way to run PyScript offline, and we already offer a `dist` folder with all the necessary scripts, we have created a foreign dependencies resolver that allow to lazy-load CDN dependencies out of the box.
|
||||||
|
|
||||||
Please **note** these dependencies are **not interpreters**, because interpreters have their own mechanism, folders structure, WASM files, and whatnot, to work locally, but at least XTerm or the TOML parser, among other lazy dependencies, should be available within the dist folder.
|
Please **note** these dependencies are **not interpreters**, because interpreters have their own mechanism, folders structure, WASM files, and whatnot, to work locally, but at least XTerm or the TOML parser, among other lazy dependencies, should be available within the dist folder.
|
||||||
|
|
||||||
|
## Licenses
|
||||||
|
|
||||||
|
All licenses provided by 3rd-party authors can be found in [3rd-party-licenses](../3rd-party-licenses/) folder.
|
||||||
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.3.
|
||||||
|
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||||
7
core/src/3rd-party/xterm.js
vendored
Normal file
7
core/src/3rd-party/xterm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm_addon-fit.js
vendored
Normal file
7
core/src/3rd-party/xterm_addon-fit.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
|
||||||
|
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var e,t,r={exports:{}};self;var s=r.exports=(e=t={},Object.defineProperty(e,"__esModule",{value:!0}),e.FitAddon=void 0,e.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(s.getPropertyValue("height")),o=Math.max(0,parseInt(s.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=i-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=o-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}},t),i=r.exports.FitAddon,o=r.exports.__esModule;export{i as FitAddon,o as __esModule,s as default};
|
||||||
7
core/src/3rd-party/xterm_addon-web-links.js
vendored
Normal file
7
core/src/3rd-party/xterm_addon-web-links.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
|
||||||
|
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var e={exports:{}};self;var t=e.exports=(()=>{var e={6:(e,t)=>{function r(e){try{const t=new URL(e),r=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(r.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,r,n={}){this._terminal=e,this._regex=t,this._handler=r,this._options=n}provideLinks(e,t){const r=n.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(r))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,r)=>{if(this._options.hover){const{range:n}=e;this._options.hover(t,r,n)}},e)))}};class n{static computeLink(e,t,o,s){const i=new RegExp(t.source,(t.flags||"")+"g"),[a,l]=n._getWindowedLineStrings(e-1,o),c=a.join("");let d;const p=[];for(;d=i.exec(c);){const e=d[0];if(!r(e))continue;const[t,i]=n._mapStrIdx(o,l,0,d.index),[a,c]=n._mapStrIdx(o,t,i,e.length);if(-1===t||-1===i||-1===a||-1===c)continue;const h={start:{x:i+1,y:t+1},end:{x:c,y:a+1}};p.push({range:h,text:e,activate:s})}return p}static _getWindowedLineStrings(e,t){let r,n=e,o=e,s=0,i="";const a=[];if(r=t.buffer.active.getLine(e)){const e=r.translateToString(!0);if(r.isWrapped&&" "!==e[0]){for(s=0;(r=t.buffer.active.getLine(--n))&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),r.isWrapped&&-1===i.indexOf(" ")););a.reverse()}for(a.push(e),s=0;(r=t.buffer.active.getLine(++o))&&r.isWrapped&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),-1===i.indexOf(" ")););}return[a,n]}static _mapStrIdx(e,t,r,n){const o=e.buffer.active,s=o.getNullCell();let i=r;for(;n;){const e=o.getLine(t);if(!e)return[-1,-1];for(let r=i;r<e.length;++r){e.getCell(r,s);const i=s.getChars();if(s.getWidth()&&(n-=i.length||1,r===e.length-1&&""===i)){const e=o.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,s),2===s.getWidth()&&(n+=1))}if(n<0)return[t,r]}t++,i=0}return[t,i]}}t.LinkComputer=n}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=r(6),o=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function s(e,t){const r=window.open();if(r){try{r.opener=null}catch{}r.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=s,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const r=this._options,n=r.urlRegex||o;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,n,this._handler,r))}dispose(){this._linkProvider?.dispose()}}})(),n})(),r=e.exports.WebLinksAddon,n=e.exports.__esModule;export{r as WebLinksAddon,n as __esModule,t as default};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
import TYPES from "./types.js";
|
import TYPES from "./types.js";
|
||||||
|
|
||||||
const waitForIt = [];
|
const waitForIt = [];
|
||||||
@@ -5,7 +6,7 @@ const waitForIt = [];
|
|||||||
for (const [TYPE] of TYPES) {
|
for (const [TYPE] of TYPES) {
|
||||||
const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`];
|
const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`];
|
||||||
for (const element of document.querySelectorAll(selectors.join(","))) {
|
for (const element of document.querySelectorAll(selectors.join(","))) {
|
||||||
const { promise, resolve } = Promise.withResolvers();
|
const { promise, resolve } = withResolvers();
|
||||||
waitForIt.push(promise);
|
waitForIt.push(promise);
|
||||||
element.addEventListener(`${TYPE}:done`, resolve, { once: true });
|
element.addEventListener(`${TYPE}:done`, resolve, { once: true });
|
||||||
}
|
}
|
||||||
169
core/src/config.js
Normal file
169
core/src/config.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* This file parses a generic <py-config> or config attribute
|
||||||
|
* to use as base config for all py-script elements, importing
|
||||||
|
* also a queue of plugins *before* the interpreter (if any) resolves.
|
||||||
|
*/
|
||||||
|
import { $$ } from "basic-devtools";
|
||||||
|
|
||||||
|
import TYPES from "./types.js";
|
||||||
|
import allPlugins from "./plugins.js";
|
||||||
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
|
import { ErrorCode } from "./exceptions.js";
|
||||||
|
|
||||||
|
const { BAD_CONFIG, CONFLICTING_CODE } = ErrorCode;
|
||||||
|
|
||||||
|
const badURL = (url, expected = "") => {
|
||||||
|
let message = `(${BAD_CONFIG}): Invalid URL: ${url}`;
|
||||||
|
if (expected) message += `\nexpected ${expected} content`;
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string, returns its trimmed content as text,
|
||||||
|
* fetching it from a file if the content is a URL.
|
||||||
|
* @param {string} config either JSON, TOML, or a file to fetch
|
||||||
|
* @param {string?} type the optional type to enforce
|
||||||
|
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||||
|
*/
|
||||||
|
export const configDetails = async (config, type) => {
|
||||||
|
let text = config?.trim();
|
||||||
|
// we only support an object as root config
|
||||||
|
let url = "",
|
||||||
|
toml = false,
|
||||||
|
json = /^{/.test(text) && /}$/.test(text);
|
||||||
|
// handle files by extension (relaxing urls parts after)
|
||||||
|
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
|
||||||
|
const ext = RegExp.$1;
|
||||||
|
if (ext === "json" && type !== "toml") json = true;
|
||||||
|
else if (ext === "toml" && type !== "json") toml = true;
|
||||||
|
else badURL(text, type);
|
||||||
|
url = text;
|
||||||
|
text = (await fetch(url).then(getText)).trim();
|
||||||
|
}
|
||||||
|
return { json, toml: toml || (!json && !!text), text, url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
|
||||||
|
|
||||||
|
const relative_url = (url, base = location.href) => new URL(url, base).href;
|
||||||
|
|
||||||
|
const syntaxError = (type, url, { message }) => {
|
||||||
|
let str = `(${BAD_CONFIG}): Invalid ${type}`;
|
||||||
|
if (url) str += ` @ ${url}`;
|
||||||
|
return new SyntaxError(`${str}\n${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configs = new Map();
|
||||||
|
|
||||||
|
for (const [TYPE] of TYPES) {
|
||||||
|
/** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||||
|
let plugins;
|
||||||
|
|
||||||
|
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||||
|
let parsed;
|
||||||
|
|
||||||
|
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
|
||||||
|
let error;
|
||||||
|
|
||||||
|
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
|
||||||
|
let configURL;
|
||||||
|
|
||||||
|
let config,
|
||||||
|
type,
|
||||||
|
parser,
|
||||||
|
pyElement,
|
||||||
|
pyConfigs = $$(`${TYPE}-config`),
|
||||||
|
attrConfigs = $$(
|
||||||
|
[
|
||||||
|
`script[type="${TYPE}"][config]:not([worker])`,
|
||||||
|
`${TYPE}-script[config]:not([worker])`,
|
||||||
|
].join(","),
|
||||||
|
);
|
||||||
|
|
||||||
|
// throw an error if there are multiple <py-config> or <mpy-config>
|
||||||
|
if (pyConfigs.length > 1) {
|
||||||
|
error = conflictError(`Too many ${TYPE}-config`);
|
||||||
|
} else {
|
||||||
|
// throw an error if there are <x-config> and config="x" attributes
|
||||||
|
if (pyConfigs.length && attrConfigs.length) {
|
||||||
|
error = conflictError(
|
||||||
|
`Ambiguous ${TYPE}-config VS config attribute`,
|
||||||
|
);
|
||||||
|
} else if (pyConfigs.length) {
|
||||||
|
[pyElement] = pyConfigs;
|
||||||
|
config = pyElement.getAttribute("src") || pyElement.textContent;
|
||||||
|
type = pyElement.getAttribute("type");
|
||||||
|
parser = pyElement.getAttribute("config-parser");
|
||||||
|
} else if (attrConfigs.length) {
|
||||||
|
[pyElement, ...attrConfigs] = attrConfigs;
|
||||||
|
config = pyElement.getAttribute("config");
|
||||||
|
parser = pyElement.getAttribute("config-parser");
|
||||||
|
// throw an error if dirrent scripts use different configs
|
||||||
|
if (
|
||||||
|
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
||||||
|
) {
|
||||||
|
error = conflictError(
|
||||||
|
"Unable to use different configs on main",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// catch possible fetch errors
|
||||||
|
if (!error && config) {
|
||||||
|
try {
|
||||||
|
const { json, toml, text, url } = await configDetails(config, type);
|
||||||
|
if (url) configURL = relative_url(url);
|
||||||
|
config = text;
|
||||||
|
if (json || type === "json") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
error = syntaxError("JSON", url, e);
|
||||||
|
}
|
||||||
|
} else if (toml || type === "toml") {
|
||||||
|
try {
|
||||||
|
const module = parser
|
||||||
|
? await import(parser)
|
||||||
|
: await import(
|
||||||
|
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
const parse = module.parse || module.default;
|
||||||
|
parsed = parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
error = syntaxError("TOML", url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse all plugins and optionally ignore only
|
||||||
|
// those flagged as "undesired" via `!` prefix
|
||||||
|
plugins = async () => {
|
||||||
|
const toBeAwaited = [];
|
||||||
|
for (const [key, value] of Object.entries(allPlugins)) {
|
||||||
|
if (error) {
|
||||||
|
if (key === "error") {
|
||||||
|
// show on page the config is broken, meaning that
|
||||||
|
// it was not possible to disable error plugin neither
|
||||||
|
// as that part wasn't correctly parsed anyway
|
||||||
|
value().then(({ notify }) => notify(error.message));
|
||||||
|
}
|
||||||
|
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||||
|
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||||
|
} else if (key === "error") {
|
||||||
|
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await Promise.all(toBeAwaited);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Number.isSafeInteger(parsed?.experimental_ffi_timeout))
|
||||||
|
globalThis.reflected_ffi_timeout = parsed?.experimental_ffi_timeout;
|
||||||
|
|
||||||
|
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { configs, relative_url };
|
||||||
61
core/src/core.css
Normal file
61
core/src/core.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
py-script,
|
||||||
|
py-config,
|
||||||
|
mpy-script,
|
||||||
|
mpy-config {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PyEditor */
|
||||||
|
.py-editor-box,
|
||||||
|
.mpy-editor-box {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.py-editor-input,
|
||||||
|
.mpy-editor-input {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.py-editor-box::before,
|
||||||
|
.mpy-editor-box::before {
|
||||||
|
content: attr(data-env);
|
||||||
|
display: block;
|
||||||
|
font-size: x-small;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
.py-editor-output,
|
||||||
|
.mpy-editor-output {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.py-editor-run-button,
|
||||||
|
.mpy-editor-run-button {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.py-editor-box:hover .py-editor-run-button,
|
||||||
|
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||||
|
.py-editor-run-button:focus,
|
||||||
|
.py-editor-run-button.running,
|
||||||
|
.mpy-editor-run-button:focus,
|
||||||
|
.mpy-editor-run-button.running {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
py-terminal span,
|
||||||
|
mpy-terminal span {
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.pyscript-fs {
|
||||||
|
border-radius: 8px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.pyscript-fs > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
/*! (c) PyScript Development Team */
|
/*! (c) PyScript Development Team */
|
||||||
|
|
||||||
|
import "./zero-redirect.js";
|
||||||
import stickyModule from "sticky-module";
|
import stickyModule from "sticky-module";
|
||||||
import "@ungap/with-resolvers";
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
INVALID_CONTENT,
|
INVALID_CONTENT,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
define,
|
define,
|
||||||
defineProperty,
|
defineProperty,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
isSync,
|
||||||
queryTarget,
|
queryTarget,
|
||||||
unescape,
|
unescape,
|
||||||
whenDefined,
|
whenDefined,
|
||||||
@@ -19,44 +21,65 @@ import {
|
|||||||
|
|
||||||
import "./all-done.js";
|
import "./all-done.js";
|
||||||
import TYPES from "./types.js";
|
import TYPES from "./types.js";
|
||||||
import configs from "./config.js";
|
import { configs, relative_url } from "./config.js";
|
||||||
import sync from "./sync.js";
|
import sync from "./sync.js";
|
||||||
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
||||||
import { ErrorCode } from "./exceptions.js";
|
import { ErrorCode } from "./exceptions.js";
|
||||||
import { robustFetch as fetch, getText } from "./fetch.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";
|
||||||
|
import * as fs from "./fs.js";
|
||||||
|
|
||||||
// allows lazy element features on code evaluation
|
import codemirror from "./plugins/codemirror.js";
|
||||||
let currentElement;
|
export { codemirror };
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
export { stdlib, optional, inputFailure };
|
||||||
|
|
||||||
|
export const donkey = (options) =>
|
||||||
|
import(/* webpackIgnore: true */ "./plugins/donkey.js").then((module) =>
|
||||||
|
module.default(options),
|
||||||
|
);
|
||||||
|
|
||||||
// generic helper to disambiguate between custom element and script
|
// generic helper to disambiguate between custom element and script
|
||||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||||
|
|
||||||
let shouldRegister = true;
|
// Used to create either Pyodide or MicroPython workers
|
||||||
const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
// with the PyScript module available within the code
|
||||||
// automatically use the pyscript stderr (when/if defined)
|
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
|
||||||
// this defaults to console.error
|
([TYPE, interpreter]) =>
|
||||||
function PyWorker(...args) {
|
/**
|
||||||
const worker = $XWorker(...args);
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
worker.onerror = ({ error }) => io.stderr(error);
|
* @param {string} file the python file to run ina worker.
|
||||||
return worker;
|
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||||
}
|
* @returns {Promise<Worker & {sync: object}>}
|
||||||
|
*/
|
||||||
// enrich the Python env with some JS utility for main
|
async function PyScriptWorker(file, options) {
|
||||||
interpreter.registerJsModule("_pyscript", {
|
await configs.get(TYPE).plugins;
|
||||||
PyWorker,
|
const xworker = XWorker.call(
|
||||||
get target() {
|
new Hook(null, hooked.get(TYPE)),
|
||||||
return isScript(currentElement)
|
file,
|
||||||
? currentElement.target.id
|
{
|
||||||
: currentElement.id;
|
...options,
|
||||||
|
type: interpreter,
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
};
|
assign(xworker.sync, sync);
|
||||||
|
return xworker.ready;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// avoid multiple initialization of the same library
|
// avoid multiple initialization of the same library
|
||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
PyWorker: exportedPyWorker,
|
PyWorker: exportedPyWorker,
|
||||||
|
MPWorker: exportedMPWorker,
|
||||||
hooks: exportedHooks,
|
hooks: exportedHooks,
|
||||||
config: exportedConfig,
|
config: exportedConfig,
|
||||||
whenDefined: exportedWhenDefined,
|
whenDefined: exportedWhenDefined,
|
||||||
@@ -64,6 +87,7 @@ const [
|
|||||||
alreadyLive,
|
alreadyLive,
|
||||||
] = stickyModule("@pyscript/core", {
|
] = stickyModule("@pyscript/core", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
|
MPWorker,
|
||||||
hooks,
|
hooks,
|
||||||
config: {},
|
config: {},
|
||||||
whenDefined,
|
whenDefined,
|
||||||
@@ -71,12 +95,17 @@ const [
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
TYPES,
|
TYPES,
|
||||||
|
relative_url,
|
||||||
exportedPyWorker as PyWorker,
|
exportedPyWorker as PyWorker,
|
||||||
|
exportedMPWorker as MPWorker,
|
||||||
exportedHooks as hooks,
|
exportedHooks as hooks,
|
||||||
exportedConfig as config,
|
exportedConfig as config,
|
||||||
exportedWhenDefined as whenDefined,
|
exportedWhenDefined as whenDefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const offline_interpreter = (config) =>
|
||||||
|
config?.interpreter && relative_url(config.interpreter);
|
||||||
|
|
||||||
const hooked = new Map();
|
const hooked = new Map();
|
||||||
|
|
||||||
for (const [TYPE, interpreter] of TYPES) {
|
for (const [TYPE, interpreter] of TYPES) {
|
||||||
@@ -88,7 +117,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
else dispatch(element, TYPE, "done");
|
else dispatch(element, TYPE, "done");
|
||||||
};
|
};
|
||||||
|
|
||||||
const { config, plugins, error } = configs.get(TYPE);
|
let { config, configURL, plugins, error } = configs.get(TYPE);
|
||||||
|
|
||||||
// create a unique identifier when/if needed
|
// create a unique identifier when/if needed
|
||||||
let id = 0;
|
let id = 0;
|
||||||
@@ -118,25 +147,55 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
return code;
|
return code;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// register once any interpreter
|
||||||
|
let alreadyRegistered = false;
|
||||||
|
|
||||||
|
// allows lazy element features on code evaluation
|
||||||
|
let currentElement;
|
||||||
|
|
||||||
|
const registerModule = ({ XWorker, interpreter, io }) => {
|
||||||
|
// avoid multiple registration of the same interpreter
|
||||||
|
if (alreadyRegistered) return;
|
||||||
|
alreadyRegistered = true;
|
||||||
|
|
||||||
|
// automatically use the pyscript stderr (when/if defined)
|
||||||
|
// this defaults to console.error
|
||||||
|
function PyWorker(...args) {
|
||||||
|
const worker = XWorker(...args);
|
||||||
|
worker.onerror = ({ error }) => io.stderr(error);
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich the Python env with some JS utility for main
|
||||||
|
interpreter.registerJsModule("_pyscript", {
|
||||||
|
PyWorker,
|
||||||
|
fs,
|
||||||
|
interpreter,
|
||||||
|
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||||
|
get target() {
|
||||||
|
return isScript(currentElement)
|
||||||
|
? currentElement.target.id
|
||||||
|
: currentElement.id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// define the module as both `<script type="py">` and `<py-script>`
|
// define the module as both `<script type="py">` and `<py-script>`
|
||||||
// but only if the config didn't throw an error
|
// but only if the config didn't throw an error
|
||||||
if (!error) {
|
if (!error) {
|
||||||
// ensure plugins are bootstrapped already before custom type definition
|
// ensure plugins are bootstrapped already before custom type definition
|
||||||
// NOTE: we cannot top-level await in here as plugins import other utilities
|
// NOTE: we cannot top-level await in here as plugins import other utilities
|
||||||
// from core.js itself so that custom definition should not be blocking.
|
// from core.js itself so that custom definition should not be blocking.
|
||||||
plugins.then(() => {
|
plugins().then(() => {
|
||||||
// possible early errors sent by polyscript
|
// possible early errors sent by polyscript
|
||||||
const errors = new Map();
|
const errors = new Map();
|
||||||
|
|
||||||
// specific main and worker hooks
|
// specific main and worker hooks
|
||||||
const hooks = {
|
const hooks = {
|
||||||
main: {
|
main: {
|
||||||
...codeFor(main),
|
...codeFor(main, TYPE),
|
||||||
async onReady(wrap, element) {
|
async onReady(wrap, element) {
|
||||||
if (shouldRegister) {
|
|
||||||
shouldRegister = false;
|
|
||||||
registerModule(wrap);
|
registerModule(wrap);
|
||||||
}
|
|
||||||
|
|
||||||
// allows plugins to do whatever they want with the element
|
// allows plugins to do whatever they want with the element
|
||||||
// before regular stuff happens in here
|
// before regular stuff happens in here
|
||||||
@@ -156,15 +215,13 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isScript(element)) {
|
if (isScript(element)) {
|
||||||
const {
|
const isAsync = !isSync(element);
|
||||||
attributes: { async: isAsync, target },
|
const target = element.getAttribute("target");
|
||||||
} = element;
|
const show = target
|
||||||
const hasTarget = !!target?.value;
|
? queryTarget(element, target)
|
||||||
const show = hasTarget
|
|
||||||
? queryTarget(element, target.value)
|
|
||||||
: document.createElement("script-py");
|
: document.createElement("script-py");
|
||||||
|
|
||||||
if (!hasTarget) {
|
if (!target) {
|
||||||
const { head, body } = document;
|
const { head, body } = document;
|
||||||
if (head.contains(element)) body.append(show);
|
if (head.contains(element)) body.append(show);
|
||||||
else element.after(show);
|
else element.after(show);
|
||||||
@@ -231,7 +288,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
worker: {
|
worker: {
|
||||||
...codeFor(worker),
|
...codeFor(worker, TYPE),
|
||||||
// these are lazy getters that returns a composition
|
// these are lazy getters that returns a composition
|
||||||
// of the current hooks or undefined, if no hook is present
|
// of the current hooks or undefined, if no hook is present
|
||||||
get onReady() {
|
get onReady() {
|
||||||
@@ -254,12 +311,24 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
|
|
||||||
hooked.set(TYPE, hooks);
|
hooked.set(TYPE, hooks);
|
||||||
|
|
||||||
|
// allow offline interpreter detection via [offline] attribute
|
||||||
|
let version = offline_interpreter(config);
|
||||||
|
if (!version) {
|
||||||
|
const css = "script[type='module'][offline]";
|
||||||
|
const s = document.querySelector(css)?.src;
|
||||||
|
if (s && import.meta.url.startsWith(s.replace(/\.js$/, ""))) {
|
||||||
|
version = `./pyscript/${interpreter}/${interpreter}.mjs`;
|
||||||
|
version = offline_interpreter({ interpreter: version });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
define(TYPE, {
|
define(TYPE, {
|
||||||
config,
|
config,
|
||||||
|
configURL,
|
||||||
interpreter,
|
interpreter,
|
||||||
hooks,
|
hooks,
|
||||||
|
version,
|
||||||
env: `${TYPE}-script`,
|
env: `${TYPE}-script`,
|
||||||
version: config?.interpreter,
|
|
||||||
onerror(error, element) {
|
onerror(error, element) {
|
||||||
errors.set(element, error);
|
errors.set(element, error);
|
||||||
},
|
},
|
||||||
@@ -270,7 +339,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
class extends HTMLElement {
|
class extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
assign(super(), {
|
assign(super(), {
|
||||||
_wrap: Promise.withResolvers(),
|
_wrap: withResolvers(),
|
||||||
srcCode: "",
|
srcCode: "",
|
||||||
executed: false,
|
executed: false,
|
||||||
});
|
});
|
||||||
@@ -284,7 +353,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
if (!this.executed) {
|
if (!this.executed) {
|
||||||
this.executed = true;
|
this.executed = true;
|
||||||
const isAsync = this.hasAttribute("async");
|
const isAsync = !isSync(this);
|
||||||
const { io, run, runAsync } = await this._wrap
|
const { io, run, runAsync } = await this._wrap
|
||||||
.promise;
|
.promise;
|
||||||
this.srcCode = await fetchSource(
|
this.srcCode = await fetchSource(
|
||||||
@@ -310,24 +379,3 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// export the used config without allowing leaks through it
|
// export the used config without allowing leaks through it
|
||||||
exportedConfig[TYPE] = structuredClone(config);
|
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.
|
* 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 = {
|
export const ErrorCode = {
|
||||||
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
|
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 { 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
|
* This is a fetch wrapper that handles any non 200 responses and throws a
|
||||||
82
core/src/fs.js
Normal file
82
core/src/fs.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import IDBMap from "@webreflection/idb-map";
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import { assign } from "polyscript/exports";
|
||||||
|
import { $$ } from "basic-devtools";
|
||||||
|
|
||||||
|
const stop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚠️ these two constants MUST be passed as `fs`
|
||||||
|
// within the worker onBeforeRunAsync hook!
|
||||||
|
export const NAMESPACE = "@pyscript.fs";
|
||||||
|
export const ERROR = "storage permissions not granted";
|
||||||
|
|
||||||
|
export const idb = new IDBMap(NAMESPACE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask a user action via dialog and returns the directory handler once granted.
|
||||||
|
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||||
|
* @returns {Promise<FileSystemDirectoryHandle>}
|
||||||
|
*/
|
||||||
|
export const getFileSystemDirectoryHandle = async (options) => {
|
||||||
|
if (!("showDirectoryPicker" in globalThis)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("showDirectoryPicker is not supported"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promise, resolve, reject } = withResolvers();
|
||||||
|
|
||||||
|
const how = { id: "pyscript", mode: "readwrite", ...options };
|
||||||
|
if (options.hint) how.startIn = options.hint;
|
||||||
|
|
||||||
|
const transient = async () => {
|
||||||
|
try {
|
||||||
|
/* eslint-disable */
|
||||||
|
const handler = await showDirectoryPicker(how);
|
||||||
|
/* eslint-enable */
|
||||||
|
if ((await handler.requestPermission(how)) === "granted") {
|
||||||
|
resolve(handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch ({ message }) {
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// in case the user decided to attach the event itself
|
||||||
|
// as opposite of relying our dialog walkthrough
|
||||||
|
if (navigator.userActivation?.isActive) {
|
||||||
|
if (!(await transient())) reject(new Error(ERROR));
|
||||||
|
} else {
|
||||||
|
const dialog = assign(document.createElement("dialog"), {
|
||||||
|
className: "pyscript-fs",
|
||||||
|
innerHTML: [
|
||||||
|
"<strong>ℹ️ Persistent FileSystem</strong><hr>",
|
||||||
|
"<p><small>PyScript would like to access a local folder.</small></p>",
|
||||||
|
"<div><button title='ok'>✅ Authorize</button>",
|
||||||
|
"<button title='cancel'>❌</button></div>",
|
||||||
|
].join(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ok, cancel] = $$("button", dialog);
|
||||||
|
|
||||||
|
ok.addEventListener("click", async (event) => {
|
||||||
|
stop(event);
|
||||||
|
if (await transient()) dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancel.addEventListener("click", async (event) => {
|
||||||
|
stop(event);
|
||||||
|
reject(new Error(ERROR));
|
||||||
|
dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(dialog).showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import { typedSet } from "type-checked-collections";
|
|||||||
import { dedent } from "polyscript/exports";
|
import { dedent } from "polyscript/exports";
|
||||||
import toJSONCallback from "to-json-callback";
|
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 main = (name) => hooks.main[name];
|
||||||
export const worker = (name) => hooks.worker[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 = {};
|
const hooks = {};
|
||||||
code(hooks, branch, `codeBeforeRun`, stdlib);
|
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||||
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
|
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||||
code(hooks, branch, `codeAfterRun`);
|
code(hooks, branch, `codeAfterRun`);
|
||||||
code(hooks, branch, `codeAfterRunAsync`);
|
code(hooks, branch, `codeAfterRunAsync`);
|
||||||
return hooks;
|
return hooks;
|
||||||
@@ -45,6 +46,19 @@ export const createFunction = (self, name) => {
|
|||||||
const SetFunction = typedSet({ typeof: "function" });
|
const SetFunction = typedSet({ typeof: "function" });
|
||||||
const SetString = typedSet({ typeof: "string" });
|
const SetString = typedSet({ typeof: "string" });
|
||||||
|
|
||||||
|
export const inputFailure = `
|
||||||
|
import builtins
|
||||||
|
def input(prompt=""):
|
||||||
|
raise Exception("\\n ".join([
|
||||||
|
"input() doesn't work when PyScript runs in the main thread.",
|
||||||
|
"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/"
|
||||||
|
]))
|
||||||
|
|
||||||
|
builtins.input = input
|
||||||
|
del builtins
|
||||||
|
del input
|
||||||
|
`;
|
||||||
|
|
||||||
export const hooks = {
|
export const hooks = {
|
||||||
main: {
|
main: {
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
@@ -60,7 +74,7 @@ export const hooks = {
|
|||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onAfterRunAsync: new SetFunction(),
|
onAfterRunAsync: new SetFunction(),
|
||||||
/** @type {Set<string>} */
|
/** @type {Set<string>} */
|
||||||
codeBeforeRun: new SetString(),
|
codeBeforeRun: new SetString([inputFailure]),
|
||||||
/** @type {Set<string>} */
|
/** @type {Set<string>} */
|
||||||
codeBeforeRunAsync: new SetString(),
|
codeBeforeRunAsync: new SetString(),
|
||||||
/** @type {Set<string>} */
|
/** @type {Set<string>} */
|
||||||
@@ -74,7 +88,19 @@ export const hooks = {
|
|||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onBeforeRun: new SetFunction(),
|
onBeforeRun: new SetFunction(),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onBeforeRunAsync: new SetFunction(),
|
onBeforeRunAsync: new SetFunction([
|
||||||
|
({ interpreter }) => {
|
||||||
|
interpreter.registerJsModule("_pyscript", {
|
||||||
|
// cannot be imported from fs.js
|
||||||
|
// because this code is stringified
|
||||||
|
fs: {
|
||||||
|
ERROR: "storage permissions not granted",
|
||||||
|
NAMESPACE: "@pyscript.fs",
|
||||||
|
},
|
||||||
|
interpreter,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
]),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onAfterRun: new SetFunction(),
|
onAfterRun: new SetFunction(),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
38
core/src/plugins.js
Normal file
38
core/src/plugins.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||||
|
export default {
|
||||||
|
codemirror: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/codemirror.js"
|
||||||
|
),
|
||||||
|
["deprecations-manager"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/deprecations-manager.js"
|
||||||
|
),
|
||||||
|
donkey: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/donkey.js"
|
||||||
|
),
|
||||||
|
error: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/error.js"
|
||||||
|
),
|
||||||
|
["py-editor"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-editor.js"
|
||||||
|
),
|
||||||
|
["py-game"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-game.js"
|
||||||
|
),
|
||||||
|
["py-terminal"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-terminal.js"
|
||||||
|
),
|
||||||
|
};
|
||||||
31
core/src/plugins/codemirror.js
Normal file
31
core/src/plugins/codemirror.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// lazy loaded on-demand codemirror related files
|
||||||
|
export default {
|
||||||
|
get core() {
|
||||||
|
return import(/* webpackIgnore: true */ "../3rd-party/codemirror.js");
|
||||||
|
},
|
||||||
|
get state() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get python() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get language() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get view() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get commands() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
27
core/src/plugins/deprecations-manager.js
Normal file
27
core/src/plugins/deprecations-manager.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// PyScript Derepcations Plugin
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
|
// react lazily on PyScript bootstrap
|
||||||
|
hooks.main.onReady.add(checkDeprecations);
|
||||||
|
hooks.main.onWorker.add(checkDeprecations);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that there are no scripts loading from pyscript.net/latest
|
||||||
|
*/
|
||||||
|
function checkDeprecations() {
|
||||||
|
const scripts = document.querySelectorAll("script");
|
||||||
|
for (const script of scripts) checkLoadingScriptsFromLatest(script.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if src being loaded from pyscript.net/latest and display a notification if true
|
||||||
|
* * @param {string} src
|
||||||
|
*/
|
||||||
|
function checkLoadingScriptsFromLatest(src) {
|
||||||
|
if (/\/pyscript\.net\/latest/.test(src)) {
|
||||||
|
notify(
|
||||||
|
"Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
core/src/plugins/donkey.js
Normal file
121
core/src/plugins/donkey.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import addPromiseListener from "add-promise-listener";
|
||||||
|
import { assign, dedent } from "polyscript/exports";
|
||||||
|
|
||||||
|
const { stringify } = JSON;
|
||||||
|
|
||||||
|
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||||
|
|
||||||
|
const donkey = ({
|
||||||
|
type = "py",
|
||||||
|
persistent,
|
||||||
|
terminal,
|
||||||
|
config,
|
||||||
|
serviceWorker,
|
||||||
|
}) => {
|
||||||
|
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
|
||||||
|
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
|
||||||
|
|
||||||
|
const src = URL.createObjectURL(
|
||||||
|
new Blob([
|
||||||
|
[
|
||||||
|
// this array is to better minify this code once in production
|
||||||
|
"from pyscript import sync, config",
|
||||||
|
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
|
||||||
|
"__locals__ = {}",
|
||||||
|
'if config["type"] == "py":',
|
||||||
|
" import sys",
|
||||||
|
" def __error__(_):",
|
||||||
|
" info = sys.exc_info()",
|
||||||
|
" return __message__(info[0], info[1])",
|
||||||
|
"else:",
|
||||||
|
" __error__ = lambda e: __message__(e.__class__, e.value)",
|
||||||
|
"def execute(code):",
|
||||||
|
` try: return ${invoke("exec", args)};`,
|
||||||
|
" except Exception as e: print(__error__(e));",
|
||||||
|
"def evaluate(code):",
|
||||||
|
` try: return ${invoke("eval", args)};`,
|
||||||
|
" except Exception as e: print(__error__(e));",
|
||||||
|
"sync.execute = execute",
|
||||||
|
"sync.evaluate = evaluate",
|
||||||
|
].join("\n"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// create the script that exposes the code to execute or evaluate
|
||||||
|
const script = assign(document.createElement("script"), { type, src });
|
||||||
|
script.toggleAttribute("worker", true);
|
||||||
|
script.toggleAttribute("terminal", true);
|
||||||
|
if (terminal) script.setAttribute("target", terminal);
|
||||||
|
if (config) {
|
||||||
|
script.setAttribute(
|
||||||
|
"config",
|
||||||
|
typeof config === "string" ? config : stringify(config),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
||||||
|
|
||||||
|
return addPromiseListener(
|
||||||
|
document.body.appendChild(script),
|
||||||
|
`${type}:done`,
|
||||||
|
{ stopPropagation: true },
|
||||||
|
).then(() => {
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
return script;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const utils = async (options) => {
|
||||||
|
const script = await donkey(options);
|
||||||
|
const { xworker, process, terminal } = script;
|
||||||
|
const { execute, evaluate } = xworker.sync;
|
||||||
|
script.remove();
|
||||||
|
return {
|
||||||
|
xworker,
|
||||||
|
process,
|
||||||
|
terminal,
|
||||||
|
execute,
|
||||||
|
evaluate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (options = {}) => {
|
||||||
|
let farmer = await utils(options);
|
||||||
|
let working = false;
|
||||||
|
const kill = () => {
|
||||||
|
if (farmer) {
|
||||||
|
farmer.xworker.terminate();
|
||||||
|
farmer.terminal.dispose();
|
||||||
|
farmer = null;
|
||||||
|
}
|
||||||
|
working = false;
|
||||||
|
};
|
||||||
|
const reload = async () => {
|
||||||
|
kill();
|
||||||
|
farmer = await utils(options);
|
||||||
|
};
|
||||||
|
const asyncTask = (method) => async (code) => {
|
||||||
|
// race condition ... a new task has been
|
||||||
|
// assigned while the previous one didn't finish
|
||||||
|
if (working) await reload();
|
||||||
|
working = true;
|
||||||
|
try {
|
||||||
|
return await farmer[method](dedent(code));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
working = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const asyncMethod = (method) => async () => {
|
||||||
|
if (working) await reload();
|
||||||
|
else farmer?.terminal[method]();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
process: asyncTask("process"),
|
||||||
|
execute: asyncTask("execute"),
|
||||||
|
evaluate: asyncTask("evaluate"),
|
||||||
|
clear: asyncMethod("clear"),
|
||||||
|
reset: asyncMethod("reset"),
|
||||||
|
kill,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
// PyScript Error Plugin
|
// PyScript Error Plugin
|
||||||
|
import { buffered } from "polyscript/exports";
|
||||||
import { hooks } from "../core.js";
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
|
let dontBotherDOM = false;
|
||||||
|
export function notOnDOM() {
|
||||||
|
dontBotherDOM = true;
|
||||||
|
}
|
||||||
|
|
||||||
hooks.main.onReady.add(function override(pyScript) {
|
hooks.main.onReady.add(function override(pyScript) {
|
||||||
// be sure this override happens only once
|
// be sure this override happens only once
|
||||||
hooks.main.onReady.delete(override);
|
hooks.main.onReady.delete(override);
|
||||||
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
|
|||||||
// trap generic `stderr` to propagate to it regardless
|
// trap generic `stderr` to propagate to it regardless
|
||||||
const { stderr } = pyScript.io;
|
const { stderr } = pyScript.io;
|
||||||
|
|
||||||
// override it with our own logic
|
const cb = (error, ...rest) => {
|
||||||
pyScript.io.stderr = (error, ...rest) => {
|
|
||||||
notify(error.message || error);
|
notify(error.message || error);
|
||||||
// let other plugins or stderr hook, if any, do the rest
|
// let other plugins or stderr hook, if any, do the rest
|
||||||
return stderr(error, ...rest);
|
return stderr(error, ...rest);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// override it with our own logic
|
||||||
|
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
|
||||||
|
|
||||||
// be sure uncaught Python errors are also visible
|
// be sure uncaught Python errors are also visible
|
||||||
addEventListener("error", ({ message }) => {
|
addEventListener("error", ({ message }) => {
|
||||||
if (message.startsWith("Uncaught PythonError")) notify(message);
|
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||||
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
|
|||||||
* @param {string} message
|
* @param {string} message
|
||||||
*/
|
*/
|
||||||
export function notify(message) {
|
export function notify(message) {
|
||||||
|
if (dontBotherDOM) return;
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "py-error";
|
div.className = "py-error";
|
||||||
div.textContent = message;
|
div.textContent = message;
|
||||||
491
core/src/plugins/py-editor.js
Normal file
491
core/src/plugins/py-editor.js
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
// PyScript py-editor plugin
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
|
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import codemirror from "./codemirror.js";
|
||||||
|
|
||||||
|
const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
|
||||||
|
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
|
const envs = new Map();
|
||||||
|
const configs = new Map();
|
||||||
|
const editors = new WeakMap();
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
worker: {
|
||||||
|
codeBeforeRun: () => stdlib,
|
||||||
|
// works on both Pyodide and MicroPython
|
||||||
|
onReady: ({ runAsync, io }, { sync }) => {
|
||||||
|
io.stdout = io.buffered(sync.write);
|
||||||
|
io.stderr = io.buffered(sync.writeErr);
|
||||||
|
sync.revoke();
|
||||||
|
sync.runAsync = runAsync;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (config, result) => {
|
||||||
|
if (typeof result === "boolean") throw `Invalid source: ${config}`;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelatedScript = (target, type) => {
|
||||||
|
const editor = target.closest(`.${type}-editor-box`);
|
||||||
|
return editor?.parentNode?.previousElementSibling;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function execute({ currentTarget, script }) {
|
||||||
|
const { env, pySrc, outDiv } = this;
|
||||||
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
|
if (hasRunButton) {
|
||||||
|
currentTarget.classList.add("running");
|
||||||
|
currentTarget.innerHTML = STOP_BUTTON;
|
||||||
|
outDiv.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envs.has(env)) {
|
||||||
|
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||||
|
const details = {
|
||||||
|
type: this.interpreter,
|
||||||
|
serviceWorker: this.serviceWorker,
|
||||||
|
};
|
||||||
|
const { config } = this;
|
||||||
|
if (config) {
|
||||||
|
// verify that config can be parsed and used
|
||||||
|
try {
|
||||||
|
details.configURL = relative_url(config);
|
||||||
|
if (config.endsWith(".toml")) {
|
||||||
|
const [{ parse }, toml] = await Promise.all([
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||||
|
),
|
||||||
|
fetch(config).then((r) => r.ok && r.text()),
|
||||||
|
]);
|
||||||
|
details.config = parse(validate(config, toml));
|
||||||
|
} else if (config.endsWith(".json")) {
|
||||||
|
const json = await fetch(config).then(
|
||||||
|
(r) => r.ok && r.json(),
|
||||||
|
);
|
||||||
|
details.config = validate(config, json);
|
||||||
|
} else {
|
||||||
|
details.configURL = relative_url("./config.txt");
|
||||||
|
details.config = JSON.parse(config);
|
||||||
|
}
|
||||||
|
details.version = offline_interpreter(details.config);
|
||||||
|
} catch (error) {
|
||||||
|
notify(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details.config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||||
|
|
||||||
|
// expose xworker like in terminal or other workers to allow
|
||||||
|
// creation and destruction of editors on the fly
|
||||||
|
if (hasRunButton) {
|
||||||
|
for (const type of TYPES.keys()) {
|
||||||
|
script = getRelatedScript(currentTarget, type);
|
||||||
|
if (script) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProperties(script, { xworker: { value: xworker } });
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
const { promise, resolve } = withResolvers();
|
||||||
|
envs.set(env, promise);
|
||||||
|
sync.revoke = () => {
|
||||||
|
URL.revokeObjectURL(srcLink);
|
||||||
|
resolve(xworker);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the env then set the target div
|
||||||
|
// before executing the current code
|
||||||
|
return envs.get(env).then((xworker) => {
|
||||||
|
xworker.onerror = ({ error }) => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
outDiv.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`<span style='color:red'>${
|
||||||
|
error.message || error
|
||||||
|
}</span>\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enable = () => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
currentTarget.classList.remove("running");
|
||||||
|
currentTarget.innerHTML = RUN_BUTTON;
|
||||||
|
const { previousElementSibling } =
|
||||||
|
currentTarget.closest("[data-env]").parentElement;
|
||||||
|
previousElementSibling?.dispatchEvent(
|
||||||
|
new Event("py-editor:done", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const { sync } = xworker;
|
||||||
|
sync.write = (str) => {
|
||||||
|
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||||
|
else console.log(str);
|
||||||
|
};
|
||||||
|
sync.writeErr = (str) => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
outDiv.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`<span style='color:red'>${str}</span>\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notify(str);
|
||||||
|
console.error(str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sync.runAsync(pySrc).then(enable, enable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceScript = (script, type) => {
|
||||||
|
script.xworker?.terminate();
|
||||||
|
const clone = script.cloneNode(true);
|
||||||
|
clone.type = `${type}-editor`;
|
||||||
|
const editor = editors.get(script);
|
||||||
|
if (editor) {
|
||||||
|
const content = editor.state.doc.toString();
|
||||||
|
clone.textContent = content;
|
||||||
|
editors.delete(script);
|
||||||
|
script.nextElementSibling.remove();
|
||||||
|
}
|
||||||
|
script.replaceWith(clone);
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeRunButton = (handler, type) => {
|
||||||
|
const runButton = document.createElement("button");
|
||||||
|
runButton.className = `absolute ${type}-editor-run-button`;
|
||||||
|
runButton.innerHTML = RUN_BUTTON;
|
||||||
|
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||||
|
runButton.addEventListener("click", async (event) => {
|
||||||
|
if (
|
||||||
|
runButton.classList.contains("running") &&
|
||||||
|
confirm("Stop evaluating this code?")
|
||||||
|
) {
|
||||||
|
const script = getRelatedScript(runButton, type);
|
||||||
|
if (script) {
|
||||||
|
const env = script.getAttribute("env");
|
||||||
|
// remove the bootstrapped env which could be one or shared
|
||||||
|
if (env) {
|
||||||
|
for (const [key, value] of TYPES) {
|
||||||
|
if (key === type) {
|
||||||
|
configs.delete(`${value}-${env}`);
|
||||||
|
envs.delete(`${value}-${env}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lonley script without setup node should be replaced
|
||||||
|
if (script.xworker) replaceScript(script, type);
|
||||||
|
// all scripts sharing the same env should be replaced
|
||||||
|
else {
|
||||||
|
const sel = `script[type^="${type}-editor"][env="${env}"]`;
|
||||||
|
for (const script of document.querySelectorAll(sel))
|
||||||
|
replaceScript(script, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runButton.blur();
|
||||||
|
await handler.handleEvent(event);
|
||||||
|
});
|
||||||
|
return runButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeEditorDiv = (handler, type) => {
|
||||||
|
const editorDiv = document.createElement("div");
|
||||||
|
editorDiv.className = `${type}-editor-input`;
|
||||||
|
editorDiv.setAttribute("aria-label", "Python Script Area");
|
||||||
|
|
||||||
|
const runButton = makeRunButton(handler, type);
|
||||||
|
const editorShadowContainer = document.createElement("div");
|
||||||
|
|
||||||
|
// avoid outer elements intercepting key events (reveal as example)
|
||||||
|
editorShadowContainer.addEventListener("keydown", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
editorDiv.append(runButton, editorShadowContainer);
|
||||||
|
|
||||||
|
return editorDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeOutDiv = (type) => {
|
||||||
|
const outDiv = document.createElement("div");
|
||||||
|
outDiv.className = `${type}-editor-output`;
|
||||||
|
outDiv.id = `${getID(type)}-output`;
|
||||||
|
return outDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeBoxDiv = (handler, type) => {
|
||||||
|
const boxDiv = document.createElement("div");
|
||||||
|
boxDiv.className = `${type}-editor-box`;
|
||||||
|
|
||||||
|
const editorDiv = makeEditorDiv(handler, type);
|
||||||
|
const outDiv = makeOutDiv(type);
|
||||||
|
boxDiv.append(editorDiv, outDiv);
|
||||||
|
|
||||||
|
return [boxDiv, outDiv, editorDiv.querySelector("button")];
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async (script, type, interpreter) => {
|
||||||
|
const [
|
||||||
|
{ basicSetup, EditorView },
|
||||||
|
{ Compartment },
|
||||||
|
{ python },
|
||||||
|
{ indentUnit },
|
||||||
|
{ keymap },
|
||||||
|
{ defaultKeymap, indentWithTab },
|
||||||
|
] = await Promise.all([
|
||||||
|
codemirror.core,
|
||||||
|
codemirror.state,
|
||||||
|
codemirror.python,
|
||||||
|
codemirror.language,
|
||||||
|
codemirror.view,
|
||||||
|
codemirror.commands,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let isSetup = script.hasAttribute("setup");
|
||||||
|
const hasConfig = script.hasAttribute("config");
|
||||||
|
const serviceWorker = script.getAttribute("service-worker");
|
||||||
|
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||||
|
|
||||||
|
// helps preventing too lazy ServiceWorker initialization on button run
|
||||||
|
if (serviceWorker) {
|
||||||
|
new XWorker("data:application/javascript,postMessage(0)", {
|
||||||
|
type: "dummy",
|
||||||
|
serviceWorker,
|
||||||
|
}).onmessage = ({ target }) => target.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasConfig && configs.has(env)) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
configs.get(env)
|
||||||
|
? `duplicated config for env: ${env}`
|
||||||
|
: `unable to add a config to the env: ${env}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
configs.set(env, hasConfig);
|
||||||
|
|
||||||
|
let source = script.textContent;
|
||||||
|
|
||||||
|
// verify the src points to a valid file that can be parsed
|
||||||
|
const { src } = script;
|
||||||
|
if (src) {
|
||||||
|
try {
|
||||||
|
source = validate(
|
||||||
|
src,
|
||||||
|
await fetch(src).then((b) => b.ok && b.text()),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
notify(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
// allow the listener to be overridden at distance
|
||||||
|
handleEvent: execute,
|
||||||
|
serviceWorker,
|
||||||
|
interpreter,
|
||||||
|
env,
|
||||||
|
config: hasConfig && script.getAttribute("config"),
|
||||||
|
get pySrc() {
|
||||||
|
return isSetup ? source : editor.state.doc.toString();
|
||||||
|
},
|
||||||
|
get outDiv() {
|
||||||
|
return isSetup ? null : outDiv;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let target;
|
||||||
|
defineProperties(script, {
|
||||||
|
target: { get: () => target },
|
||||||
|
handleEvent: {
|
||||||
|
get: () => context.handleEvent,
|
||||||
|
set: (callback) => {
|
||||||
|
// do not bother with logic if it was set back as its original handler
|
||||||
|
if (callback === execute) context.handleEvent = execute;
|
||||||
|
// in every other case be sure that if the listener override returned
|
||||||
|
// `false` nothing happens, otherwise keep doing what it always did
|
||||||
|
else {
|
||||||
|
context.handleEvent = async (event) => {
|
||||||
|
// trap the currentTarget ASAP (if any)
|
||||||
|
// otherwise it gets lost asynchronously
|
||||||
|
const { currentTarget } = event;
|
||||||
|
// augment a code snapshot before invoking the override
|
||||||
|
defineProperties(event, {
|
||||||
|
code: { value: context.pySrc },
|
||||||
|
});
|
||||||
|
// avoid executing the default handler if the override returned `false`
|
||||||
|
if ((await callback(event)) !== false)
|
||||||
|
await execute.call(context, { currentTarget });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
get: () => context.pySrc,
|
||||||
|
set: (insert) => {
|
||||||
|
if (isSetup) return;
|
||||||
|
editor.update([
|
||||||
|
editor.state.update({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
/**
|
||||||
|
* Simulate a setup node overriding the source to evaluate.
|
||||||
|
* @param {string} code the Python code to evaluate.
|
||||||
|
* @param {boolean} asRunButtonAction invoke the `Run` button handler.
|
||||||
|
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||||
|
*/
|
||||||
|
value(code, asRunButtonAction = false) {
|
||||||
|
if (asRunButtonAction) return listener();
|
||||||
|
const wasSetup = isSetup;
|
||||||
|
const wasSource = source;
|
||||||
|
isSetup = true;
|
||||||
|
source = code;
|
||||||
|
const restore = () => {
|
||||||
|
isSetup = wasSetup;
|
||||||
|
source = wasSource;
|
||||||
|
};
|
||||||
|
return context
|
||||||
|
.handleEvent({ currentTarget: null })
|
||||||
|
.then(restore, restore);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifyEditor = () => {
|
||||||
|
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSetup) {
|
||||||
|
await context.handleEvent({ currentTarget: null, script });
|
||||||
|
notifyEditor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = script.getAttribute("target");
|
||||||
|
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement(`${type}-editor`);
|
||||||
|
target.style.display = "block";
|
||||||
|
script.after(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.id) target.id = getID(type);
|
||||||
|
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||||
|
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||||
|
|
||||||
|
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
||||||
|
const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
|
||||||
|
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
|
||||||
|
|
||||||
|
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
|
||||||
|
const parent = inputChild.attachShadow({ mode: "open" });
|
||||||
|
// avoid inheriting styles from the outer component
|
||||||
|
parent.innerHTML = `<style> :host { all: initial; }</style>`;
|
||||||
|
|
||||||
|
target.appendChild(boxDiv);
|
||||||
|
|
||||||
|
const doc = dedent(script.textContent).trim();
|
||||||
|
|
||||||
|
// preserve user indentation, if any
|
||||||
|
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
||||||
|
|
||||||
|
const listener = () => !runButton.click();
|
||||||
|
const editor = new EditorView({
|
||||||
|
extensions: [
|
||||||
|
indentUnit.of(indentation),
|
||||||
|
new Compartment().of(python()),
|
||||||
|
keymap.of([
|
||||||
|
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||||
|
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||||
|
{ key: "Shift-Enter", run: listener, preventDefault: true },
|
||||||
|
// Consider removing defaultKeymap as likely redundant with basicSetup
|
||||||
|
...defaultKeymap,
|
||||||
|
// @see https://codemirror.net/examples/tab/
|
||||||
|
indentWithTab,
|
||||||
|
]),
|
||||||
|
basicSetup,
|
||||||
|
],
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||||
|
parent,
|
||||||
|
doc,
|
||||||
|
});
|
||||||
|
|
||||||
|
editors.set(script, editor);
|
||||||
|
editor.focus();
|
||||||
|
notifyEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// avoid too greedy MutationObserver operations at distance
|
||||||
|
let timeout = 0;
|
||||||
|
|
||||||
|
// avoid delayed initialization
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
// reset interval value then check for new scripts
|
||||||
|
const resetTimeout = () => {
|
||||||
|
timeout = 0;
|
||||||
|
pyEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||||
|
const pyEditor = () => {
|
||||||
|
if (timeout) return;
|
||||||
|
timeout = setTimeout(resetTimeout, 250);
|
||||||
|
for (const [type, interpreter] of TYPES) {
|
||||||
|
const selector = `script[type="${type}-editor"]`;
|
||||||
|
for (const script of document.querySelectorAll(selector)) {
|
||||||
|
// avoid any further bootstrap by changing the type as active
|
||||||
|
script.type += "-active";
|
||||||
|
// don't await in here or multiple calls might happen
|
||||||
|
// while the first script is being initialized
|
||||||
|
queue = queue.then(() => init(script, type, interpreter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
new MutationObserver(pyEditor).observe(document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// try to check the current document ASAP
|
||||||
|
export default pyEditor();
|
||||||
112
core/src/plugins/py-game.js
Normal file
112
core/src/plugins/py-game.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
dedent,
|
||||||
|
define,
|
||||||
|
createProgress,
|
||||||
|
loadProgress,
|
||||||
|
} from "polyscript/exports";
|
||||||
|
|
||||||
|
import { stdlib } from "../core.js";
|
||||||
|
import { configDetails } from "../config.js";
|
||||||
|
import { getText } from "../fetch.js";
|
||||||
|
|
||||||
|
const progress = createProgress("py-game");
|
||||||
|
|
||||||
|
const inputPatch = `
|
||||||
|
import builtins
|
||||||
|
def input(prompt=""):
|
||||||
|
import js
|
||||||
|
return js.prompt(prompt)
|
||||||
|
|
||||||
|
builtins.input = input
|
||||||
|
del builtins
|
||||||
|
del input
|
||||||
|
`;
|
||||||
|
|
||||||
|
let toBeWarned = true;
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
main: {
|
||||||
|
onReady: async (wrap, script) => {
|
||||||
|
if (toBeWarned) {
|
||||||
|
toBeWarned = false;
|
||||||
|
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
if (script.hasAttribute("config")) {
|
||||||
|
const value = script.getAttribute("config");
|
||||||
|
const { json, toml, text, url } = await configDetails(value);
|
||||||
|
if (json) config = JSON.parse(text);
|
||||||
|
else if (toml) {
|
||||||
|
const { parse } = await import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
config = parse(text);
|
||||||
|
}
|
||||||
|
if (config.packages) {
|
||||||
|
await wrap.interpreter.loadPackage("micropip");
|
||||||
|
const micropip = wrap.interpreter.pyimport("micropip");
|
||||||
|
await micropip.install(config.packages, {
|
||||||
|
keep_going: true,
|
||||||
|
});
|
||||||
|
micropip.destroy();
|
||||||
|
}
|
||||||
|
await loadProgress(
|
||||||
|
"py-game",
|
||||||
|
progress,
|
||||||
|
wrap.interpreter,
|
||||||
|
config,
|
||||||
|
url ? new URL(url, location.href).href : location.href,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap.interpreter.registerJsModule("_pyscript", {
|
||||||
|
PyWorker() {
|
||||||
|
throw new Error(
|
||||||
|
"Unable to use PyWorker in py-game scripts",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
js_import: (...urls) =>
|
||||||
|
Promise.all(urls.map((url) => import(url))),
|
||||||
|
get target() {
|
||||||
|
return script.id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrap.interpreter.runPythonAsync(stdlib);
|
||||||
|
wrap.interpreter.runPython(inputPatch);
|
||||||
|
|
||||||
|
let code = dedent(script.textContent);
|
||||||
|
if (script.src) code = await fetch(script.src).then(getText);
|
||||||
|
|
||||||
|
const target = script.getAttribute("target") || "canvas";
|
||||||
|
const canvas = document.getElementById(target);
|
||||||
|
wrap.interpreter.canvas.setCanvas2D(canvas);
|
||||||
|
|
||||||
|
// allow 3rd party to hook themselves right before
|
||||||
|
// the code gets executed
|
||||||
|
const event = new CustomEvent("py-game", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
detail: {
|
||||||
|
canvas,
|
||||||
|
code,
|
||||||
|
config,
|
||||||
|
wrap,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
// run only if the default was not prevented
|
||||||
|
if (!event.defaultPrevented)
|
||||||
|
await wrap.interpreter.runPythonAsync(code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
define("py-game", {
|
||||||
|
config: { packages: ["pygame-ce"] },
|
||||||
|
configURL: new URL("./config.txt", location.href).href,
|
||||||
|
interpreter: "pyodide",
|
||||||
|
env: "py-game",
|
||||||
|
hooks,
|
||||||
|
});
|
||||||
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
256
core/src/plugins/py-terminal/mpy.js
Normal file
256
core/src/plugins/py-terminal/mpy.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
// PyScript pyodide terminal plugin
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
import { hooks, inputFailure } from "../../core.js";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "mpy" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
|
||||||
|
|
||||||
|
interpreter.registerJsModule("_pyscript_input", {
|
||||||
|
input: pyterminal_read,
|
||||||
|
});
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from _pyscript_input import input",
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) pyterminal_write(missingReturn);
|
||||||
|
pyterminal_write(buffer);
|
||||||
|
};
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
sync.pyterminal_stream_write = () => {};
|
||||||
|
|
||||||
|
// tiny shim of the code module with only interact
|
||||||
|
// to bootstrap a REPL like environment
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer) interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const writer = encoder.writable.getWriter();
|
||||||
|
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pyterminal_ready();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const terminalOptions = {
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = () => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...terminalOptions,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
await stream.write(`${line}\r`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
|
||||||
|
// handle the read mode on input
|
||||||
|
let promisedChunks = null;
|
||||||
|
let readChunks = "";
|
||||||
|
|
||||||
|
sync.is_pyterminal = () => true;
|
||||||
|
|
||||||
|
// put the terminal in a read-only state
|
||||||
|
// frees the worker on \r
|
||||||
|
sync.pyterminal_read = (buffer) => {
|
||||||
|
terminal.write(buffer);
|
||||||
|
promisedChunks = withResolvers();
|
||||||
|
return promisedChunks.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// write if not reading input
|
||||||
|
sync.pyterminal_write = (buffer) => {
|
||||||
|
if (!promisedChunks) terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// add the onData terminal listener which forwards to the worker
|
||||||
|
// everything typed in a queued char-by-char way
|
||||||
|
sync.pyterminal_ready = () => {
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
stream = {
|
||||||
|
write: (buffer) =>
|
||||||
|
(queue = queue.then(() =>
|
||||||
|
sync.pyterminal_stream_write(buffer),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
terminal.onData((buffer) => {
|
||||||
|
if (promisedChunks) {
|
||||||
|
// handle backspace on input
|
||||||
|
if (buffer === "\x7f") {
|
||||||
|
// avoid over-greedy backspace
|
||||||
|
if (readChunks.length) {
|
||||||
|
readChunks = readChunks.slice(0, -1);
|
||||||
|
// override previous char position
|
||||||
|
// put an empty space to clear the char
|
||||||
|
// move back position again
|
||||||
|
buffer = "\b \b";
|
||||||
|
} else buffer = "";
|
||||||
|
} else readChunks += buffer;
|
||||||
|
if (buffer) {
|
||||||
|
terminal.write(buffer);
|
||||||
|
if (readChunks.endsWith("\r")) {
|
||||||
|
terminal.write("\n");
|
||||||
|
promisedChunks.resolve(readChunks.slice(0, -1));
|
||||||
|
promisedChunks = null;
|
||||||
|
readChunks = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream.write(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
} else {
|
||||||
|
// ⚠️ In an ideal world the inputFailure should never be used on main.
|
||||||
|
// However, Pyodide still can't compete with MicroPython REPL mode
|
||||||
|
// so while it's OK to keep that entry on main as default, we need
|
||||||
|
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
|
||||||
|
// also throw whenever an `input(...)` is required / digited.
|
||||||
|
hooks.main.codeBeforeRun.delete(inputFailure);
|
||||||
|
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "mpy") return;
|
||||||
|
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) terminal.write(missingReturn);
|
||||||
|
terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// expose the __terminal__ one-off reference
|
||||||
|
globalThis.__py_terminal__ = terminal;
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from js import prompt as input",
|
||||||
|
"from js import __py_terminal__ as __terminal__",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
// NOTE: this is NOT the same as the one within
|
||||||
|
// the onWorkerReady callback!
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer)
|
||||||
|
interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
stream = encoder.writable.getWriter();
|
||||||
|
terminal.onData((buffer) => stream.write(buffer));
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
192
core/src/plugins/py-terminal/py.js
Normal file
192
core/src/plugins/py-terminal/py.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// PyScript py-terminal plugin
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
import { hooks } from "../../core.js";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "py" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const { pyterminal_read, pyterminal_write } = sync;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
pyterminal_write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => pyterminal_read(data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
|
||||||
|
await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const readline = new Readline();
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = (options) => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(readline);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
terminal.paste(`${line}`);
|
||||||
|
terminal.write("\r\n");
|
||||||
|
do {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 0),
|
||||||
|
);
|
||||||
|
} while (!readline.activeRead?.resolve);
|
||||||
|
readline.activeRead.resolve(line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
init({
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
xworker.sync.is_pyterminal = () => true;
|
||||||
|
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||||
|
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
|
||||||
|
// @see https://github.com/pyscript/pyscript/issues/2246
|
||||||
|
const patchInput = [
|
||||||
|
"import builtins as _b",
|
||||||
|
"from pyscript import sync as _s",
|
||||||
|
"_b.input = _s.pyterminal_read",
|
||||||
|
"del _b",
|
||||||
|
"del _s",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
hooks.worker.codeBeforeRun.add(patchInput);
|
||||||
|
hooks.worker.codeBeforeRunAsync.add(patchInput);
|
||||||
|
} else {
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "py") return;
|
||||||
|
|
||||||
|
console.warn("py-terminal is read only on main thread");
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
// on main, it's easy to trash and clean the current terminal
|
||||||
|
globalThis.__py_terminal__ = init({
|
||||||
|
disableStdin: true,
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: "underline",
|
||||||
|
});
|
||||||
|
run("from js import __py_terminal__ as __terminal__");
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
readline.write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
readline.write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => readline.read(data),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
1
core/src/service-worker.js
Normal file
1
core/src/service-worker.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "polyscript/service-worker";
|
||||||
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");
|
||||||
19
core/src/stdlib/pyscript.js
Normal file
19
core/src/stdlib/pyscript.js
Normal file
File diff suppressed because one or more lines are too long
120
core/src/stdlib/pyscript/__init__.py
Normal file
120
core/src/stdlib/pyscript/__init__.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
This is the main `pyscript` namespace. It provides the primary Pythonic API
|
||||||
|
for users to interact with the
|
||||||
|
[browser's own API](https://developer.mozilla.org/en-US/docs/Web/API). It
|
||||||
|
includes utilities for common activities such as displaying content, handling
|
||||||
|
events, fetching resources, managing local storage, and coordinating with
|
||||||
|
web workers.
|
||||||
|
|
||||||
|
The most important names provided by this namespace can be directly imported
|
||||||
|
from `pyscript`, for example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import display, HTML, fetch, when, storage, WebSocket
|
||||||
|
```
|
||||||
|
|
||||||
|
The following names are available in the `pyscript` namespace:
|
||||||
|
|
||||||
|
- `RUNNING_IN_WORKER`: Boolean indicating if the code is running in a Web
|
||||||
|
Worker.
|
||||||
|
- `PyWorker`: Class for creating Web Workers running Python code.
|
||||||
|
- `config`: Configuration object for pyscript settings.
|
||||||
|
- `current_target`: The element in the DOM that is the current target for
|
||||||
|
output.
|
||||||
|
- `document`: The standard `document` object, proxied in workers.
|
||||||
|
- `window`: The standard `window` object, proxied in workers.
|
||||||
|
- `js_import`: Function to dynamically import JS modules.
|
||||||
|
- `js_modules`: Object containing JS modules available to Python.
|
||||||
|
- `sync`: Utility for synchronizing between worker and main thread.
|
||||||
|
- `display`: Function to render Python objects in the web page.
|
||||||
|
- `HTML`: Helper class to create HTML content for display.
|
||||||
|
- `fetch`: Function to perform HTTP requests.
|
||||||
|
- `Storage`: Class representing browser storage (local/session).
|
||||||
|
- `storage`: Object to interact with browser's local storage.
|
||||||
|
- `WebSocket`: Class to create and manage WebSocket connections.
|
||||||
|
- `when`: Function to register event handlers on DOM elements.
|
||||||
|
- `Event`: Class representing user defined or DOM events.
|
||||||
|
- `py_import`: Function to lazily import Pyodide related Python modules.
|
||||||
|
|
||||||
|
If running in the main thread, the following additional names are available:
|
||||||
|
|
||||||
|
- `create_named_worker`: Function to create a named Web Worker.
|
||||||
|
- `workers`: Object to manage and interact with existing Web Workers.
|
||||||
|
|
||||||
|
All of these names are defined in the various submodules of `pyscript` and
|
||||||
|
are imported and re-exported here for convenience. Please refer to the
|
||||||
|
respective submodule documentation for more details on each component.
|
||||||
|
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Some notes about the naming conventions and the relationship between
|
||||||
|
various similar-but-different names found within this code base.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pyscript
|
||||||
|
```
|
||||||
|
|
||||||
|
The `pyscript` package contains the main user-facing API offered by
|
||||||
|
PyScript. All the names which are supposed be used by end users should
|
||||||
|
be made available in `pyscript/__init__.py` (i.e., this source file).
|
||||||
|
|
||||||
|
```python
|
||||||
|
import _pyscript
|
||||||
|
```
|
||||||
|
|
||||||
|
The `_pyscript` module is an internal API implemented in JS. **End users
|
||||||
|
should not use it directly**. For its implementation, grep for
|
||||||
|
`interpreter.registerJsModule("_pyscript",...)` in `core.js`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import js
|
||||||
|
```
|
||||||
|
|
||||||
|
The `js` object is
|
||||||
|
[the JS `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis),
|
||||||
|
as exported by Pyodide and/or Micropython's foreign function interface
|
||||||
|
(FFI). As such, it contains different things in the main thread or in a
|
||||||
|
worker, as defined by web standards.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pyscript.context
|
||||||
|
```
|
||||||
|
|
||||||
|
The `context` submodule abstracts away some of the differences between
|
||||||
|
the main thread and a worker. Its most important features are made
|
||||||
|
available in the root `pyscript` namespace. All other functionality is
|
||||||
|
mostly for internal PyScript use or advanced users. In particular, it
|
||||||
|
defines `window` and `document` in such a way that these names work in
|
||||||
|
both cases: in the main thread, they are the "real" objects, in a worker
|
||||||
|
they are proxies which work thanks to
|
||||||
|
[coincident](https://github.com/WebReflection/coincident).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import window, document
|
||||||
|
```
|
||||||
|
|
||||||
|
These are just the `window` and `document` objects as defined by
|
||||||
|
`pyscript.context`. This is the blessed way to access them from `pyscript`,
|
||||||
|
as it works transparently in both the main thread and worker cases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from polyscript import lazy_py_modules as py_import
|
||||||
|
from pyscript.context import (
|
||||||
|
RUNNING_IN_WORKER,
|
||||||
|
PyWorker,
|
||||||
|
config,
|
||||||
|
current_target,
|
||||||
|
document,
|
||||||
|
js_import,
|
||||||
|
js_modules,
|
||||||
|
sync,
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
|
from pyscript.storage import Storage, storage
|
||||||
|
from pyscript.websocket import WebSocket
|
||||||
|
from pyscript.events import when, Event
|
||||||
|
|
||||||
|
if not RUNNING_IN_WORKER:
|
||||||
|
from pyscript.workers import create_named_worker, workers
|
||||||
198
core/src/stdlib/pyscript/context.py
Normal file
198
core/src/stdlib/pyscript/context.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
Execution context management for PyScript.
|
||||||
|
|
||||||
|
This module handles the differences between running in the
|
||||||
|
[main browser thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread)
|
||||||
|
versus running in a
|
||||||
|
[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers),
|
||||||
|
providing a consistent API regardless of the execution context.
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
|
||||||
|
- Detects whether code is running in a worker or main thread. Read this via
|
||||||
|
the boolean `pyscript.context.RUNNING_IN_WORKER`.
|
||||||
|
- Parses and normalizes configuration from `polyscript.config` and adds the
|
||||||
|
Python interpreter type via the `type` key in `pyscript.context.config`.
|
||||||
|
- Provides appropriate implementations of `window`, `document`, and `sync`.
|
||||||
|
- Sets up JavaScript module import system, including a lazy `js_import`
|
||||||
|
function.
|
||||||
|
- Manages `PyWorker` creation.
|
||||||
|
- Provides access to the current display target via
|
||||||
|
`pyscript.context.display_target`.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
These are key differences between the main thread and worker contexts:
|
||||||
|
|
||||||
|
Main thread context:
|
||||||
|
|
||||||
|
- `window` and `document` are available directly.
|
||||||
|
- `PyWorker` can be created to spawn worker threads.
|
||||||
|
- `sync` is not available (raises `NotSupported`).
|
||||||
|
|
||||||
|
Worker context:
|
||||||
|
|
||||||
|
- `window` and `document` are proxied from main thread (if SharedArrayBuffer
|
||||||
|
available).
|
||||||
|
- `PyWorker` is not available (raises `NotSupported`).
|
||||||
|
- `sync` utilities are available for main thread communication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import js
|
||||||
|
from polyscript import config as _polyscript_config
|
||||||
|
from polyscript import js_modules
|
||||||
|
from pyscript.util import NotSupported
|
||||||
|
|
||||||
|
RUNNING_IN_WORKER = not hasattr(js, "document")
|
||||||
|
"""Detect execution context: True if running in a worker, False if main thread."""
|
||||||
|
|
||||||
|
config = json.loads(js.JSON.stringify(_polyscript_config))
|
||||||
|
"""Parsed and normalized configuration."""
|
||||||
|
if isinstance(config, str):
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
js_import = None
|
||||||
|
"""Function to import JavaScript modules dynamically."""
|
||||||
|
|
||||||
|
window = None
|
||||||
|
"""The `window` object (proxied if in a worker)."""
|
||||||
|
|
||||||
|
document = None
|
||||||
|
"""The `document` object (proxied if in a worker)."""
|
||||||
|
|
||||||
|
sync = None
|
||||||
|
"""Sync utilities for worker-main thread communication (only in workers)."""
|
||||||
|
|
||||||
|
# Detect and add Python interpreter type to config.
|
||||||
|
if "MicroPython" in sys.version:
|
||||||
|
config["type"] = "mpy"
|
||||||
|
else:
|
||||||
|
config["type"] = "py"
|
||||||
|
|
||||||
|
|
||||||
|
class _JSModuleProxy:
|
||||||
|
"""
|
||||||
|
Proxy for JavaScript modules imported via js_modules.
|
||||||
|
|
||||||
|
This allows Python code to import JavaScript modules using Python's
|
||||||
|
import syntax:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript.js_modules lodash import debounce
|
||||||
|
```
|
||||||
|
|
||||||
|
The proxy lazily retrieves the actual JavaScript module when accessed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
"""
|
||||||
|
Create a proxy for the named JavaScript module.
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __getattr__(self, field):
|
||||||
|
"""
|
||||||
|
Retrieve a JavaScript object/function from the proxied JavaScript
|
||||||
|
module via the given `field` name.
|
||||||
|
"""
|
||||||
|
# Avoid Pyodide looking for non-existent special methods.
|
||||||
|
if not field.startswith("_"):
|
||||||
|
return getattr(getattr(js_modules, self.name), field)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Register all available JavaScript modules in Python's module system.
|
||||||
|
# This enables: from pyscript.js_modules.xxx import yyy
|
||||||
|
for module_name in js.Reflect.ownKeys(js_modules):
|
||||||
|
sys.modules[f"pyscript.js_modules.{module_name}"] = _JSModuleProxy(module_name)
|
||||||
|
sys.modules["pyscript.js_modules"] = js_modules
|
||||||
|
|
||||||
|
|
||||||
|
# Context-specific setup: Worker vs Main Thread.
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
import polyscript
|
||||||
|
|
||||||
|
# PyWorker cannot be created from within a worker.
|
||||||
|
PyWorker = NotSupported(
|
||||||
|
"pyscript.PyWorker",
|
||||||
|
"pyscript.PyWorker works only when running in the main thread",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to access main thread's window and document via SharedArrayBuffer.
|
||||||
|
try:
|
||||||
|
window = polyscript.xworker.window
|
||||||
|
document = window.document
|
||||||
|
js.document = document
|
||||||
|
|
||||||
|
# Create js_import function that runs imports on the main thread.
|
||||||
|
js_import = window.Function(
|
||||||
|
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||||
|
)()
|
||||||
|
|
||||||
|
except:
|
||||||
|
# SharedArrayBuffer not available - window/document cannot be proxied.
|
||||||
|
sab_error_message = (
|
||||||
|
"Unable to use `window` or `document` in worker. "
|
||||||
|
"This requires SharedArrayBuffer support. "
|
||||||
|
"See: https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
|
||||||
|
)
|
||||||
|
js.console.warn(sab_error_message)
|
||||||
|
window = NotSupported("pyscript.window", sab_error_message)
|
||||||
|
document = NotSupported("pyscript.document", sab_error_message)
|
||||||
|
|
||||||
|
# Worker-specific utilities for main thread communication.
|
||||||
|
sync = polyscript.xworker.sync
|
||||||
|
|
||||||
|
def current_target():
|
||||||
|
"""
|
||||||
|
Get the current output target in worker context.
|
||||||
|
"""
|
||||||
|
return polyscript.target
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Main thread context setup.
|
||||||
|
import _pyscript
|
||||||
|
from _pyscript import PyWorker as _PyWorker
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
|
||||||
|
js_import = _pyscript.js_import
|
||||||
|
|
||||||
|
def PyWorker(url, **options):
|
||||||
|
"""
|
||||||
|
Create a Web Worker running Python code.
|
||||||
|
|
||||||
|
This spawns a new worker thread that can execute Python code
|
||||||
|
found at the `url`, independently of the main thread. The
|
||||||
|
`**options` can be used to configure the worker.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import PyWorker
|
||||||
|
|
||||||
|
|
||||||
|
# Create a worker to run background tasks.
|
||||||
|
# (`type` MUST be either `micropython` or `pyodide`)
|
||||||
|
worker = PyWorker("./worker.py", type="micropython")
|
||||||
|
```
|
||||||
|
|
||||||
|
PyWorker **can only be created from the main thread**, not from
|
||||||
|
within another worker.
|
||||||
|
"""
|
||||||
|
return _PyWorker(url, to_js(options))
|
||||||
|
|
||||||
|
# Main thread has direct access to window and document.
|
||||||
|
window = js
|
||||||
|
document = js.document
|
||||||
|
|
||||||
|
# sync is not available in main thread (only in workers).
|
||||||
|
sync = NotSupported(
|
||||||
|
"pyscript.sync", "pyscript.sync works only when running in a worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
def current_target():
|
||||||
|
"""
|
||||||
|
Get the current output target in main thread context.
|
||||||
|
"""
|
||||||
|
return _pyscript.target
|
||||||
263
core/src/stdlib/pyscript/display.py
Normal file
263
core/src/stdlib/pyscript/display.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
"""
|
||||||
|
Display Pythonic content in the browser.
|
||||||
|
|
||||||
|
This module provides the `display()` function for rendering Python objects
|
||||||
|
in the web page. The function introspects objects to determine the appropriate
|
||||||
|
[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types)
|
||||||
|
and rendering method.
|
||||||
|
|
||||||
|
Supported MIME types:
|
||||||
|
|
||||||
|
- `text/plain`: Plain text (HTML-escaped)
|
||||||
|
- `text/html`: HTML content
|
||||||
|
- `image/png`: PNG images as data URLs
|
||||||
|
- `image/jpeg`: JPEG images as data URLs
|
||||||
|
- `image/svg+xml`: SVG graphics
|
||||||
|
- `application/json`: JSON data
|
||||||
|
- `application/javascript`: JavaScript code (discouraged)
|
||||||
|
|
||||||
|
The `display()` function uses standard Python representation methods
|
||||||
|
(`_repr_html_`, `_repr_png_`, etc.) to determine how to render objects.
|
||||||
|
Objects can provide a `_repr_mimebundle_` method to specify preferred formats
|
||||||
|
like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _repr_mimebundle_(self):
|
||||||
|
return {
|
||||||
|
"text/html": "<b>Bold HTML</b>",
|
||||||
|
"image/png": "<base64-encoded-png-data>",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Heavily inspired by
|
||||||
|
[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import html
|
||||||
|
import io
|
||||||
|
from collections import OrderedDict
|
||||||
|
from pyscript.context import current_target, document, window
|
||||||
|
from pyscript.ffi import is_none
|
||||||
|
|
||||||
|
|
||||||
|
def _render_image(mime, value, meta):
|
||||||
|
"""
|
||||||
|
Render image (`mime`) data (`value`) as an HTML img element with data URL.
|
||||||
|
Any `meta` attributes are added to the img tag.
|
||||||
|
|
||||||
|
Accepts both raw bytes and base64-encoded strings for flexibility. This
|
||||||
|
only handles PNG and JPEG images. SVG images are handled separately as
|
||||||
|
their raw XML content (which the browser can render directly).
|
||||||
|
"""
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = base64.b64encode(value).decode("utf-8")
|
||||||
|
attrs = "".join([f' {k}="{v}"' for k, v in meta.items()])
|
||||||
|
return f'<img src="data:{mime};base64,{value}"{attrs}>'
|
||||||
|
|
||||||
|
|
||||||
|
# Maps MIME types to rendering functions.
|
||||||
|
_MIME_TO_RENDERERS = {
|
||||||
|
"text/plain": lambda v, m: html.escape(v),
|
||||||
|
"text/html": lambda v, m: v,
|
||||||
|
"image/png": lambda v, m: _render_image("image/png", v, m),
|
||||||
|
"image/jpeg": lambda v, m: _render_image("image/jpeg", v, m),
|
||||||
|
"image/svg+xml": lambda v, m: v,
|
||||||
|
"application/json": lambda v, m: v,
|
||||||
|
"application/javascript": lambda v, m: f"<script>{v}<\\/script>",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Maps Python representation methods to MIME types. This is an ordered dict
|
||||||
|
# because the order defines preference when multiple methods are available,
|
||||||
|
# and MicroPython's limited dicts don't preserve insertion order.
|
||||||
|
_METHOD_TO_MIME = OrderedDict(
|
||||||
|
[
|
||||||
|
("savefig", "image/png"),
|
||||||
|
("_repr_png_", "image/png"),
|
||||||
|
("_repr_jpeg_", "image/jpeg"),
|
||||||
|
("_repr_svg_", "image/svg+xml"),
|
||||||
|
("_repr_html_", "text/html"),
|
||||||
|
("_repr_json_", "application/json"),
|
||||||
|
("_repr_javascript_", "application/javascript"),
|
||||||
|
("__repr__", "text/plain"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HTML:
|
||||||
|
"""
|
||||||
|
Wrap a string to render as unescaped HTML in `display()`. This is
|
||||||
|
necessary because plain strings are automatically HTML-escaped for safety:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import HTML, display
|
||||||
|
|
||||||
|
|
||||||
|
display(HTML("<h1>Hello World</h1>"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspired by
|
||||||
|
[`IPython.display.HTML`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.HTML).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, html):
|
||||||
|
self._html = html
|
||||||
|
|
||||||
|
def _repr_html_(self):
|
||||||
|
return self._html
|
||||||
|
|
||||||
|
|
||||||
|
def _get_representation(obj, method):
|
||||||
|
"""
|
||||||
|
Call the given representation `method` on an object (`obj`).
|
||||||
|
|
||||||
|
Handles special cases like matplotlib's `savefig`. Returns `None`
|
||||||
|
if the `method` doesn't exist.
|
||||||
|
"""
|
||||||
|
if method == "__repr__":
|
||||||
|
return repr(obj)
|
||||||
|
if not hasattr(obj, method):
|
||||||
|
return None
|
||||||
|
if method == "savefig":
|
||||||
|
buf = io.BytesIO()
|
||||||
|
obj.savefig(buf, format="png")
|
||||||
|
buf.seek(0)
|
||||||
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
|
return getattr(obj, method)()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_content_and_mime(obj):
|
||||||
|
"""
|
||||||
|
Returns the formatted raw content to be inserted into the DOM representing
|
||||||
|
the given object, along with the object's detected MIME type.
|
||||||
|
|
||||||
|
Returns a tuple of (html_string, mime_type).
|
||||||
|
|
||||||
|
Prefers _repr_mimebundle_ if available, otherwise tries individual
|
||||||
|
representation methods, falling back to __repr__ (with a warning in
|
||||||
|
the console).
|
||||||
|
|
||||||
|
Implements a subset of IPython's rich display system (mimebundle support,
|
||||||
|
etc...).
|
||||||
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return html.escape(obj), "text/plain"
|
||||||
|
# Prefer an object's mimebundle.
|
||||||
|
mimebundle = _get_representation(obj, "_repr_mimebundle_")
|
||||||
|
if mimebundle:
|
||||||
|
if isinstance(mimebundle, tuple):
|
||||||
|
# Grab global metadata.
|
||||||
|
format_dict, global_meta = mimebundle
|
||||||
|
else:
|
||||||
|
format_dict, global_meta = mimebundle, {}
|
||||||
|
# Try to render using mimebundle formats.
|
||||||
|
for mime_type, output in format_dict.items():
|
||||||
|
if mime_type in _MIME_TO_RENDERERS:
|
||||||
|
meta = global_meta.get(mime_type, {})
|
||||||
|
# If output is a tuple, merge format-specific metadata.
|
||||||
|
if isinstance(output, tuple):
|
||||||
|
output, format_meta = output
|
||||||
|
meta.update(format_meta)
|
||||||
|
return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
|
||||||
|
# No mimebundle or no available renderers therein, so try individual
|
||||||
|
# methods.
|
||||||
|
for method, mime_type in _METHOD_TO_MIME.items():
|
||||||
|
if mime_type not in _MIME_TO_RENDERERS:
|
||||||
|
continue
|
||||||
|
output = _get_representation(obj, method)
|
||||||
|
if output is None:
|
||||||
|
continue
|
||||||
|
meta = {}
|
||||||
|
if isinstance(output, tuple):
|
||||||
|
output, meta = output
|
||||||
|
return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
|
||||||
|
# Ultimate fallback to repr with warning.
|
||||||
|
window.console.warn(
|
||||||
|
f"Object {type(obj).__name__} has no supported representation method. "
|
||||||
|
"Using __repr__ as fallback."
|
||||||
|
)
|
||||||
|
output = repr(obj)
|
||||||
|
return html.escape(output), "text/plain"
|
||||||
|
|
||||||
|
|
||||||
|
def _write_to_dom(element, value, append):
|
||||||
|
"""
|
||||||
|
Given an `element` and a `value`, write formatted content to the referenced
|
||||||
|
DOM element. If `append` is True, content is added to the existing content;
|
||||||
|
otherwise, the existing content is replaced.
|
||||||
|
|
||||||
|
Creates a wrapper `div` when appending multiple items to preserve
|
||||||
|
structure.
|
||||||
|
"""
|
||||||
|
html_content, mime_type = _get_content_and_mime(value)
|
||||||
|
if not html_content.strip():
|
||||||
|
return
|
||||||
|
if append:
|
||||||
|
container = document.createElement("div")
|
||||||
|
element.append(container)
|
||||||
|
else:
|
||||||
|
container = element
|
||||||
|
if mime_type in ("application/javascript", "text/html"):
|
||||||
|
container.append(document.createRange().createContextualFragment(html_content))
|
||||||
|
else:
|
||||||
|
container.innerHTML = html_content
|
||||||
|
|
||||||
|
|
||||||
|
def display(*values, target=None, append=True):
|
||||||
|
"""
|
||||||
|
Display Python objects in the web page.
|
||||||
|
|
||||||
|
* `*values`: Python objects to display. Each object is introspected to
|
||||||
|
determine the appropriate rendering method.
|
||||||
|
* `target`: DOM element ID where content should be displayed. If `None`
|
||||||
|
(default), uses the current script tag's designated output area. This
|
||||||
|
can start with '#' (which will be stripped for compatibility).
|
||||||
|
* `append`: If `True` (default), add content to existing output. If
|
||||||
|
`False`, replace existing content before displaying.
|
||||||
|
|
||||||
|
When used in a worker, `display()` requires an explicit `target` parameter
|
||||||
|
to identify where content will be displayed. If used on the main thread,
|
||||||
|
it automatically uses the current `<script>` tag as the target. If the
|
||||||
|
script tag has a `target` attribute, that element will be used instead.
|
||||||
|
|
||||||
|
A ValueError is raised if a valid target cannot be found for the current
|
||||||
|
context.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import display, HTML
|
||||||
|
|
||||||
|
|
||||||
|
# Display raw HTML.
|
||||||
|
display(HTML("<h1>Hello, World!</h1>"))
|
||||||
|
|
||||||
|
# Display in current script's output area.
|
||||||
|
display("Hello, World!")
|
||||||
|
|
||||||
|
# Display in a specific element.
|
||||||
|
display("Hello", target="my-div")
|
||||||
|
|
||||||
|
# Replace existing content (note the `#`).
|
||||||
|
display("New content", target="#my-div", append=False)
|
||||||
|
|
||||||
|
# Display multiple values in the default target.
|
||||||
|
display("First", "Second", "Third")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if isinstance(target, str):
|
||||||
|
# There's a valid target.
|
||||||
|
target = target[1:] if target.startswith("#") else target
|
||||||
|
elif is_none(target):
|
||||||
|
target = current_target()
|
||||||
|
element = document.getElementById(target)
|
||||||
|
if is_none(element):
|
||||||
|
raise ValueError(f"Cannot find element with id='{target}' in the page.")
|
||||||
|
# If possible, use a script tag's target attribute.
|
||||||
|
if element.tagName == "SCRIPT" and hasattr(element, "target"):
|
||||||
|
element = element.target
|
||||||
|
# Clear before displaying all values when not appending.
|
||||||
|
if not append:
|
||||||
|
element.replaceChildren()
|
||||||
|
# Add each value.
|
||||||
|
for value in values:
|
||||||
|
_write_to_dom(element, value, append)
|
||||||
237
core/src/stdlib/pyscript/events.py
Normal file
237
core/src/stdlib/pyscript/events.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
"""
|
||||||
|
Event handling for PyScript.
|
||||||
|
|
||||||
|
This module provides two complementary systems:
|
||||||
|
|
||||||
|
1. The `Event` class: A simple publish-subscribe pattern for custom events
|
||||||
|
within *your* Python code.
|
||||||
|
|
||||||
|
2. The `@when` decorator: Connects Python functions to browser DOM events,
|
||||||
|
or instances of the `Event` class, allowing you to respond to user
|
||||||
|
interactions like clicks, key presses and form submissions, or to custom
|
||||||
|
events defined in your Python code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
from functools import wraps
|
||||||
|
from pyscript.context import document
|
||||||
|
from pyscript.ffi import create_proxy, to_js
|
||||||
|
from pyscript.util import is_awaitable
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
"""
|
||||||
|
A custom event that can notify multiple listeners when triggered.
|
||||||
|
|
||||||
|
Use this class to create your own event system within Python code.
|
||||||
|
Listeners can be either regular functions or async functions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript.events import Event
|
||||||
|
|
||||||
|
# Create a custom event.
|
||||||
|
data_loaded = Event()
|
||||||
|
|
||||||
|
# Add a listener.
|
||||||
|
def on_data_loaded(result):
|
||||||
|
print(f"Data loaded: {result}")
|
||||||
|
|
||||||
|
data_loaded.add_listener(on_data_loaded)
|
||||||
|
|
||||||
|
# Time passes.... trigger the event.
|
||||||
|
data_loaded.trigger({"data": 123})
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
def trigger(self, result):
|
||||||
|
"""
|
||||||
|
Trigger the event and notify all listeners with the given `result`.
|
||||||
|
"""
|
||||||
|
for listener in self._listeners:
|
||||||
|
if is_awaitable(listener):
|
||||||
|
asyncio.create_task(listener(result))
|
||||||
|
else:
|
||||||
|
listener(result)
|
||||||
|
|
||||||
|
def add_listener(self, listener):
|
||||||
|
"""
|
||||||
|
Add a function to be called when this event is triggered.
|
||||||
|
|
||||||
|
The `listener` must be callable. It can be either a regular function
|
||||||
|
or an async function. Duplicate listeners are ignored.
|
||||||
|
"""
|
||||||
|
if not callable(listener):
|
||||||
|
msg = "Listener must be callable."
|
||||||
|
raise ValueError(msg)
|
||||||
|
if listener not in self._listeners:
|
||||||
|
self._listeners.append(listener)
|
||||||
|
|
||||||
|
def remove_listener(self, *listeners):
|
||||||
|
"""
|
||||||
|
Remove specified `listeners`. If none specified, remove all listeners.
|
||||||
|
"""
|
||||||
|
if listeners:
|
||||||
|
for listener in listeners:
|
||||||
|
try:
|
||||||
|
self._listeners.remove(listener)
|
||||||
|
except ValueError:
|
||||||
|
pass # Silently ignore listeners not in the list.
|
||||||
|
else:
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
|
||||||
|
def when(event_type, selector=None, **options):
|
||||||
|
"""
|
||||||
|
A decorator to handle DOM events or custom `Event` objects.
|
||||||
|
|
||||||
|
For DOM events, specify the `event_type` (e.g. `"click"`) and a `selector`
|
||||||
|
for target elements. For custom `Event` objects, just pass the `Event`
|
||||||
|
instance as the `event_type`. It's also possible to pass a list of `Event`
|
||||||
|
objects. The `selector` is required only for DOM events. It should be a
|
||||||
|
[CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors),
|
||||||
|
`Element`, `ElementCollection`, or list of DOM elements.
|
||||||
|
|
||||||
|
For DOM events only, you can specify optional
|
||||||
|
[addEventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options):
|
||||||
|
`capture`, `once`, `passive`, or `signal`.
|
||||||
|
|
||||||
|
The decorated function can be either a regular function or an async
|
||||||
|
function. If the function accepts an argument, it will receive the event
|
||||||
|
object (for DOM events) or the Event's result (for custom events). A
|
||||||
|
function does not need to accept any arguments if it doesn't require them.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import when, display
|
||||||
|
|
||||||
|
# Handle DOM events.
|
||||||
|
@when("click", "#my-button")
|
||||||
|
def handle_click(event):
|
||||||
|
display("Button clicked!")
|
||||||
|
|
||||||
|
# Handle DOM events with options.
|
||||||
|
@when("click", "#my-button", once=True)
|
||||||
|
def handle_click_once(event):
|
||||||
|
display("Button clicked once!")
|
||||||
|
|
||||||
|
# Handle custom events.
|
||||||
|
my_event = Event()
|
||||||
|
|
||||||
|
@when(my_event)
|
||||||
|
def handle_custom(): # No event argument needed.
|
||||||
|
display("Custom event triggered!")
|
||||||
|
|
||||||
|
# Handle multiple custom events.
|
||||||
|
another_event = Event()
|
||||||
|
|
||||||
|
def another_handler():
|
||||||
|
display("Another custom event handler.")
|
||||||
|
|
||||||
|
# Attach the same handler to multiple events but not as a decorator.
|
||||||
|
when([my_event, another_event])(another_handler)
|
||||||
|
|
||||||
|
# Trigger an Event instance from a DOM event via @when.
|
||||||
|
@when("click", "#my-button")
|
||||||
|
def handle_click(event):
|
||||||
|
another_event.trigger("Button clicked!")
|
||||||
|
|
||||||
|
# Stacked decorators also work.
|
||||||
|
@when("mouseover", "#my-div")
|
||||||
|
@when(my_event)
|
||||||
|
def handle_both(event):
|
||||||
|
display("Either mouseover or custom event triggered!")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if isinstance(event_type, str):
|
||||||
|
# This is a DOM event to handle, so check and use the selector.
|
||||||
|
if not selector:
|
||||||
|
raise ValueError("Selector required for DOM event handling.")
|
||||||
|
elements = _get_elements(selector)
|
||||||
|
if not elements:
|
||||||
|
raise ValueError(f"No elements found for selector: {selector}")
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
wrapper = _create_wrapper(func)
|
||||||
|
if isinstance(event_type, Event):
|
||||||
|
# Custom Event - add listener.
|
||||||
|
event_type.add_listener(wrapper)
|
||||||
|
elif isinstance(event_type, list) and all(
|
||||||
|
isinstance(t, Event) for t in event_type
|
||||||
|
):
|
||||||
|
# List of custom Events - add listener to each.
|
||||||
|
for event in event_type:
|
||||||
|
event.add_listener(wrapper)
|
||||||
|
else:
|
||||||
|
# DOM event - attach to all matched elements.
|
||||||
|
for element in elements:
|
||||||
|
element.addEventListener(
|
||||||
|
event_type,
|
||||||
|
create_proxy(wrapper),
|
||||||
|
to_js(options) if options else False,
|
||||||
|
)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _get_elements(selector):
|
||||||
|
"""
|
||||||
|
Convert various `selector` types into a list of DOM elements.
|
||||||
|
"""
|
||||||
|
from pyscript.web import Element, ElementCollection
|
||||||
|
|
||||||
|
if isinstance(selector, str):
|
||||||
|
return list(document.querySelectorAll(selector))
|
||||||
|
elif isinstance(selector, Element):
|
||||||
|
return [selector._dom_element]
|
||||||
|
elif isinstance(selector, ElementCollection):
|
||||||
|
return [el._dom_element for el in selector]
|
||||||
|
elif isinstance(selector, list):
|
||||||
|
return selector
|
||||||
|
else:
|
||||||
|
return [selector]
|
||||||
|
|
||||||
|
|
||||||
|
def _create_wrapper(func):
|
||||||
|
"""
|
||||||
|
Create an appropriate wrapper for the given function, `func`.
|
||||||
|
|
||||||
|
The wrapper handles both sync and async functions, and respects whether
|
||||||
|
the function expects to receive event arguments.
|
||||||
|
"""
|
||||||
|
# Get the original function if it's been wrapped. This avoids wrapper
|
||||||
|
# loops when stacking decorators.
|
||||||
|
original_func = func
|
||||||
|
while hasattr(original_func, "__wrapped__"):
|
||||||
|
original_func = original_func.__wrapped__
|
||||||
|
# Inspect the original function signature.
|
||||||
|
sig = inspect.signature(original_func)
|
||||||
|
accepts_args = bool(sig.parameters)
|
||||||
|
if is_awaitable(func):
|
||||||
|
if accepts_args:
|
||||||
|
|
||||||
|
async def wrapper(event):
|
||||||
|
return await func(event)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
if accepts_args:
|
||||||
|
# Always create a new wrapper function to avoid issues with
|
||||||
|
# stacked decorators getting into an infinite loop.
|
||||||
|
|
||||||
|
def wrapper(event):
|
||||||
|
return func(event)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func()
|
||||||
|
|
||||||
|
return wraps(func)(wrapper)
|
||||||
218
core/src/stdlib/pyscript/fetch.py
Normal file
218
core/src/stdlib/pyscript/fetch.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
"""
|
||||||
|
This module provides a Python-friendly interface to the
|
||||||
|
[browser's fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
|
||||||
|
returning native Python data types and supporting directly awaiting the promise
|
||||||
|
and chaining method calls directly on the promise.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript.fetch import fetch
|
||||||
|
url = "https://api.example.com/data"
|
||||||
|
|
||||||
|
# Pattern 1: Await the response, then extract data.
|
||||||
|
response = await fetch(url)
|
||||||
|
if response.ok:
|
||||||
|
data = await response.json()
|
||||||
|
else:
|
||||||
|
raise NetworkError(f"Fetch failed: {response.status}")
|
||||||
|
|
||||||
|
# Pattern 2: Chain method calls directly on the promise.
|
||||||
|
data = await fetch(url).json()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
|
||||||
|
class _FetchResponse:
|
||||||
|
"""
|
||||||
|
Wraps a JavaScript Response object with Pythonic data extraction methods.
|
||||||
|
|
||||||
|
This wrapper ensures that data returned from fetch is, if possible, in
|
||||||
|
native Python types rather than JavaScript types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, response):
|
||||||
|
self._response = response
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
"""
|
||||||
|
Provide access to underlying Response properties like ok, status, etc.
|
||||||
|
"""
|
||||||
|
return getattr(self._response, attr)
|
||||||
|
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
"""
|
||||||
|
Get response body as a buffer (memoryview or bytes).
|
||||||
|
|
||||||
|
Returns a memoryview in MicroPython or bytes in Pyodide, representing
|
||||||
|
the raw binary data.
|
||||||
|
"""
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
if hasattr(buffer, "to_py"):
|
||||||
|
# Pyodide conversion.
|
||||||
|
return buffer.to_py()
|
||||||
|
# MicroPython conversion.
|
||||||
|
return memoryview(as_bytearray(buffer))
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
"""
|
||||||
|
Get response body as a JavaScript Blob object.
|
||||||
|
|
||||||
|
Returns the raw JS Blob for use with other JS APIs.
|
||||||
|
"""
|
||||||
|
return await self._response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
"""
|
||||||
|
Get response body as a Python bytearray.
|
||||||
|
|
||||||
|
Returns a mutable bytearray containing the response data.
|
||||||
|
"""
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
return as_bytearray(buffer)
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
"""
|
||||||
|
Parse response body as JSON and return Python objects.
|
||||||
|
|
||||||
|
Returns native Python dicts, lists, strings, numbers, etc.
|
||||||
|
"""
|
||||||
|
return json.loads(await self.text())
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
"""
|
||||||
|
Get response body as a text string.
|
||||||
|
"""
|
||||||
|
return await self._response.text()
|
||||||
|
|
||||||
|
|
||||||
|
class _FetchPromise:
|
||||||
|
"""
|
||||||
|
Wraps the fetch promise to enable direct method chaining.
|
||||||
|
|
||||||
|
This allows calling response methods directly on the fetch promise:
|
||||||
|
`await fetch(url).json()` instead of requiring two separate awaits.
|
||||||
|
|
||||||
|
This feels more Pythonic since it matches typical usage patterns
|
||||||
|
Python developers have got used to via libraries like `requests`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, promise):
|
||||||
|
self._promise = promise
|
||||||
|
# To be resolved in the future via the setup() static method.
|
||||||
|
promise._response = None
|
||||||
|
# Add convenience methods directly to the promise.
|
||||||
|
promise.arrayBuffer = self.arrayBuffer
|
||||||
|
promise.blob = self.blob
|
||||||
|
promise.bytearray = self.bytearray
|
||||||
|
promise.json = self.json
|
||||||
|
promise.text = self.text
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setup(promise, response):
|
||||||
|
"""
|
||||||
|
Store the resolved response on the promise for later access.
|
||||||
|
"""
|
||||||
|
promise._response = _FetchResponse(response)
|
||||||
|
return promise._response
|
||||||
|
|
||||||
|
async def _get_response(self):
|
||||||
|
"""
|
||||||
|
Get the cached response, or await the promise if not yet resolved.
|
||||||
|
"""
|
||||||
|
if not self._promise._response:
|
||||||
|
await self._promise
|
||||||
|
return self._promise._response
|
||||||
|
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
response = await self._get_response()
|
||||||
|
return await response.arrayBuffer()
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
response = await self._get_response()
|
||||||
|
return await response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
response = await self._get_response()
|
||||||
|
return await response.bytearray()
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
response = await self._get_response()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
response = await self._get_response()
|
||||||
|
return await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, **options):
|
||||||
|
"""
|
||||||
|
Fetch a resource from the network using a Pythonic interface.
|
||||||
|
|
||||||
|
This wraps JavaScript's fetch API, returning Python-native data types
|
||||||
|
and supporting both direct promise awaiting and method chaining.
|
||||||
|
|
||||||
|
The function takes a `url` and optional fetch `options` as keyword
|
||||||
|
arguments. The `options` correspond to the JavaScript fetch API's
|
||||||
|
[RequestInit dictionary](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit),
|
||||||
|
and commonly include:
|
||||||
|
|
||||||
|
- `method`: HTTP method (e.g., `"GET"`, `"POST"`, `"PUT"` etc.)
|
||||||
|
- `headers`: Dict of request headers.
|
||||||
|
- `body`: Request body (string, dict for JSON, etc.)
|
||||||
|
|
||||||
|
The function returns a promise that resolves to a Response-like object
|
||||||
|
with Pythonic methods to extract data:
|
||||||
|
|
||||||
|
- `await response.json()` to get JSON as Python objects.
|
||||||
|
- `await response.text()` to get text data.
|
||||||
|
- `await response.bytearray()` to get raw data as a bytearray.
|
||||||
|
- `await response.arrayBuffer()` to get raw data as a memoryview or bytes.
|
||||||
|
- `await response.blob()` to get the raw JS Blob object.
|
||||||
|
|
||||||
|
It's also possible to chain these methods directly on the fetch promise:
|
||||||
|
`data = await fetch(url).json()`
|
||||||
|
|
||||||
|
The returned response object also exposes standard properties like
|
||||||
|
`ok`, `status`, and `statusText` for checking response status.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Simple GET request.
|
||||||
|
response = await fetch("https://api.example.com/data")
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
# Method chaining.
|
||||||
|
data = await fetch("https://api.example.com/data").json()
|
||||||
|
|
||||||
|
# POST request with JSON.
|
||||||
|
response = await fetch(
|
||||||
|
"https://api.example.com/users",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
body=json.dumps({"name": "Alice"})
|
||||||
|
)
|
||||||
|
result = await response.json()
|
||||||
|
|
||||||
|
# Check response status codes.
|
||||||
|
response = await fetch("https://api.example.com/data")
|
||||||
|
if response.ok:
|
||||||
|
# Status in the range 200-299.
|
||||||
|
data = await response.json()
|
||||||
|
elif response.status == 404:
|
||||||
|
print("Resource not found")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.status} {response.statusText}")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# Convert Python dict to JavaScript object.
|
||||||
|
js_options = js.JSON.parse(json.dumps(options))
|
||||||
|
|
||||||
|
# Setup response handler to wrap the result.
|
||||||
|
def on_response(response, *_):
|
||||||
|
return _FetchPromise.setup(promise, response)
|
||||||
|
|
||||||
|
promise = js.fetch(url, js_options).then(on_response)
|
||||||
|
_FetchPromise(promise)
|
||||||
|
return promise
|
||||||
164
core/src/stdlib/pyscript/ffi.py
Normal file
164
core/src/stdlib/pyscript/ffi.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""
|
||||||
|
This module provides a unified
|
||||||
|
[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface)
|
||||||
|
layer for Python/JavaScript interactions, that works consistently across both
|
||||||
|
Pyodide and MicroPython, and in a worker or main thread context, abstracting
|
||||||
|
away the differences in their JavaScript interop APIs.
|
||||||
|
|
||||||
|
The following utilities work on both the main thread and in worker contexts:
|
||||||
|
|
||||||
|
- `create_proxy`: Create a persistent JavaScript proxy of a Python function.
|
||||||
|
- `to_js`: Convert Python objects to JavaScript objects.
|
||||||
|
- `is_none`: Check if a value is Python `None` or JavaScript `null`.
|
||||||
|
- `assign`: Merge objects (like JavaScript's `Object.assign`).
|
||||||
|
|
||||||
|
The following utilities are specific to worker contexts:
|
||||||
|
|
||||||
|
- `direct`: Mark objects for direct JavaScript access.
|
||||||
|
- `gather`: Collect multiple values from worker contexts.
|
||||||
|
- `query`: Query objects in worker contexts.
|
||||||
|
|
||||||
|
More details of the `direct`, `gather`, and `query` utilities
|
||||||
|
[can be found here](https://github.com/WebReflection/reflected-ffi?tab=readme-ov-file#remote-extra-utilities).
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to import Pyodide's FFI utilities.
|
||||||
|
import js
|
||||||
|
from pyodide.ffi import create_proxy as _cp
|
||||||
|
from pyodide.ffi import to_js as _py_tjs
|
||||||
|
from pyodide.ffi import jsnull
|
||||||
|
|
||||||
|
from_entries = js.Object.fromEntries
|
||||||
|
|
||||||
|
def _to_js_wrapper(value, **kw):
|
||||||
|
if "dict_converter" not in kw:
|
||||||
|
kw["dict_converter"] = from_entries
|
||||||
|
return _py_tjs(value, **kw)
|
||||||
|
|
||||||
|
except:
|
||||||
|
# Fallback to jsffi for MicroPython.
|
||||||
|
from jsffi import create_proxy as _cp
|
||||||
|
from jsffi import to_js as _to_js_wrapper
|
||||||
|
import js
|
||||||
|
|
||||||
|
jsnull = js.Object.getPrototypeOf(js.Object.prototype)
|
||||||
|
|
||||||
|
|
||||||
|
def create_proxy(func):
|
||||||
|
"""
|
||||||
|
Create a persistent JavaScript proxy of a Python function.
|
||||||
|
|
||||||
|
This proxy allows JavaScript code to call the Python function
|
||||||
|
seamlessly, maintaining the correct context and argument handling.
|
||||||
|
|
||||||
|
This is especially useful when passing Python functions as callbacks
|
||||||
|
to JavaScript APIs (without `create_proxy`, the function would be
|
||||||
|
garbage collected after the declaration of the callback).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import ffi
|
||||||
|
from pyscript import document
|
||||||
|
|
||||||
|
my_button = document.getElementById("my-button")
|
||||||
|
|
||||||
|
def py_callback(x):
|
||||||
|
print(f"Callback called with {x}")
|
||||||
|
|
||||||
|
my_button.addEventListener("click", ffi.create_proxy(py_callback))
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return _cp(func)
|
||||||
|
|
||||||
|
|
||||||
|
def to_js(value, **kw):
|
||||||
|
"""
|
||||||
|
Convert Python objects to JavaScript objects.
|
||||||
|
|
||||||
|
This ensures a Python `dict` becomes a
|
||||||
|
[proper JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
|
||||||
|
rather a JavaScript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map),
|
||||||
|
which is more intuitive for most use cases.
|
||||||
|
|
||||||
|
Where required, the underlying `to_js` uses `Object.fromEntries` for
|
||||||
|
`dict` conversion.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import ffi
|
||||||
|
import js
|
||||||
|
|
||||||
|
|
||||||
|
note = {
|
||||||
|
"body": "This is a notification",
|
||||||
|
"icon": "icon.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
js.Notification.new("Hello!", ffi.to_js(note))
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return _to_js_wrapper(value, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def is_none(value):
|
||||||
|
"""
|
||||||
|
Check if a value is `None` or JavaScript `null`.
|
||||||
|
|
||||||
|
In Pyodide, JavaScript `null` is represented by the `jsnull` object,
|
||||||
|
so we check for both Python `None` and `jsnull`. This function ensures
|
||||||
|
consistent behavior across Pyodide and MicroPython for null-like
|
||||||
|
values.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import ffi
|
||||||
|
import js
|
||||||
|
|
||||||
|
|
||||||
|
val1 = None
|
||||||
|
val2 = js.null
|
||||||
|
val3 = 42
|
||||||
|
|
||||||
|
print(ffi.is_none(val1)) # True
|
||||||
|
print(ffi.is_none(val2)) # True
|
||||||
|
print(ffi.is_none(val3)) # False
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return value is None or value is jsnull
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Worker context utilities from reflected-ffi.
|
||||||
|
# See https://github.com/WebReflection/reflected-ffi for more details.
|
||||||
|
from polyscript import ffi as _ffi
|
||||||
|
|
||||||
|
_assign = _ffi.assign
|
||||||
|
|
||||||
|
direct = _ffi.direct
|
||||||
|
gather = _ffi.gather
|
||||||
|
query = _ffi.query
|
||||||
|
|
||||||
|
except:
|
||||||
|
# Fallback implementations for main thread context.
|
||||||
|
import js
|
||||||
|
|
||||||
|
_assign = js.Object.assign
|
||||||
|
|
||||||
|
direct = lambda source: source
|
||||||
|
|
||||||
|
|
||||||
|
def assign(source, *args):
|
||||||
|
"""
|
||||||
|
Merge JavaScript objects (like
|
||||||
|
[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)).
|
||||||
|
|
||||||
|
Takes a target object and merges properties from one or more source
|
||||||
|
objects into it, returning the modified target.
|
||||||
|
|
||||||
|
```python
|
||||||
|
obj = js.Object.new()
|
||||||
|
ffi.assign(obj, {"a": 1}, {"b": 2})
|
||||||
|
# obj now has properties a=1 and b=2
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
for arg in args:
|
||||||
|
_assign(source, to_js(arg))
|
||||||
|
return source
|
||||||
225
core/src/stdlib/pyscript/flatted.py
Normal file
225
core/src/stdlib/pyscript/flatted.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
"""
|
||||||
|
This module is a Python implementation of the
|
||||||
|
[Flatted JavaScript library](https://www.npmjs.com/package/flatted), which
|
||||||
|
provides a light and fast way to serialize and deserialize JSON structures
|
||||||
|
that contain circular references.
|
||||||
|
|
||||||
|
Standard JSON cannot handle circular references - attempting to serialize an
|
||||||
|
object that references itself will cause an error. Flatted solves this by
|
||||||
|
transforming circular structures into a flat array format that can be safely
|
||||||
|
serialized and later reconstructed.
|
||||||
|
|
||||||
|
Common use cases:
|
||||||
|
|
||||||
|
- Serializing complex object graphs with circular references.
|
||||||
|
- Working with DOM-like structures that contain parent/child references.
|
||||||
|
- Preserving object identity when serializing data structures.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import flatted
|
||||||
|
|
||||||
|
|
||||||
|
# Create a circular structure.
|
||||||
|
obj = {"name": "parent"}
|
||||||
|
obj["self"] = obj # Circular reference!
|
||||||
|
|
||||||
|
# Standard json.dumps would fail here.
|
||||||
|
serialized = flatted.stringify(obj)
|
||||||
|
|
||||||
|
# Reconstruct the original structure.
|
||||||
|
restored = flatted.parse(serialized)
|
||||||
|
assert restored["self"] is restored # Circular reference preserved!
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
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, 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 value not in known:
|
||||||
|
known.append(value)
|
||||||
|
value = _loop(_array_keys(value), input, known, value)
|
||||||
|
elif _is_object(value) and value not 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):
|
||||||
|
"""
|
||||||
|
Parse a Flatted JSON string and reconstruct the original structure.
|
||||||
|
|
||||||
|
This function takes a `value` containing a JSON string created by
|
||||||
|
Flatted's stringify() and reconstructs the original Python object,
|
||||||
|
including any circular references. The `*args` and `**kwargs` are passed
|
||||||
|
to json.loads() for additional customization.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import flatted
|
||||||
|
|
||||||
|
|
||||||
|
# Parse a Flatted JSON string.
|
||||||
|
json_string = '[{"name": "1", "self": "0"}, "parent"]'
|
||||||
|
obj = flatted.parse(json_string)
|
||||||
|
|
||||||
|
# Circular references are preserved.
|
||||||
|
assert obj["self"] is obj
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Serialize a Python object to a Flatted JSON string.
|
||||||
|
|
||||||
|
This function converts `value`, a Python object (including those with
|
||||||
|
circular references), into a JSON string that can be safely transmitted
|
||||||
|
or stored. The resulting string can be reconstructed using Flatted's
|
||||||
|
parse(). The `*args` and `**kwargs` are passed to json.dumps() for
|
||||||
|
additional customization.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import flatted
|
||||||
|
|
||||||
|
|
||||||
|
# Create an object with a circular reference.
|
||||||
|
parent = {"name": "parent", "children": []}
|
||||||
|
child = {"name": "child", "parent": parent}
|
||||||
|
parent["children"].append(child)
|
||||||
|
|
||||||
|
# Serialize it (standard json.dumps would fail here).
|
||||||
|
json_string = flatted.stringify(parent)
|
||||||
|
|
||||||
|
# Can optionally pretty-print via JSON indentation etc.
|
||||||
|
pretty = flatted.stringify(parent, indent=2)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
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)
|
||||||
258
core/src/stdlib/pyscript/fs.py
Normal file
258
core/src/stdlib/pyscript/fs.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
"""
|
||||||
|
This module provides an API for mounting directories from the user's local
|
||||||
|
filesystem into the browser's virtual filesystem. This means Python code,
|
||||||
|
running in the browser, can read and write files on the user's local machine.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
**This API only works in Chromium-based browsers** (Chrome, Edge,
|
||||||
|
Vivaldi, Brave, etc.) that support the
|
||||||
|
[File System Access API](https://wicg.github.io/file-system-access/).
|
||||||
|
|
||||||
|
The module maintains a `mounted` dictionary that tracks all currently mounted
|
||||||
|
paths and their associated filesystem handles.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import fs, document, when
|
||||||
|
|
||||||
|
|
||||||
|
# Mount a local directory to the `/local` mount point in the browser's
|
||||||
|
# virtual filesystem (may prompt user for permission).
|
||||||
|
await fs.mount("/local")
|
||||||
|
|
||||||
|
# Alternatively, mount on a button click event. This is important because
|
||||||
|
# if the call to `fs.mount` happens after a click or other transient event,
|
||||||
|
# the confirmation dialog will not be shown.
|
||||||
|
@when("click", "#mount-button")
|
||||||
|
async def handler(event):
|
||||||
|
await fs.mount("/another_dir")
|
||||||
|
|
||||||
|
# Work with files in the mounted directory as usual.
|
||||||
|
with open("/local/example.txt", "w") as f:
|
||||||
|
f.write("Hello from PyScript!")
|
||||||
|
|
||||||
|
# Ensure changes are written to local filesystem.
|
||||||
|
await fs.sync("/local")
|
||||||
|
|
||||||
|
# Clean up when done.
|
||||||
|
await fs.unmount("/local")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
import js
|
||||||
|
from _pyscript import fs as _fs, interpreter
|
||||||
|
from pyscript import window
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
from pyscript.context import RUNNING_IN_WORKER
|
||||||
|
|
||||||
|
# Worker-specific imports.
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
from pyscript.context import sync as sync_with_worker
|
||||||
|
from polyscript import IDBMap
|
||||||
|
|
||||||
|
mounted = {}
|
||||||
|
"""Global dictionary tracking mounted paths and their filesystem handles."""
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_permission(details):
|
||||||
|
"""
|
||||||
|
Check if permission has been granted for a filesystem handler. Returns
|
||||||
|
the handler if permission is granted, otherwise None.
|
||||||
|
"""
|
||||||
|
handler = details.handler
|
||||||
|
options = details.options
|
||||||
|
permission = await handler.queryPermission(options)
|
||||||
|
return handler if permission == "granted" else None
|
||||||
|
|
||||||
|
|
||||||
|
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
||||||
|
"""
|
||||||
|
Mount a directory from the local filesystem to the virtual filesystem
|
||||||
|
at the specified `path` mount point. The `mode` can be "readwrite" or
|
||||||
|
"read" to specify access level. The `root` parameter provides a hint
|
||||||
|
for the file picker starting location. The `id` parameter allows multiple
|
||||||
|
distinct mounts at the same path.
|
||||||
|
|
||||||
|
On first use, the browser will prompt the user to select a directory
|
||||||
|
and grant permission.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import fs
|
||||||
|
|
||||||
|
|
||||||
|
# Basic mount with default settings.
|
||||||
|
await fs.mount("/local")
|
||||||
|
|
||||||
|
# Mount with read-only access.
|
||||||
|
await fs.mount("/readonly", mode="read")
|
||||||
|
|
||||||
|
# Mount with a hint to start in Downloads folder.
|
||||||
|
await fs.mount("/downloads", root="downloads")
|
||||||
|
|
||||||
|
# Mount with a custom ID to track different directories.
|
||||||
|
await fs.mount("/project", id="my-project")
|
||||||
|
```
|
||||||
|
|
||||||
|
If called during a user interaction (like a button click), the
|
||||||
|
permission dialog may be skipped if permission was previously granted.
|
||||||
|
"""
|
||||||
|
js.console.warn("experimental pyscript.fs ⚠️")
|
||||||
|
|
||||||
|
# Check if path is already mounted with a different ID.
|
||||||
|
mount_key = f"{path}@{id}"
|
||||||
|
if path in mounted:
|
||||||
|
# Path already mounted - check if it's the same ID.
|
||||||
|
for existing_key in mounted.keys():
|
||||||
|
if existing_key.startswith(f"{path}@") and existing_key != mount_key:
|
||||||
|
raise ValueError(
|
||||||
|
f"Path '{path}' is already mounted with a different ID. "
|
||||||
|
f"Unmount it first or use a different path."
|
||||||
|
)
|
||||||
|
|
||||||
|
details = None
|
||||||
|
handler = None
|
||||||
|
|
||||||
|
options = {"id": id, "mode": mode}
|
||||||
|
if root != "":
|
||||||
|
options["startIn"] = root
|
||||||
|
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
fs_handler = sync_with_worker.storeFSHandler(mount_key, to_js(options))
|
||||||
|
|
||||||
|
# Handle both async and SharedArrayBuffer use cases.
|
||||||
|
if isinstance(fs_handler, bool):
|
||||||
|
success = fs_handler
|
||||||
|
else:
|
||||||
|
success = await fs_handler
|
||||||
|
|
||||||
|
if success:
|
||||||
|
idbm = IDBMap.new(_fs.NAMESPACE)
|
||||||
|
details = await idbm.get(mount_key)
|
||||||
|
handler = await _check_permission(details)
|
||||||
|
if handler is None:
|
||||||
|
# Force await in either async or sync scenario.
|
||||||
|
await js.Promise.resolve(sync_with_worker.getFSHandler(details.options))
|
||||||
|
handler = details.handler
|
||||||
|
else:
|
||||||
|
raise RuntimeError(_fs.ERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
success = await _fs.idb.has(mount_key)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
details = await _fs.idb.get(mount_key)
|
||||||
|
handler = await _check_permission(details)
|
||||||
|
if handler is None:
|
||||||
|
handler = await _fs.getFileSystemDirectoryHandle(details.options)
|
||||||
|
else:
|
||||||
|
js_options = to_js(options)
|
||||||
|
handler = await _fs.getFileSystemDirectoryHandle(js_options)
|
||||||
|
details = {"handler": handler, "options": js_options}
|
||||||
|
await _fs.idb.set(mount_key, to_js(details))
|
||||||
|
|
||||||
|
mounted[path] = await interpreter.mountNativeFS(path, handler)
|
||||||
|
|
||||||
|
|
||||||
|
async def sync(path):
|
||||||
|
"""
|
||||||
|
Synchronise the virtual and local filesystems for a mounted `path`.
|
||||||
|
|
||||||
|
This ensures all changes made in the browser's virtual filesystem are
|
||||||
|
written to the user's local filesystem, and vice versa.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import fs
|
||||||
|
|
||||||
|
|
||||||
|
await fs.mount("/local")
|
||||||
|
|
||||||
|
# Make changes to files.
|
||||||
|
with open("/local/data.txt", "w") as f:
|
||||||
|
f.write("Important data")
|
||||||
|
|
||||||
|
# Ensure changes are written to local disk.
|
||||||
|
await fs.sync("/local")
|
||||||
|
```
|
||||||
|
|
||||||
|
This is automatically called by unmount(), but you may want to call
|
||||||
|
it explicitly to ensure data persistence at specific points.
|
||||||
|
"""
|
||||||
|
if path not in mounted:
|
||||||
|
raise KeyError(
|
||||||
|
f"Path '{path}' is not mounted. " f"Use fs.mount() to mount it first."
|
||||||
|
)
|
||||||
|
await mounted[path].syncfs()
|
||||||
|
|
||||||
|
|
||||||
|
async def unmount(path):
|
||||||
|
"""
|
||||||
|
Unmount a directory, specified by `path`, from the virtual filesystem.
|
||||||
|
|
||||||
|
This synchronises any pending changes and then removes the mount point,
|
||||||
|
freeing up memory. The `path` can be reused for mounting a different
|
||||||
|
directory.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import fs
|
||||||
|
|
||||||
|
|
||||||
|
await fs.mount("/local")
|
||||||
|
# ... work with files ...
|
||||||
|
await fs.unmount("/local")
|
||||||
|
|
||||||
|
# Path can now be reused.
|
||||||
|
await fs.mount("/local", id="different-folder")
|
||||||
|
```
|
||||||
|
|
||||||
|
This automatically calls `sync()` before unmounting to ensure no data
|
||||||
|
is lost.
|
||||||
|
"""
|
||||||
|
if path not in mounted:
|
||||||
|
raise KeyError(f"Path '{path}' is not mounted. Cannot unmount.")
|
||||||
|
|
||||||
|
await sync(path)
|
||||||
|
interpreter._module.FS.unmount(path)
|
||||||
|
del mounted[path]
|
||||||
|
|
||||||
|
|
||||||
|
async def revoke(path, id="pyscript"):
|
||||||
|
"""
|
||||||
|
Revoke filesystem access permission and unmount for a given
|
||||||
|
`path` and `id` combination.
|
||||||
|
|
||||||
|
This removes the stored permission for accessing the user's local
|
||||||
|
filesystem at the specified path and ID. Unlike `unmount()`, which only
|
||||||
|
removes the mount point, `revoke()` also clears the permission so the
|
||||||
|
user will be prompted again on next mount.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import fs
|
||||||
|
|
||||||
|
|
||||||
|
await fs.mount("/local", id="my-app")
|
||||||
|
# ... work with files ...
|
||||||
|
|
||||||
|
# Revoke permission (user will be prompted again next time).
|
||||||
|
revoked = await fs.revoke("/local", id="my-app")
|
||||||
|
|
||||||
|
if revoked:
|
||||||
|
print("Permission revoked successfully")
|
||||||
|
```
|
||||||
|
|
||||||
|
After revoking, the user will need to grant permission again and
|
||||||
|
select a directory when `mount()` is called next time.
|
||||||
|
"""
|
||||||
|
mount_key = f"{path}@{id}"
|
||||||
|
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
handler_exists = sync_with_worker.deleteFSHandler(mount_key)
|
||||||
|
else:
|
||||||
|
handler_exists = await _fs.idb.has(mount_key)
|
||||||
|
if handler_exists:
|
||||||
|
handler_exists = await _fs.idb.delete(mount_key)
|
||||||
|
|
||||||
|
if handler_exists:
|
||||||
|
interpreter._module.FS.unmount(path)
|
||||||
|
if path in mounted:
|
||||||
|
del mounted[path]
|
||||||
|
|
||||||
|
return handler_exists
|
||||||
247
core/src/stdlib/pyscript/media.py
Normal file
247
core/src/stdlib/pyscript/media.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
"""
|
||||||
|
This module provides classes and functions for interacting with
|
||||||
|
[media devices and streams](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API)
|
||||||
|
in the browser, enabling you to work with cameras, microphones,
|
||||||
|
and other media input/output devices directly from Python.
|
||||||
|
|
||||||
|
Use this module for:
|
||||||
|
|
||||||
|
- Accessing webcams for video capture.
|
||||||
|
- Recording audio from microphones.
|
||||||
|
- Enumerating available media devices.
|
||||||
|
- Applying constraints to media streams (resolution, frame rate, etc.).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import document
|
||||||
|
from pyscript.media import Device, list_devices
|
||||||
|
|
||||||
|
|
||||||
|
# Get a video stream from the default camera.
|
||||||
|
stream = await Device.request_stream(video=True)
|
||||||
|
|
||||||
|
# Display in a video element.
|
||||||
|
video = document.getElementById("my-video")
|
||||||
|
video.srcObject = stream
|
||||||
|
|
||||||
|
# Or list all available devices.
|
||||||
|
devices = await list_devices()
|
||||||
|
for device in devices:
|
||||||
|
print(f"{device.kind}: {device.label}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Using media devices requires user permission. Browsers will show a
|
||||||
|
permission dialog when accessing devices for the first time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyscript import window
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
|
||||||
|
|
||||||
|
class Device:
|
||||||
|
"""
|
||||||
|
Represents a media input or output device.
|
||||||
|
|
||||||
|
This class wraps a browser
|
||||||
|
[MediaDeviceInfo object](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo),
|
||||||
|
providing Pythonic access to device properties like `ID`, `label`, and
|
||||||
|
`kind` (audio/video, input/output).
|
||||||
|
|
||||||
|
Devices are typically obtained via the `list_devices()` function in this
|
||||||
|
module, rather than constructed directly.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript.media import list_devices
|
||||||
|
|
||||||
|
|
||||||
|
# Get all available devices.
|
||||||
|
devices = await list_devices()
|
||||||
|
|
||||||
|
# Find video input devices (cameras).
|
||||||
|
cameras = [d for d in devices if d.kind == "videoinput"]
|
||||||
|
|
||||||
|
# Get a stream from a specific camera.
|
||||||
|
if cameras:
|
||||||
|
stream = await cameras[0].get_stream()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""
|
||||||
|
Create a Device wrapper around a MediaDeviceInfo `device`.
|
||||||
|
"""
|
||||||
|
self._device_info = device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
"""
|
||||||
|
Unique identifier for this device.
|
||||||
|
|
||||||
|
This `ID` persists across sessions but is reset when the user clears
|
||||||
|
cookies. It's unique to the origin of the calling application.
|
||||||
|
"""
|
||||||
|
return self._device_info.deviceId
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group(self):
|
||||||
|
"""
|
||||||
|
Group identifier for related devices.
|
||||||
|
|
||||||
|
Devices belonging to the same physical device (e.g., a monitor with
|
||||||
|
both a camera and microphone) share the same `group ID`.
|
||||||
|
"""
|
||||||
|
return self._device_info.groupId
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kind(self):
|
||||||
|
"""
|
||||||
|
Device type: `"videoinput"`, `"audioinput"`, or `"audiooutput"`.
|
||||||
|
"""
|
||||||
|
return self._device_info.kind
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
"""
|
||||||
|
Human-readable description of the device.
|
||||||
|
|
||||||
|
Example: `"External USB Webcam"` or `"Built-in Microphone"`.
|
||||||
|
"""
|
||||||
|
return self._device_info.label
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""
|
||||||
|
Support bracket notation for JavaScript interop.
|
||||||
|
|
||||||
|
Allows accessing properties via `device["id"]` syntax. Necessary
|
||||||
|
when Device instances are proxied to JavaScript.
|
||||||
|
"""
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def request_stream(cls, audio=False, video=True):
|
||||||
|
"""
|
||||||
|
Request a media stream with the specified constraints.
|
||||||
|
|
||||||
|
This is a class method that requests access to media devices matching
|
||||||
|
the given `audio` and `video` constraints. The browser will prompt the
|
||||||
|
user for permission if needed and return a `MediaStream` object that
|
||||||
|
can be assigned to video/audio elements.
|
||||||
|
|
||||||
|
Simple boolean constraints for `audio` and `video` can be used to
|
||||||
|
request default devices. More complex constraints can be specified as
|
||||||
|
dictionaries conforming to
|
||||||
|
[the MediaTrackConstraints interface](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import document
|
||||||
|
from pyscript.media import Device
|
||||||
|
|
||||||
|
|
||||||
|
# Get default video stream.
|
||||||
|
stream = await Device.request_stream()
|
||||||
|
|
||||||
|
# Get stream with specific constraints.
|
||||||
|
stream = await Device.request_stream(
|
||||||
|
video={"width": 1920, "height": 1080}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get audio and video.
|
||||||
|
stream = await Device.request_stream(audio=True, video=True)
|
||||||
|
|
||||||
|
# Use the stream.
|
||||||
|
video_el = document.getElementById("camera")
|
||||||
|
video_el.srcObject = stream
|
||||||
|
```
|
||||||
|
|
||||||
|
This method will trigger a browser permission dialog on first use.
|
||||||
|
"""
|
||||||
|
options = {}
|
||||||
|
if isinstance(audio, bool):
|
||||||
|
options["audio"] = audio
|
||||||
|
elif isinstance(audio, dict):
|
||||||
|
# audio is a dict of constraints (sampleRate, echoCancellation etc...).
|
||||||
|
options["audio"] = audio
|
||||||
|
if isinstance(video, bool):
|
||||||
|
options["video"] = video
|
||||||
|
elif isinstance(video, dict):
|
||||||
|
# video is a dict of constraints (width, height etc...).
|
||||||
|
options["video"] = video
|
||||||
|
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def load(cls, audio=False, video=True):
|
||||||
|
"""
|
||||||
|
!!! warning
|
||||||
|
**Deprecated: Use `request_stream()` instead.**
|
||||||
|
|
||||||
|
This method is retained for backwards compatibility but will be
|
||||||
|
removed in a future release. Please use `request_stream()` instead.
|
||||||
|
"""
|
||||||
|
return await cls.request_stream(audio=audio, video=video)
|
||||||
|
|
||||||
|
async def get_stream(self):
|
||||||
|
"""
|
||||||
|
Get a media stream from this specific device.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript.media import list_devices
|
||||||
|
|
||||||
|
|
||||||
|
# List all devices.
|
||||||
|
devices = await list_devices()
|
||||||
|
|
||||||
|
# Find a specific camera.
|
||||||
|
my_camera = None
|
||||||
|
for device in devices:
|
||||||
|
if device.kind == "videoinput" and "USB" in device.label:
|
||||||
|
my_camera = device
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get a stream from that specific camera.
|
||||||
|
if my_camera:
|
||||||
|
stream = await my_camera.get_stream()
|
||||||
|
```
|
||||||
|
|
||||||
|
This will trigger a permission dialog if the user hasn't already
|
||||||
|
granted permission for this device type.
|
||||||
|
"""
|
||||||
|
# Extract media type from device kind (e.g., "videoinput" -> "video").
|
||||||
|
media_type = self.kind.replace("input", "").replace("output", "")
|
||||||
|
# Request stream with exact device ID constraint.
|
||||||
|
options = {media_type: {"deviceId": {"exact": self.id}}}
|
||||||
|
return await self.request_stream(**options)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_devices():
|
||||||
|
"""
|
||||||
|
Returns a list of all media devices currently available to the browser,
|
||||||
|
such as microphones, cameras, and speakers.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript.media import list_devices
|
||||||
|
|
||||||
|
|
||||||
|
# Get all devices.
|
||||||
|
devices = await list_devices()
|
||||||
|
|
||||||
|
# Print device information.
|
||||||
|
for device in devices:
|
||||||
|
print(f"{device.kind}: {device.label} (ID: {device.id})")
|
||||||
|
|
||||||
|
# Filter for specific device types.
|
||||||
|
cameras = [d for d in devices if d.kind == "videoinput"]
|
||||||
|
microphones = [d for d in devices if d.kind == "audioinput"]
|
||||||
|
speakers = [d for d in devices if d.kind == "audiooutput"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The returned list will omit devices that are blocked by the document
|
||||||
|
[Permission Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy)
|
||||||
|
(microphone, camera, speaker-selection) or for
|
||||||
|
which the user has not granted explicit permission.
|
||||||
|
|
||||||
|
For security and privacy, device labels may be empty strings until
|
||||||
|
permission is granted. See
|
||||||
|
[this document](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices)
|
||||||
|
for more information about this web standard.
|
||||||
|
"""
|
||||||
|
device_infos = await window.navigator.mediaDevices.enumerateDevices()
|
||||||
|
return [Device(device_info) for device_info in device_infos]
|
||||||
250
core/src/stdlib/pyscript/storage.py
Normal file
250
core/src/stdlib/pyscript/storage.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
"""
|
||||||
|
This module wraps the browser's
|
||||||
|
[IndexedDB persistent storage](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||||
|
to provide a familiar Python dictionary API. Data is automatically
|
||||||
|
serialized and persisted, surviving page reloads and browser restarts.
|
||||||
|
|
||||||
|
Storage is persistent per origin (domain), isolated between different sites
|
||||||
|
for security. Browsers typically allow each origin to store up to 10-60% of
|
||||||
|
total disk space, depending on browser and configuration.
|
||||||
|
|
||||||
|
What this module provides:
|
||||||
|
|
||||||
|
- A `dict`-like API (get, set, delete, iterate).
|
||||||
|
- Automatic serialization of common Python types.
|
||||||
|
- Background persistence with optional explicit `sync()`.
|
||||||
|
- Support for custom `Storage` subclasses.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import storage
|
||||||
|
|
||||||
|
|
||||||
|
# Create or open a named storage.
|
||||||
|
my_data = await storage("user-preferences")
|
||||||
|
|
||||||
|
# Use like a regular dictionary.
|
||||||
|
my_data["theme"] = "dark"
|
||||||
|
my_data["font_size"] = 14
|
||||||
|
my_data["settings"] = {"notifications": True, "sound": False}
|
||||||
|
|
||||||
|
# Changes are queued automatically.
|
||||||
|
# To ensure immediate write, sync explicitly.
|
||||||
|
await my_data.sync()
|
||||||
|
|
||||||
|
# Read values (survives page reload).
|
||||||
|
theme = my_data.get("theme", "light")
|
||||||
|
```
|
||||||
|
|
||||||
|
Common types are automatically serialized: `bool`, `int`, `float`, `str`, `None`,
|
||||||
|
`list`, `dict`, `tuple`. Binary data (`bytearray`, `memoryview`) can be stored as
|
||||||
|
single values but not nested in structures.
|
||||||
|
|
||||||
|
Tuples are deserialized as lists due to IndexedDB limitations.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Browsers typically allow 10-60% of total disk space per origin. Chrome
|
||||||
|
and Edge allow up to 60%, Firefox up to 10 GiB (or 10% of disk, whichever
|
||||||
|
is smaller). Safari varies by app type. These limits are unlikely to be
|
||||||
|
reached in typical usage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from polyscript import storage as _polyscript_storage
|
||||||
|
from pyscript.flatted import parse as _parse
|
||||||
|
from pyscript.flatted import stringify as _stringify
|
||||||
|
from pyscript.ffi import is_none
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_to_idb(value):
|
||||||
|
"""
|
||||||
|
Convert a Python `value` to an IndexedDB-compatible format.
|
||||||
|
|
||||||
|
Values are serialized using Flatted (for circular reference support)
|
||||||
|
with type information to enable proper deserialization. It returns a
|
||||||
|
JSON string representing the serialized value.
|
||||||
|
|
||||||
|
Will raise a TypeError if the value type is not supported.
|
||||||
|
"""
|
||||||
|
if is_none(value):
|
||||||
|
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", list(value)])
|
||||||
|
if isinstance(value, memoryview):
|
||||||
|
return _stringify(["memoryview", list(value)])
|
||||||
|
raise TypeError(f"Cannot serialize type {type(value).__name__} for storage.")
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_from_idb(value):
|
||||||
|
"""
|
||||||
|
Convert an IndexedDB `value` back to its Python representation.
|
||||||
|
|
||||||
|
Uses type information stored during serialization to reconstruct the
|
||||||
|
original Python type.
|
||||||
|
"""
|
||||||
|
kind, data = _parse(value)
|
||||||
|
|
||||||
|
if kind == "null":
|
||||||
|
return None
|
||||||
|
if kind == "generic":
|
||||||
|
return data
|
||||||
|
if kind == "bytearray":
|
||||||
|
return bytearray(data)
|
||||||
|
if kind == "memoryview":
|
||||||
|
return memoryview(bytearray(data))
|
||||||
|
# Fallback for all other types.
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Storage(dict):
|
||||||
|
"""
|
||||||
|
A persistent dictionary backed by the browser's IndexedDB.
|
||||||
|
|
||||||
|
This class provides a dict-like interface with automatic persistence.
|
||||||
|
Changes are queued for background writing, with optional explicit
|
||||||
|
synchronization via `sync()`.
|
||||||
|
|
||||||
|
Inherits from `dict`, so all standard dictionary methods work as expected.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import storage
|
||||||
|
|
||||||
|
|
||||||
|
# Open a storage.
|
||||||
|
prefs = await storage("preferences")
|
||||||
|
|
||||||
|
# Use as a dictionary.
|
||||||
|
prefs["color"] = "blue"
|
||||||
|
prefs["count"] = 42
|
||||||
|
|
||||||
|
# Iterate like a dict.
|
||||||
|
for key, value in prefs.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
|
||||||
|
# Ensure writes complete immediately.
|
||||||
|
await prefs.sync()
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes you may need to subclass `Storage` to add custom behavior:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import storage, Storage, window
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingStorage(Storage):
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
window.console.log(f"Setting {key} = {value}")
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
|
my_store = await storage("app-data", storage_class=LoggingStorage)
|
||||||
|
my_store["test"] = 123 # Logs to console.
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, store):
|
||||||
|
"""
|
||||||
|
Create a Storage instance wrapping an IndexedDB `store` (a JS
|
||||||
|
proxy).
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
{key: _convert_from_idb(value) for key, value in store.entries()}
|
||||||
|
)
|
||||||
|
self._store = store
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"""
|
||||||
|
Delete an item from storage via its `key`.
|
||||||
|
|
||||||
|
The deletion is queued for persistence. Use `sync()` to ensure
|
||||||
|
immediate completion.
|
||||||
|
"""
|
||||||
|
self._store.delete(key)
|
||||||
|
super().__delitem__(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""
|
||||||
|
Set a `key` to a `value` in storage.
|
||||||
|
|
||||||
|
The change is queued for persistence. Use `sync()` to ensure
|
||||||
|
immediate completion. The `value` must be a supported type for
|
||||||
|
serialization.
|
||||||
|
"""
|
||||||
|
self._store.set(key, _convert_to_idb(value))
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Remove all items from storage.
|
||||||
|
|
||||||
|
The `clear()` operation is queued for persistence. Use `sync()` to ensure
|
||||||
|
immediate completion.
|
||||||
|
"""
|
||||||
|
self._store.clear()
|
||||||
|
super().clear()
|
||||||
|
|
||||||
|
async def sync(self):
|
||||||
|
"""
|
||||||
|
Force immediate synchronization to IndexedDB.
|
||||||
|
|
||||||
|
By default, storage operations are queued and written asynchronously.
|
||||||
|
Call `sync()` when you need to guarantee changes are persisted immediately,
|
||||||
|
such as before critical operations or page unload.
|
||||||
|
|
||||||
|
```python
|
||||||
|
store = await storage("important-data")
|
||||||
|
store["critical_value"] = data
|
||||||
|
|
||||||
|
# Ensure it's written before proceeding.
|
||||||
|
await store.sync()
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a blocking operation that waits for IndexedDB to complete
|
||||||
|
the write.
|
||||||
|
"""
|
||||||
|
await self._store.sync()
|
||||||
|
|
||||||
|
|
||||||
|
async def storage(name="", storage_class=Storage):
|
||||||
|
"""
|
||||||
|
Open or create persistent storage with a unique `name` and optional
|
||||||
|
`storage_class` (used to extend the default `Storage` based behavior).
|
||||||
|
|
||||||
|
Each storage is isolated by name within the current origin (domain).
|
||||||
|
If the storage doesn't exist, it will be created. If it does exist,
|
||||||
|
its current contents will be loaded.
|
||||||
|
|
||||||
|
This function returns a `Storage` instance (or custom subclass instance)
|
||||||
|
acting as a persistent dictionary. A `ValueError` is raised if `name` is
|
||||||
|
empty or not provided.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import storage
|
||||||
|
|
||||||
|
|
||||||
|
# Basic usage.
|
||||||
|
user_data = await storage("user-profile")
|
||||||
|
user_data["name"] = "Alice"
|
||||||
|
user_data["age"] = 30
|
||||||
|
|
||||||
|
# Multiple independent storages.
|
||||||
|
settings = await storage("app-settings")
|
||||||
|
cache = await storage("api-cache")
|
||||||
|
|
||||||
|
# With custom Storage class.
|
||||||
|
class ValidatingStorage(Storage):
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise TypeError("Keys must be strings")
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
|
validated = await storage("validated-data", ValidatingStorage)
|
||||||
|
```
|
||||||
|
|
||||||
|
Storage names are automatically prefixed with `"@pyscript/"` to
|
||||||
|
namespace them within IndexedDB.
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
raise ValueError("Storage name must be a non-empty string")
|
||||||
|
|
||||||
|
underlying_store = await _polyscript_storage(f"@pyscript/{name}")
|
||||||
|
return storage_class(underlying_store)
|
||||||
79
core/src/stdlib/pyscript/util.py
Normal file
79
core/src/stdlib/pyscript/util.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
This module contains general-purpose utility functions that don't fit into
|
||||||
|
more specific modules. These utilities handle cross-platform compatibility
|
||||||
|
between Pyodide and MicroPython, feature detection, and common type
|
||||||
|
conversions:
|
||||||
|
|
||||||
|
- `as_bytearray`: Convert JavaScript `ArrayBuffer` to Python `bytearray`.
|
||||||
|
- `NotSupported`: Placeholder for unavailable features in specific contexts.
|
||||||
|
- `is_awaitable`: Detect `async` functions across Python implementations.
|
||||||
|
|
||||||
|
These utilities are primarily used internally by PyScript but are available
|
||||||
|
for use in application code when needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import js
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def as_bytearray(buffer):
|
||||||
|
"""
|
||||||
|
Given a JavaScript `ArrayBuffer`, convert it to a Python `bytearray` in a
|
||||||
|
MicroPython friendly manner.
|
||||||
|
"""
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
size = ui8a.length
|
||||||
|
ba = bytearray(size)
|
||||||
|
for i in range(size):
|
||||||
|
ba[i] = ui8a[i]
|
||||||
|
return ba
|
||||||
|
|
||||||
|
|
||||||
|
class NotSupported:
|
||||||
|
"""
|
||||||
|
Small helper that raises exceptions if you try to get/set any attribute on
|
||||||
|
it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, error):
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "error", error)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<NotSupported {self.name} [{self.error}]>"
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
raise AttributeError(self.error)
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
raise AttributeError(self.error)
|
||||||
|
|
||||||
|
def __call__(self, *args):
|
||||||
|
raise TypeError(self.error)
|
||||||
|
|
||||||
|
|
||||||
|
def is_awaitable(obj):
|
||||||
|
"""
|
||||||
|
Returns a boolean indication if the passed in obj is an awaitable
|
||||||
|
function. This is interpreter agnostic.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
MicroPython treats awaitables as generator functions, and if
|
||||||
|
the object is a closure containing an async function or a bound method
|
||||||
|
we need to work carefully.
|
||||||
|
"""
|
||||||
|
from pyscript import config
|
||||||
|
|
||||||
|
if config["type"] == "mpy":
|
||||||
|
# MicroPython doesn't appear to have a way to determine if a closure is
|
||||||
|
# an async function except via the repr. This is a bit hacky.
|
||||||
|
r = repr(obj)
|
||||||
|
if "<closure <generator>" in r:
|
||||||
|
return True
|
||||||
|
# Same applies to bound methods.
|
||||||
|
if "<bound_method" in r and "<generator>" in r:
|
||||||
|
return True
|
||||||
|
# In MicroPython, generator functions are awaitable.
|
||||||
|
return inspect.isgeneratorfunction(obj)
|
||||||
|
|
||||||
|
return inspect.iscoroutinefunction(obj)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user